動作
ボタンを右クリックすると、ContextMenuが、KeyGestureのテキストと共に表示される。
- Action A, Action B, Action Cがクリックされたことを検出する
- メニューが開いている時、キーボード「A」「B」「C」が押されたことを検出する
- ESCボタンが押されたら、ContextMenuが閉じる
例えば、キーボードBが押されると
こんな表示になるようにする。
プロジェクトファイル
実装
準備
動作環境についてはこの記事と同様とする
(.NET Framework 4.8.1+Visual Studio 2022で、Prism.Wpfを使用)
悩みどころ
通常、Controlにキーイベントを割り当てるときは、KeyBindingを使用することが多い
つまり、単にキーでBindingしたければ
<Button>
<Button.InputBindings>
<KeyBinding Key="A" Command="{Binding CommandKeyHandle}" />
</Button.InputBindings>
</Button>
という風に書けばいいわけだが・・・
ここで、ContextMenuでは、実はItemsSourceが使用できる。
これはViewModel等で定義するContextMenuの中身そのものであるが、
例えばxamlをこう定義し
<Button.ContextMenu>
<Button.InputBindings>
<!-- ※問題が発生する場所※
Key Aを押したとき、KeyCommandを実行するようにするが、
バインド時のパスがClickCommandと同じ場所ではなくなるので
エラーとなってしまう
-->
<KeyBinding Key="A" Command="{Binding KeyCommand}" />
</Button.InputBindings>
<ContextMenu ItemsSource="{Binding MenuItems}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<!-- ContextMenuに表示するテキストの内容を表す -->
<Setter Property="Header" Value="{Binding MenuText}" />
<!-- ContextMenuに表示されるキーの表示 -->
<Setter Property="InputGestureText" Value="{Binding KeyGesture}" />
<!-- ContextMenuをクリックしたときに実行されるコマンド -->
<Setter Property="Command" Value="{Binding ClickCommand}"></Setter>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Button.ContextMenu>
ViewModelをこう書いたとすると、
public class MainViewModel : BindableBase
{
public ObservableCollection<MenuViewModel> MenuItems
{
get;
set;
}
public MainViewModel()
{
MenuItems = new ObservableCollection<MenuViewModel>();
MenuItems.Add(new MenuViewModel()
{
MenuText = "Action A",
Key = Key.A,
KeyCommand = new DelegateCommand(RunActionAKey),
ClickCommand = new DelegateCommand(RunActionAClick)
});
MenuItems.Add(new MenuViewModel()
{
MenuText = "Action B",
Key = Key.B
}); MenuItems.Add(new MenuViewModel()
{
MenuText = "Action C",
Key = Key.C
});
}
}
private void RunActionAClick()
{
// Action Aをクリックしたときの処理を書く
}
private void RunActionAKey()
{
// ContextMenuが開いて'A'のショートカットキーを押されたときの処理を書く
}
public class MenuViewModel
{
public string MenuText
{
get;
set;
}
public Key Key
{
get;
set;
}
public string KeyGesture
{
get
{
return Key.ToString();
}
}
public ICommand KeyCommand
{
get;
set;
}
public ICommand ClickCommand
{
get;
set;
}
}
ContextMenuとしては表示が出来るのだが、
KeyCommandがMainViewModelのパスを参照して実行しようとして
コマンドが見つからないという状況になるので、実行時にエラーとなってしまう。
改善
単純に、ContextMenuがキー入力を感知するようにすればいいので、
Interaction.Behaviorsを利用して、ビヘイビアを登録する
<Button Content="Launch ContextMenu" HorizontalAlignment="Left" Margin="50,54,0,0" VerticalAlignment="Top" Height="25" Width="155">
<Button.ContextMenu>
<ContextMenu ItemsSource="{Binding MenuItems}">
<bh:Interaction.Behaviors>
<!-- CommandKeyHandleを実行するようにする(依存プロパティが必要) -->
<user_bh:ContextMenuKeyHandleBehavior CommandKeyHandle="{Binding CommandKeyHandle}" />
</bh:Interaction.Behaviors>
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding MenuText}" />
<Setter Property="InputGestureText" Value="{Binding KeyGesture}"></Setter>
<Setter Property="Command" Value="{Binding ClickCommand}"></Setter>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Button.ContextMenu>
</Button>
ここで、新たなビヘイビアとしてContextMenuKeyHandleBehavior.csを追加
public class ContextMenuKeyHandleBehavior : Behavior<ContextMenu>
{
/* 依存プロパティの定義 */
public static readonly DependencyProperty CommandKeyHandleProperty =
DependencyProperty.Register(nameof(CommandKeyHandle),
typeof(ICommand),
typeof(ContextMenuKeyHandleBehavior),
new PropertyMetadata(null));
public ICommand CommandKeyHandle
{
get
{
return (ICommand)GetValue(CommandKeyHandleProperty);
}
set
{
SetValue(CommandKeyHandleProperty, value);
}
}
/* ビヘイビア生成時の挙動 */
protected override void OnAttached()
{
base.OnAttached();
// キー入力があった場合に、ContextMenuの処理判定を行う
AssociatedObject.KeyDown += AssociatedObjectOnKeyDown;
}
/* ビヘイビア削除時の挙動 */
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.KeyDown -= AssociatedObjectOnKeyDown;
}
/* キーイベント入力 */
private void AssociatedObjectOnKeyDown(object sender, KeyEventArgs e)
{
// 例として、Escapeキーを押すと、ContextMenuが終了するようにする
if (e.Key == Key.Escape)
{
AssociatedObject.IsOpen = false;
return;
}
// もし所定のキーが入力されたとして、
// コマンドが定義されていて実行可能であれば、
// 実行し、ContextMenuが終了するようにする
if (CommandKeyHandle != null && CommandKeyHandle.CanExecute(e))
{
CommandKeyHandle.Execute(e);
AssociatedObject.IsOpen = false;
}
}
}
ユーザプロパティの追加方法については次などを参照
つまり、ビヘイビアに、依存プロパティとしてコマンドを入れておいて、
キー入力があった時に実行するという風にしているだけである。
ViewModelには以下を付与
private ICommand _command_key_handle;
public ICommand CommandKeyHandle
{
get
{
return _command_key_handle ?? (_command_key_handle = new DelegateCommand<KeyEventArgs>(HandleKeys, AcceptHandleKeys));
}
}
private bool AcceptHandleKeys(KeyEventArgs arg)
{
foreach (var item in MenuItems)
{
if (item.Key == arg.Key)
{
return true;
}
}
return false;
}
まとめ
- InputGestureTextはショートカットキーの情報を表示するだけ。キー入力のためには、XAMLファイルでKeyBindingを使えばいいが、XAMLファイルで決められたものしか使用することが出来ない
- もし、ContextMenuの内容が変わる可能性があるのであれば、ViewModelにその内容を記述してしまい、その挙動を制御する方が使い勝手が良い