【WPF】KeyBindingの方法色々

WPFで開発しているアプリケーションで、ショートカットキーをユーザが設定できるようにする(=固定値にできない)という仕様がありました。
どうすれば実現できるか、WPFにおけるショートカットキーの設定方法、もといKeyBindingの方法について色々調べたのでまとめておきます。

前提:WPFの概要

今回のプロジェクトは、PrismとReactivePropertyによってViewとViewModel間のやりとりをするものですが、本題とは関係ないので割愛します。
軽く動作の説明すると、ViewModelのCommandを呼ぶと、画面のテキストがYesとNoで切り替わるというものです。
全体のコードはこちら

MainWindow.xaml

<Window x:Class="KeyBindingTest.Views.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"
        xmlns:local="clr-namespace:KeyBindingTest"
        xmlns:prism="http://prismlibrary.com/"
        prism:ViewModelLocator.AutoWireViewModel="True"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <TextBlock VerticalAlignment="Center"
                   TextAlignment="Center"
                   FontSize="50"
                   Background="DarkCyan"
                   Foreground="White"
                   Width="250"
                   Visibility="Visible"
                   Text="{Binding DisplayText.Value}"/>
    </Grid>
</Window>

MainWindowViewModel.cs

public class MainWindowViewModel : BindableBase
{
    public MainWindowViewModel() 
    {
        DisplayText = new ReactivePropertySlim<string>("Yes!");
        Command = new ReactiveCommand();
        Command.Subscribe(ChangeText);
    }

    private void ChangeText()
    {
        if (m_isYes)
        {
            DisplayText.Value = "No!";
            m_isYes= false;
        }
        else
        {
            DisplayText.Value = "Yes!";
            m_isYes = true;
        }
    }

    public ReactivePropertySlim<string> DisplayText { get; set; }

    public ReactiveCommand Command { get; set; }

    private bool m_isYes = true;
}

① xamlファイルで指定

Viewの.xamlファイルに記載するケース。
キーの割り当てが決まっていて、変わることがない場合には、この方法が一番良いと思います。

<Window.InputBindings>および<KeyBinding>タグを記載して、KeyBindingのオプションでキーの指定および呼び出すコマンドを指定します。
キーの指定はKeyオプションを使用し、Alt・Control・Shift・WindowsのModifierKeys(修飾キー)も合わせて指定したい場合はModifiersオプションも使用します。
Gestureオプションでキーと修飾キーをまとめて設定できる場合もあるのですが、どんな組み合わせでも使えるわけではないようです(後述します)。

<Window x:Class="KeyBindingTest.Views.MainWindow"
        (略)>
    <Window.InputBindings>
        <!-- Keyオプションでキーを指定 -->
        <KeyBinding Key="H" Command="{Binding Command}"/>

        <!-- ModifierKeyも指定 -->
    <KeyBinding Key="H" Modifiers="Shift" Command="{Binding Command}"/>

        <!-- ModifierKeyを複数指定したい場合 -->
        <KeyBinding Key="H" Modifiers="Alt+Shift" Command="{Binding Command}"/>

        <!-- Gestureで指定できるケースもある -->
        <KeyBinding Gesture="Shift+Enter" Command="{Binding Command}"/>
    </Window.InputBindings>
    <Grid>
    (略)
    </Grid>
</Window>

② コードバインディングでKeyBindingする

①のxamlで行ったことを、コードバインディング(xaml.csファイル)で記述する場合です。
設定ファイルを読み込んでキーを指定する場合など、何かしら可変にする必要がある場合はこちらを使用することになると思います。

KeyBindingのインスタンスを作成し、各プロパティでキーやコマンドを指定します。使用するプロパティは先ほどと同様にKeyおよびModifiers、またはGestureです。
なお、こちらもGestureで指定できる組み合わせとできない組み合わせがあるようです。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // InputBindingsをコードバインディングで行う
        var windowKeyBinding = new KeyBinding();

        // GestureまたはKeyとModifiersで指定する
        windowKeyBinding.Key = Key.H;
        windowKeyBinding.Modifiers = ModifierKeys.Shift;
        // Modifierキーを複数設定したければ以下のように書く
        windowKeyBinding.Modifiers = (ModifierKeys.Shift | ModifierKeys.Alt);

        // Gestureプロパティで指定できるケースもある
        windowKeyBinding.Gesture = new KeyGesture(Key.Enter, ModifierKeys.Shift);

        // コマンドの指定
        windowKeyBinding.Command = (DataContext as MainWindowViewModel).Command;
        this.InputBindings.Add(windowKeyBinding);
    }
}

③ KeyDownイベントを使用

キーの押下をKeyDownイベントで拾い、押されたキーやキーの組み合わせが合致しているかメソッド内で判定するという方法です。

