[WPF] TextBox に影響を与えないキーショートカット

久々に更新。今回は WPF ネタで書いてみる。

WPF でキーショートカットを作る

// MainWindow.xaml
<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="300">
    <Window.InputBindings>
        <KeyBinding Command="{Binding TestCommand}" Key="A"/>
    </Window.InputBindings>
    <StackPanel>
        <TextBox Margin="10" Text="Text" VerticalAlignment="Center" />
        <CheckBox Margin="10" Content="CanExecute" IsChecked="{Binding CanExecuteFlag}"  />
        <Label Margin="10" Content="{Binding ExecuteCount}" />
    </StackPanel>
</Window>
// MainWindow.xaml.cs
namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new WindowViewModel();
        }
    }
}
// WindowViewModel.cs
namespace WpfApp1
{
    // ショートカットで実行したいコマンド
    class TestCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;

        public TestCommand(WindowViewModel vm)
        {
            _vm = vm;
        }

        public bool CanExecute(object parameter)
        {
            return _vm.CanExecuteFlag;
        }

        public void Execute(object parameter)
        {
            _vm.ExecuteCount++;
        }

        private WindowViewModel _vm;
    }

    class WindowViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ICommand TestCommand
        {
            get
            {
                if(_testCommand == null)
                {
                    _testCommand = new TestCommand(this);
                }
                return _testCommand;
            }
        }
        ICommand _testCommand;


        // コマンド実行許可フラグ
        public bool CanExecuteFlag { get; set; }

        // コマンドが実行された回数
        public int ExecuteCount {
            get => _executeCount;
            set
            {
                _executeCount = value;
                OnPropertyChanged(nameof(ExecuteCount));
            }
        }
        private int _executeCount;


        protected  void OnPropertyChanged(string name)
        {
            if (PropertyChanged == null) return;

            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}

WPF でキーショートカットからコマンドを実行したい場合、大体こんな感じになると思う。
が、このとき、TextBox はショートカットに指定した A が一切入力できなくなってしまうという問題がある。

入力できなくなる原因

.Net はソースコードが公開されてるので読んでみる。

Reference Source

主にこの CommandManager が原因の様子。
この箇所でコマンドを実行する際に、

inputEventArgs.Handled = true;

となっているため、TextBox に入力イベントが渡る前にイベントが停止する。

RoutedCommand を使う

ただし、このソースコードを見る限り、

RoutedCommand を使用して ContinueRouting が true であれば、
コマンド実行できなかった場合に限り入力イベントを中断しない

ということがわかる。

ということで、先程のサンプルを RoutedCommand を通すように変更してみる。

// MainWindow.xaml
<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="200" Width="300">
    <Window.InputBindings>
        <!--
        <KeyBinding Command="{Binding TestCommand}" Key="A"/>
        -->
        <KeyBinding Command="{x:Static local:MainWindow.TestCommand}" Key="A"/>
    </Window.InputBindings>
    <StackPanel>
        <TextBox Margin="10" Text="Text" VerticalAlignment="Center" />
        <CheckBox Margin="10" Content="CanExecute" IsChecked="{Binding CanExecuteFlag}"  />
        <Label Margin="10" Content="{Binding ExecuteCount}" />
    </StackPanel>
</Window>
// MainWindow.xaml.cs
namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public readonly static RoutedCommand TestCommand = new RoutedCommand("TestCommand", typeof(MainWindow));

        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = new WindowViewModel();

            this.CommandBindings.Add(
                new CommandBinding(TestCommand,
                (s, e) => {
                    ((WindowViewModel)DataContext).TestCommand.Execute(null);
                },
                (s, e) => {
                    // これを必ず true にしておく
                    e.ContinueRouting = true;
                    e.CanExecute = ((WindowViewModel)DataContext).TestCommand.CanExecute(null);
                })
            );
        }
    }
}

コマンドが実行できない場合は、A が問題なく入力できることが確認できた。

追記

明確に TextBox 等の入力時にコマンドを実行させない場合は以下のようにしておくと良い

// テキスト入力中はコマンドを許可しない
e.CanExecute = (Keyboard.FocusedElement is TextBox || Keyboard.FocusedElement is TextBlock) ? false : true;

参考