9
9

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 5 years have passed since last update.

ショートカットキーのためのKeyBinding付き拡張MenuItem

Last updated at Posted at 2017-06-04

#概要
利便性を考えて、アプリで特定のMenuItemにキーボードショートカット(ex. Ctrl+Oで「開く」)を導入することがよくあります。
しかし、WPF標準のMenuItemではKeyGestureの説明表示は出来ても、その検出は出来ません。
そのためMenuItemとは別に、Window直下に検出するKeyBindingを書かなければいけません。
これは同じような記述を離れた場所に書くことになり、バグの原因になります。
そこで、KeyBindingを受け取ることのできる拡張MenuItemを作ることで、この問題を解決します。

#変更前
##実行画面

スクリーンショット 2017-06-03 09.25.18.png
こんなアプリを題材にします。
Menu->Hogeを選択するか、Ctrl+Hを押すと、TextBoxに"-Hoge"が追記されます。

##View
ViewではGridで区切って上にMenu、下にTextBoxが置いてあるだけです。

MainWindow.xaml
<Window
    x:Class="MenuExt.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MenuExt"
    Width="525"
    Height="150">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Window.InputBindings>
        <KeyBinding Command="{Binding HogeCommand}" Gesture="Ctrl+H" />
    </Window.InputBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Menu Grid.Row="0">
            <MenuItem Header="File(_F)">
                <MenuItem
                    Header="Hoge"
                    Command="{Binding HogeCommand}"
                    InputGestureText="Ctrl+H" />
            </MenuItem>
        </Menu>

        <TextBox Grid.Row="1" Text="{Binding MyText.Value}" />
    </Grid>
</Window>

コードビハインドには何も書いていないので省略します。

##ViewModel
ViewModelは今回の本題にはあまり関係ありませんが、以下のとおりです。
HogeCommandが使用されたらMyTextに"-Hoge"と追記します。
ReactivePropertyを使用していますが、他のMVVMライブラリでも話は同じだと思います。

MainWindowViewModel
using Reactive.Bindings;
using System;

namespace MenuExt
{
    public class MainWindowViewModel
    {
        public ReactiveCommand HogeCommand { get; } = new ReactiveCommand();
        public ReactiveProperty<string> MyText { get; } = new ReactiveProperty<string>();

        public MainWindowViewModel()
        {
            HogeCommand.Subscribe(_ =>
                 Text.Value += "-Hoge");
        }
    }
}

#問題点
ViewにWindow直下のKeyBindingとMenuItemがあります。

<KeyBinding Command="{Binding HogeCommand}" Gesture="Ctrl+H" />
<MenuItem
    Header="Hoge"
    Command="{Binding HogeCommand}"
    InputGestureText="Ctrl+H" />

両者はともにHogeCommandとKeyGesture(*)を含んでいます。
今は短いから問題には感じませんが、これが膨大なMenuを持つアプリ(ex. VisualStudio)になったら、両者が食い違っていても、コードから発見するのは困難でしょう。

(*) MenuItemのInputGestureTextは単に指定したテキストをMenuの右側に表示するだけで、機能は持ちません。
InputGestureTextに書いたのに応答しない!という罠はこれを利用した全員がはまる通過儀礼のようなものです。
名前をGestureCaptionとかにしとけば、もう少し混乱が少なくて済んだのになぁ。

#解決策
この問題の解決で最初に思いつく方法はMenuItem自体にKeyBinding機能を持たせることです。
しかしKeyBindingはその時Focusがあるコントロールでしか検出できません。
つまりMenuItemに実装しても選択されている時以外は動作しない無意味なKeyBindingになってしまいます。
そこでKeyBinding自体は通常通りWindow直下で実装して、それをx:Nameで拡張MenuItemに渡してしまい、そこからCommandとKeyGestureを同期します。

##拡張MenuItemクラス
まずKeyBindingを受け取るMenuItemを継承した拡張MenuItemです。

MenuItemKeyBinded.cs
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace MenuExt
{
    /// <summary>
    /// キーバインド付きMenuItem
    /// </summary>
    public class MenuItemKeyBinded : MenuItem
    {
        /// <summary>
        /// KeyBinding依存関係プロパティ
        /// </summary>
        public KeyBinding KeyBind
        {
            get { return (KeyBinding)GetValue(KeyBindProperty); }
            set { SetValue(KeyBindProperty, value); }
        }
        public static readonly DependencyProperty KeyBindProperty =
            DependencyProperty.Register(nameof(KeyBind), typeof(KeyBinding), typeof(MenuItemKeyBinded),
                new PropertyMetadata(new KeyBinding(),
                    //KeyBindingが指定された時に呼ばれるコールバック
                    KeyBindPropertyChanged));

        private static void KeyBindPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var menuItemKB = d as MenuItemKeyBinded;
            var kb = e.NewValue as KeyBinding;
            //KeyBindingに結び付けられたコマンドをこのMenuItemのCommandに反映
            menuItemKB.Command = kb.Command;
            //KeyBindingのローカライズされた文字列("Ctrl"など)をこのMenuItemのInputGestureTextに反映
            menuItemKB.InputGestureText = (kb.Gesture as KeyGesture).GetDisplayStringForCulture(CultureInfo.CurrentCulture);
        }
    }
}

依存関係プロパティとしてKeyBindingを受け取り、中身を継承元MenuItemのCommandとInputGestureTextに渡しています。
InputGestureTextにはKeyGestureを現在のCultureでローカライズした文字列(ex. "Ctrl")を設定します。
なおKeyGestureのModifierKeysやDisplayStringを使用しても"Control"や空白になっており、うまく動きません。

##View
使用する際はWindowのKeyBinding側にCommandとGestureを書いて、x:Name経由で拡張MenuItemにKeyBindingごと渡します。

MainWindow.xaml
<Window
    x:Class="MenuExt.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MenuExt"
    Width="525"
    Height="150">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Window.InputBindings>
        <KeyBinding
            x:Name="HogeKeyBinding"
            Command="{Binding HogeCommand}"
            Gesture="Ctrl+H" />
    </Window.InputBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Menu Grid.Row="0">
            <MenuItem Header="File(_F)">
                <local:MenuItemKeyBinded
                    Header="Hoge"
                    KeyBind="{Binding ElementName=HogeKeyBinding}" />
            </MenuItem>
        </Menu>

        <TextBox Grid.Row="1" Text="{Binding MyText.Value}" />
    </Grid>
</Window>

ViewModelと実行結果は同じです。

#環境
VisualStudio2017
.NET Framework 4.6
C#6

#別解
今回はKeyBindingにx:Nameをつけ、KeyBinding経由でCommandとKeyGestureを共有しました。
別の方法として、Command側にKeyGestureを埋め込む方法もあります。

How to Display Working Keyboard Shortcut for Menu Items?-stackoverflow

中段あたりのCommandWithHotkeyの回答

9
9
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
9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?