キーは、KeyEventArgsのKeyプロパティから押されているキーを取得するか、Keyboard.IsKeyDownで指定したキーが押下されているか否かを取得します。
修飾キーは、Keyboard.Modifiersで取得します。
なお、この方法ではModifierKey同士を複数指定することはできるのですが、複数の修飾キーと非修飾キーを指定することはできないようです。

この方法は前2つと異なり、修飾キーでないキーを複数押したときという条件が使用できます。そういった条件が必要なケースでは、この方法を使うことになると思います(そんな状況があるか分かりませんが)。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // キーダウンイベントを設定し、メソッド内で押下キーの判断
        this.KeyDown += Window_OnKeyDown;
    }

    private void Window_OnKeyDown(object sender, KeyEventArgs e)
    {

        // Keyboard.IsKeyDown(Key.K)でも
        if (Keyboard.Modifiers == ModifierKeys.Shift && e.Key == Key.K)
        {
            (DataContext as MainWindowViewModel).Command.Execute();
        }

        // 複数のModifierKeyを条件にしたい場合
        if (Keyboard.Modifiers == (ModifierKeys.Shift | ModifierKeys.Alt))
        {
            (DataContext as MainWindowViewModel).Command.Execute();
        }

        // ModifierKey複数と普通のキーを混ぜる場合は無理っぽい?
        // 以下の分岐は反応しない
        if (Keyboard.Modifiers == (ModifierKeys.Shift | ModifierKeys.Alt) && e.Key == Key.K)
        {
            (DataContext as MainWindowViewModel).Command.Execute();
        }

        // ModifierKeyじゃないキーを複数指定したい場合はIsKeyDownを並べていくのが唯一の方法?
        if (Keyboard.IsKeyDown(Key.K) && Keyboard.IsKeyDown(Key.V))
        {
            (DataContext as MainWindowViewModel).Command.Execute();
        }
    }
}

留意点

既に述べているように、Gestureでは指定できない組み合わせがあります(例えばHキーとShiftキーなど)。
なぜ指定できないのか、指定できる・できない組み合わせが何なのかは軽く調べただけでは分かりませんでした……。

KeyプロパティとModifierプロパティでそれぞれ指定すれば問題ないので、基本的にはGestureプロパティを使用せず、KeyとModifierを使用するようにすべきだと思います。

おまけ:設定ファイルでショートカットキーを指定する

設定ファイルとして、以下のような、キーの組み合わせとコマンドを指定するxmlファイルを作成します。
Key属性に、指定するキーの組み合わせを+で連結し、Valueには、コマンドの動作を区別するための値を指定します。

shortcut.xml

<?xml version="1.0" encoding="UTF-8" ?>
<shortcuts>
  <shortcut Key="Shift+Alt+A">Add</shortcut>
  <shortcut Key="Control+D">Delete</shortcut>
</shortcuts>

このxmlファイルを読み込んで、値を解析し、『② コードバインディングでKeyBindingする』を行っていきます。
なおEnum.Parseを使って文字列を列挙子の値に変換する都合上、Key属性で指定するキーは、Enumで指定されている名前にする必要があります(例えば数字の0ならD0、コントロールキーなら Ctrl ではなく Control といった具合)。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // 設定ファイルを読み込んでコードバインディングを行う
        XDocument doc = XDocument.Load(@"\shortcut.xml");
        foreach (var element in doc.Root.Elements())
        {
            var key = element.Attribute("Key").Value;
            var func = element.Value;

            var windowKeyBinding = new KeyBinding();
            var (mainKey, modifiers) = KeyFromString(key);

            windowKeyBinding.Key = mainKey;
            windowKeyBinding.Modifiers = modifiers;

            windowKeyBinding.Command = GetCommandForAction(func);
            this.InputBindings.Add(windowKeyBinding);
        }
    }

    // キーの組み合わせを取得
    private (Key, ModifierKeys) KeyFromString(string key)
    {
        string[] keyParts = key.Split('+');
        var mainKey = (Key)Enum.Parse(typeof(Key), keyParts[keyParts.Length - 1]);
        var modifiers = ModifierKeys.None;
        for (int i = 0; i < keyParts.Length - 1; i++)
        {
            modifiers |= (ModifierKeys)Enum.Parse(typeof(ModifierKeys), keyParts[i]);
        }
        return (mainKey, modifiers);
    }

    // 対応するコマンドを取得
    private ICommand GetCommandForAction(string func)
    {
        // 指定された機能に応じたコマンドを返す
        if (func == "Add")
        {
            return (DataContext as MainWindowViewModel).Command;
        }
        else
        {
            return (DataContext as MainWindowViewModel).Command;
        }
    }
}

以上、WPFのKeyBinding方法でした。

コメント

タイトルとURLをコピーしました