바로 가기 키를 위한 KeyBinding이 있는 확장 MenuItem

개요



편의를 위해 앱에서 특정 MenuItem에 키보드 단축키(ex. Ctrl+O로 '열기')를 도입하는 경우가 많습니다.
그러나, WPF 표준의 MenuItem에서는 KeyGesture의 설명 표시는 할 수 있어도, 그 검출은 할 수 없습니다.
그 때문에 MenuItem과는 별도로, Window 바로 아래에 검출하는 KeyBinding를 써야 합니다.
이것은 유사한 설명을 멀리 떨어진 곳에 쓰게 되어 버그의 원인이 됩니다.
그래서 KeyBinding을받을 수있는 확장 MenuItem을 만들어이 문제를 해결합니다.

변경 전



실행 화면




이런 앱을 소재로 합니다.
Menu->Hoge를 선택하거나 Ctrl+H를 누르면 TextBox에 "-Hoge"가 추가됩니다.

보기



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"이나 공백으로되어있어 잘 움직이지 않습니다.

보기



사용할 때는 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의 답변

좋은 웹페이지 즐겨찾기