はじめに
本記事では、MVVM 構成 で WPF のボタン要素から以下のようなコンテキストメニューを表示する方法を解説します。
ボタンを右クリックするとコンテキストメニューが表示されます。
画面のレイアウト
MainWindow.xaml
を以下のようにコーディングします。
<Window x:Class="MVVM.ButtonContextMenu.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="450"
Height="450"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Grid>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button Width="200"
Height="50"
Margin="10,10,10,10"
Content="コンテキストメニュー"
Cursor="Hand">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding FirstMenuItemCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
Header="メニュー1" />
<MenuItem Command="{Binding SecondMenuItemCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
Header="メニュー2" />
</ContextMenu>
</Button.ContextMenu>
</Button>
</StackPanel>
</Grid>
</Window>
ここでのポイントは、Button
要素内にButton.ContextMenu
を作成し、コンテキストメニューを表示するように記述しているところです。
各メニューのボタンを実行した際の処理と、どのボタンが押されたのかを判定する処理を記述しています。
CommandParameter
CommandParameter
はICommandインターフェイス
を実装したコマンドに引数を渡すためのプロパティです。例えば、Button 要素がクリックされた時に、どのアクションをトリガーするかをICommand
で指定するときに使います。
今回のコードでは、ICommand
はDelegateCommand
と言うクラスに実装して使用します。
using System;
using System.Windows.Input;
namespace Button_Sample
{
/// <summary>
/// 任意の型を受け取るDelegateCommandクラス
/// </summary>
/// <typeparam name="T"></typeparam>
public class DelegateCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<bool> _canExecute;
public DelegateCommand(Action<T> execute) : this(execute, () => true)
{ }
public DelegateCommand(Action<T> execute, Func<bool> canExecute)
{
this._execute = execute;
this._canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
// RequerySuggested: コマンドを実行できるかどうかを変更する可能性のある条件が、
// CommandManagerによって検出された場合に発生する
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object parameter)
{
return this._canExecute();
}
public void Execute(object parameter)
{
this._execute((T)parameter);
}
}
/// <summary>
/// 任意の型を指定する必要がないDelegateCommandクラス
/// </summary>
public class DelegateCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public DelegateCommand(Action execute) : this(execute, () => true)
{ }
public DelegateCommand(Action execute, Func<bool> canExecute)
{
this._execute = execute;
this._canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
// RequerySuggested: コマンドを実行できるかどうかを変更する可能性のある条件が、
// CommandManagerによって検出された場合に発生する
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object parameter)
{
return this._canExecute();
}
public void Execute(object parameter)
{
this._execute();
}
}
}
MainWindow.xaml
でのRelativeSource={RelativeSource Self}
という記述は、Binding されたその要素自体に指定しています。つまり、CommandParameter
はその要素自体(自分自身)をバインドしているので、MenuItem
をクリックした際に、MenuItem
というオブジェクトが渡ってきます。
Command に Binding された DelegateCommand でMenuItem
を受け取るには以下のように記述します。
using System.Windows;
using System.Windows.Controls;
namespace MVVM.ButtonContextMenu
{
/// <summary>
/// MainWindowのViewModel。ウィンドウのコンポーネントのコマンドとインタラクションを処理します。
/// </summary>
public sealed class MainWindowViewModel : BindableBase
{
#region コマンド
/// <summary>
/// 一番目メニューアイテムの実行に関連するコマンド。
/// </summary>
public DelegateCommand<object> FirstMenuItemCommand
=> _firstMenuItemCommand ?? (_firstMenuItemCommand = new DelegateCommand<object>(OnFirstMenuItemFirst));
private DelegateCommand<object> _firstMenuItemCommand;
/// <summary>
/// 二番目のメニューアイテムの実行に関連するコマンド。
/// </summary>
public DelegateCommand<object> SecondMenuItemCommand
=> _secondMenuItemCommand ?? (_secondMenuItemCommand = new DelegateCommand<object>(OnSecondMenuItem));
private DelegateCommand<object> _secondMenuItemCommand;
#endregion
/// <summary>
/// コンストラクタ
/// </summary>
public MainWindowViewModel()
{ }
// ~ 中略 ~
}
}
MenuItem
で受け取ることも出来ますが、object
で受け取りMenuItem
でキャストした際に null でなければ処理を実行するようにします。
using System.Windows;
using System.Windows.Controls;
namespace MVVM.ButtonContextMenu
{
/// <summary>
/// MainWindowのViewModel。ウィンドウのコンポーネントのコマンドとインタラクションを処理します。
/// </summary>
public sealed class MainWindowViewModel : BindableBase
{
#region コマンド
/// <summary>
/// 一番目メニューアイテムの実行に関連するコマンド。
/// </summary>
public DelegateCommand<object> FirstMenuItemCommand
=> _firstMenuItemCommand ?? (_firstMenuItemCommand = new DelegateCommand<object>(OnFirstMenuItemFirst));
private DelegateCommand<object> _firstMenuItemCommand;
/// <summary>
/// 二番目のメニューアイテムの実行に関連するコマンド。
/// </summary>
public DelegateCommand<object> SecondMenuItemCommand
=> _secondMenuItemCommand ?? (_secondMenuItemCommand = new DelegateCommand<object>(OnSecondMenuItem));
private DelegateCommand<object> _secondMenuItemCommand;
#endregion
/// <summary>
/// コンストラクタ
/// </summary>
public MainWindowViewModel()
{ }
/// <summary>
/// メインのボタンを押したときにContextMenuを開く方法
/// </summary>
/// <param name="o"></param>
private void OnOpenContextMenu(object o)
{
var button = o as Button;
if (button != null)
{
button.ContextMenu.PlacementTarget = button;
button.ContextMenu.IsOpen = true;
}
}
/// <summary>
/// 最初のメニューアイテムのコマンド実行メソッド。そのチェック状態をトグルし、メッセージボックスを表示します。
/// </summary>
/// <param name="o">コマンドを呼び出したメニューアイテムオブジェクト。</param>
private void OnFirstMenuItemFirst(object o)
{
var meuItem = o as MenuItem;
if (meuItem != null)
{
if (!Status.IsMenuItemFirst)
{
Status.IsMenuItemFirst = !Status.IsMenuItemFirst;
meuItem.IsChecked = Status.IsMenuItemFirst;
MessageBox.Show("メニュー1");
return;
}
Status.IsMenuItemFirst = !Status.IsMenuItemFirst;
meuItem.IsChecked = Status.IsMenuItemFirst;
MessageBox.Show("メニュー1");
}
}
/// <summary>
/// 二番目のメニューアイテムのコマンド実行メソッド。そのチェック状態をトグルし、メッセージボックスを表示します。
/// </summary>
/// <param name="o">コマンドを呼び出したメニューアイテムオブジェクト。</param>
private void OnSecondMenuItem(object o)
{
var meuItem = o as MenuItem;
if (meuItem != null)
{
if (!Status.IsMenuItemSecond)
{
Status.IsMenuItemSecond = !Status.IsMenuItemSecond;
meuItem.IsChecked = Status.IsMenuItemSecond;
MessageBox.Show("メニュー2");
return;
}
Status.IsMenuItemSecond = !Status.IsMenuItemSecond;
meuItem.IsChecked = Status.IsMenuItemSecond;
MessageBox.Show("メニュー2");
}
}
}
}
メソッドで受け取った object という引数がMenuItem
オブジェクトであることが分かります。
MenuItem の状態管理
MenuItem
がチェックされていることを管理するために、Status.cs
と言うファイルを作成します。クラスはstatic
にします。
namespace MVVM.ButtonContextMenu
{
/// <summary>
/// ContextMenuのステータスを切り替える
/// </summary>
public static class Status
{
/// <summary>
/// メニュー1
/// </summary>
public static bool IsMenuItemFirst { get; set; }
/// <summary>
/// メニュー2
/// </summary>
public static bool IsMenuItemSecond { get; set; }
}
}
MainWindowViewModel
側で実装しているコードと合わせることで、チェックが入っていなければチェックを付け、付いて いればチェックを外す実装をしています。
ボタンをクリックした際にコンテキストメニューを表示させたい場合
右クリックではなくボタンをクリックした場合にコンテキストメニューを表示させたい場合は以下の手順で実装します。
- ボタンがクリックされたときのコマンドを ViewModel に登録する
CommandParameter
に Button 要素自身を渡す- ViewModel 側のメソッドでコンテキストメニューを開く処理を実装する
<!-- 省略 -->
<Button
Margin="10,10,10,10"
+ Command="{Binding OpenContextMenuCommand}"
+ CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
Content="コンテキスト
メニュー"
Cursor="Hand">
<Button.ContextMenu>
<ContextMenu>
<MenuItem
Command="{Binding FirstMenuItemCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
Header="メニュー1" />
<MenuItem
Command="{Binding SecondMenuItemCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
Header="メニュー2" />
</ContextMenu>
</Button.ContextMenu>
</Button>
</Grid>
</Window>
ViewModel 側に新しいメソッドとコマンドを登録します。
// 省略
/// <summary>
/// ボタンをクリックした際のメニューアイテムの実行に関連するコマンド。
/// </summary>
public DelegateCommand<object> OpenContextMenuCommand
=> _openContextMenuCommand ?? (_openContextMenuCommand = new DelegateCommand<object>(OnOpenContextMenu));
private DelegateCommand<object> _openContextMenuCommand;
/// <summary>
/// メインのボタンを押したときにContextMenuを開く方法
/// </summary>
/// <param name="o"></param>
private void OnOpenContextMenu(object o)
{
var button = o as Button;
if (button != null)
{
button.ContextMenu.PlacementTarget = button;
button.ContextMenu.IsOpen = true;
}
}
// 省略
映像だとわかりにくいですが、右クリックではなく通常の左クリックでコンテキストメニューが開くようになります。
おわりに
MVVM で実装すると XAML 側の記述で悩むことが多いですが、参考になれば幸いです。
今回作成したコードはこちらのリポジトリから参照できます。