1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【WPF】ContextMenuのショートカットキーの処理をViewModelに書いてしまう

Last updated at Posted at 2022-09-09

動作

ContextMenu選択.png

ボタンを右クリックすると、ContextMenuが、KeyGestureのテキストと共に表示される。

  • Action A, Action B, Action Cがクリックされたことを検出する
  • メニューが開いている時、キーボード「A」「B」「C」が押されたことを検出する
  • ESCボタンが押されたら、ContextMenuが閉じる

例えば、キーボードBが押されると

Screenshot 2022-09-07 151955.png

こんな表示になるようにする。

プロジェクトファイル

実装

準備

動作環境についてはこの記事と同様とする
(.NET Framework 4.8.1+Visual Studio 2022で、Prism.Wpfを使用)

悩みどころ

通常、Controlにキーイベントを割り当てるときは、KeyBindingを使用することが多い

つまり、単にキーでBindingしたければ

MainWindow.xaml
<Button>
 <Button.InputBindings>
   <KeyBinding Key="A" Command="{Binding CommandKeyHandle}" />
 </Button.InputBindings>
</Button>

という風に書けばいいわけだが・・・

ここで、ContextMenuでは、実はItemsSourceが使用できる。
これはViewModel等で定義するContextMenuの中身そのものであるが、

例えばxamlをこう定義し

MainWindow.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をこう書いたとすると、

MainViewModel.cs
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'のショートカットキーを押されたときの処理を書く
 }

MenuViewModel.cs
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を利用して、ビヘイビアを登録する

MainWindow.xaml
<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を追加

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には以下を付与

MainViewModel.cs
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にその内容を記述してしまい、その挙動を制御する方が使い勝手が良い
1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?