3
5

More than 1 year has passed since last update.

【WPF】F10キーをショートカットキーとして登録する方法【MVVM】

Last updated at Posted at 2022-04-03

実現したいこと

F10をショートカットキーに設定したい

  • F10をショートカットキーに設定し、ViewModelにある処理を実行したい。
  • この記事の冒頭にあるようにサクッと作りたいだけ。できないので色々試した。

環境

  • Windows10 Pro
  • .Net Core 3.1
  • WPF 3.0.6系
  • ReactiveProperty 8.0.5

目次

1. 方法1:Behaviorに登録する
2. 方法2:PreviewKeyDownに設定する
3. 方法3:WIN32 APIを使う

結論から言うと方法3のみ望んだ挙動になりました。その他の方法でも条件次第では動きそうです。

方法1:Behaviorに登録する

まっとうな方法の一つだと思われるBehaviorに登録する方法で試してみる。
しかし、F10だけはBehaviorのOnKeyDownまで届いてすらいない。

以下実装。

MainWindow.xaml

<Window x:Class="RegistCodeBehind.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:b="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:behaviors="clr-namespace:RegistCodeBehind"
        xmlns:vm="clr-namespace:RegistCodeBehind"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
    <b:Interaction.Behaviors>
        <behaviors:KeyboardBehavior
            KeyType="F9"
            KeyDown="{Binding F9Command}"/>
        <behaviors:KeyboardBehavior
            KeyType="F10"
            KeyDown="{Binding F10Command}"/>
    </b:Interaction.Behaviors>
    <StackPanel>
        <!--F9を押すと値が変わる-->
        <TextBlock Text="{Binding F9Text.Value}"/>
        <!--F9同様の書き方で実装してもF10では反応しない-->
        <TextBlock Text="{Binding F10Text.Value}"/>
    </StackPanel>
</Window>

MainWindowViewModel.cs

using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;

namespace RegistCodeBehind
{
    public class MainWindowViewModel
    {

        public ReactivePropertySlim<string> F9Text { get; } = new ReactivePropertySlim<string>("何も押されてないよ");
        public ReactivePropertySlim<string> F10Text { get; } = new ReactivePropertySlim<string>("何も押されてないよ");
        public ReactiveCommand F9Command { get; } = new ReactiveCommand();
        public ReactiveCommand F10Command { get; } = new ReactiveCommand();
        private CompositeDisposable _disposable = new CompositeDisposable();

       public MainWindowViewModel()
       {
           F9Command.Subscribe(OnF9Down)
               .AddTo(_disposable);
           F10Command.Subscribe(OnF10Down)
               .AddTo(_disposable);
       }

       private void OnF9Down()
       {
           F9Text.Value = "F9が押されたよ";
       }
       private void OnF10Down()
       {
           F10Text.Value = "F10が押されたよ";
       }
    }
}

KeyboardBehavior.cs

using System.Windows;
using System.Windows.Input;
using Microsoft.Xaml.Behaviors;

namespace RegistCodeBehind
{
    public class KeyboardBehavior : Behavior<FrameworkElement>
    {
        public static readonly DependencyProperty KeyTypeProperty =
            DependencyProperty.Register(
                nameof(KeyType),
                typeof(Key),
                typeof(KeyboardBehavior),
                new FrameworkPropertyMetadata());

        public static readonly DependencyProperty KeyDownProperty =
            DependencyProperty.Register(
                nameof(KeyDown),
                typeof(ICommand),
                typeof(KeyboardBehavior),
                new FrameworkPropertyMetadata());

        public Key KeyType
        {
            get => (Key) GetValue(KeyTypeProperty);
            set => SetValue(KeyTypeProperty, value);
        }

        public ICommand KeyDown
        {
            get => (ICommand) GetValue(KeyDownProperty);
            set => SetValue(KeyDownProperty, value);
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.KeyDown += OnKeyDown;
            var window = Application.Current.MainWindow;
            if (window == null) return;

            window.KeyDown += OnKeyDown;
        }

        private void OnKeyDown(object sender, KeyEventArgs e)
        {
            var isF9 = e.Key == KeyType;
            var canExecuteF9Command = KeyDown.CanExecute(null);

			// ブレークポイントを打ってもF10の時だけ来ない
            if (isF9 && canExecuteF9Command)
            {
                KeyDown.Execute(null);
            }
        }
    }
}

方法2:reviewKeyDownに設定する

とりあえず何かに吸われてるっぽいのでWindow_PreviewKeyDownでWindowが読み込まれたタイミングでイベントを発行してみます。

MainWindow.xaml

<Window x:Class="RegistCodeBehind.MainWindow"
		
		// 追加
        PreviewKeyDown="Window_PreviewKeyDown"/>

MainwWindow.xaml.cs

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

		// 追加
		private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
			{
				var vm = (MainWindowViewModel) DataContext;
				if (vm.F10Command.CanExecute())
				{
					vm.F10Command.Execute();
				}
			}

    }

するといけました。ちゃんとF10を押すとViewModelに登録したコマンドが実行され画面も変更されました!やったね!

………と思ったのですが、この方法UserControlの場合うまくいきません。

MainWindow.xaml

    <StackPanel>
        <!--F9を押すと値が変わる-->
        <TextBlock Text="{Binding F9Text.Value}"/>

		<!-- UserControlとして埋め込む -->
        <local:DisplayPage/>
    </StackPanel>

DisplayPage.xaml

<UserControl x:Class="RegistCodeBehind.DisplayPage"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:vm="clr-namespace:RegistCodeBehind"
             mc:Ignorable="d" 
             // UserControleが呼ばれるタイミングで発火
             PreviewKeyDown="UserControl_PreviewKeyDown"
             d:DesignHeight="450" d:DesignWidth="800">
             <UserControl.DataContext>
                 <vm:DisplayPageViewModel/>
             </UserControl.DataContext>

             <StackPanel>
                <TextBlock Text="{Binding F10Text.Value}"/>
             </StackPanel>
</UserControl>

DisplayPageViewModel.cs

using System.Reactive.Disposables;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;

// 先程のMainWindowViewModel.csにあったF10にかかわる宣言を移植しただけ
namespace RegistCodeBehind
{
    public class DisplayPageViewModel
    {
        public ReactivePropertySlim<string> F10Text { get; } = new ReactivePropertySlim<string>("何も押されてないよ");
        public ReactiveCommand F10Command { get; } = new ReactiveCommand();
        private CompositeDisposable _disposable = new CompositeDisposable();

        public DisplayPageViewModel()
        {
            F10Command.Subscribe(OnF10Down)
                .AddTo(_disposable);
        }

        private void OnF10Down()
        {
            F10Text.Value = "F10が押されたよ";
        }
    }
}

F10を押しても画面上には変化はありません。
しかしBehaviorのOnKeyDownメソッドの中には到達するようになりました!

ここで入力されたキーが何か見てみると、F9の場合はe.Key == F9であったのに対し、F10の場合はe.Key == Systemでした。
Pasted image 20220403041643.png

え?System?なんですかそれ?
WindowsではF10は特殊なキーらしく公式ドキュメント等を漁ってもそれらしい記述はないのですが、
どうもシステムに予約されたキーらしいです。

そういった「Winodwsに予約されたキー」がe.Key == Systemとなるのでしょうか。

何か参考になる文献等あれば教えていただきたいです。

とりあえずWiondowsではソフト側で使わない方が良い意見があちこちで散見されました。

しかしなんとか実現したい。

方法3:WIN32 APIを使う

するとMSDNの方で.NET Frameworkのネイティブコード(dll)を呼び出してキーボードの入力を生で受け取る方法を見つけたのでこちらを試してみます。
結論これで動くものは作れます。

MainWindow.xaml

<Window x:Class="RegistCodeBehind.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:b="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:behaviors="clr-namespace:RegistCodeBehind"
        xmlns:local="clr-namespace:RegistCodeBehind"
        // 追加
        Loaded="MainWindow_OnLoaded"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <b:Interaction.Behaviors>
        <behaviors:KeyboardBehavior
            KeyType="F9"
            KeyDown="{Binding F9Command}"/>
    </b:Interaction.Behaviors>
    <StackPanel>
        <TextBlock Text="{Binding F9Text.Value}"/>
        // x:Nameを追加
        <local:DisplayPage x:Name="DisplayPageControl"/>
    </StackPanel>
</Window>


MainWindow.xaml.cs

using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;

namespace RegistCodeBehind
{
    public partial class MainWindow : Window
    {
        private readonly DisplayPage _displayPageControl;

        public MainWindow()
        {
            InitializeComponent();
            // DisplayPageViewModelのコマンドを発火させるために取得
            _displayPageControl = this.DisplayPageControl;
        }

        private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)

        {
            // ショートカットキーを受け取るウィンドウを指定
            var handle = new WindowInteropHelper(this).Handle;

            // ショートカットキーを受け付けるウィンドウ、生入力を取りたいデバイスを指定する。
            var devices = new RawInputDevice[1];
            // デスクトップへの入力を指定
            devices[0].UsagePage = 0x01;
            // キーボードからの入力を指定 
            devices[0].Usage = 0x06;
            // ウィンドウが前面にいない場合もTargetへの入力を受け取るよう指定
            devices[0].Flags = 0x00000100;
            // 生入力を受け取るウィンドウの指定
            devices[0].Target = handle;
            RegisterRawInputDevices(devices, 1, Marshal.SizeOf(typeof(RawInputDevice)));

            // GetWndProcフック。常に上記RawInputDeviceの条件で入力を受け付ける。
            var hwndSourceHook = new HwndSourceHook(GetWndProc);
            var hwndSource = (HwndSource) PresentationSource.FromVisual(this);
            hwndSource?.AddHook(hwndSourceHook);
        }

        // F10の生入力を検知する用
        private IntPtr GetWndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            if (msg == WmInput) InputWm(lParam);

            return IntPtr.Zero;
        }

        // 受け取った生入力が任意の条件の時に処理を行う
        private void InputWm(IntPtr lParam)
        {
            var window = Application.Current.Windows.OfType<MainWindow>().SingleOrDefault(s => s.IsActive);
            if (window == null) return;

            var headerSize = Marshal.SizeOf(typeof(RawInputHeader));
            var size = Marshal.SizeOf(typeof(RawInput));
            GetRawInputData(lParam, 0x10000003, out RawInput input, ref size, headerSize);

            var vm = (DisplayPageViewModel) _displayPageControl.DataContext;
            var isF10 = input.Keyboard.VKey == VkF10;
            var isKeyDown = input.Keyboard.Message == WmKeyDown;
            var canExecuteFunction10 = vm.F10Command.CanExecute();
            if (isF10 && isKeyDown && canExecuteFunction10)
            {
                vm.F10Command.Execute();
            }
        }

        private const int WmInput = 0xFF;
        private const int WmKeyDown = 0x100;
        private const int VkF10 = 0x79;

        [DllImport("user32.dll")]
        private static extern int RegisterRawInputDevices(
            RawInputDevice[] devices,
            int number,
            int size);

        [DllImport("user32.dll")]
        private static extern int GetRawInputData(
            IntPtr rawInput,
            int command,
            out RawInput data,
            ref int size,
            int headerSize);

        private struct RawInputDevice
        {
            public short UsagePage;
            public short Usage;
            public int Flags;
            public IntPtr Target;
        }

        private struct RawInputHeader
        {
            public int Type;
            public int Size;
            public IntPtr Device;
            public IntPtr WParam;
        }

        private struct RawInput
        {
            public RawInputHeader Header;
            public RawKeyboard Keyboard;
        }

        private struct RawKeyboard
        {
            public short MakeCode;
            public short Flags;
            public short Reserved;
            public short VKey;
            public int Message;
            public long ExtrInformation;
        }
    }
}

これをMainWinodw.xaml.csに追加することでMainWindowキーの生入力を常に監視し、任意の条件で任意の処理を実行することができます。

上記コードはこちらを参考にさせていただきました。非常に参考になりました!ありがとうございます! :pray:

RawInputDevice構造体の各プロパティは公式Docが一番明快ですが一応日本語参考資料も置いておきます。

コードビハインドに書きたくない

そうですね。
DisplayPageViewModel辺りに書けると幾分棲み分けがわかりやすそうですね。
DisplayPageViewModelに記述する場合はMainWindow_OnLoaded内の記述をDisplayPageViewModelのコンストラクタで宣言し、GetWndProc()以下を移植すればいけそうです。

この時入力を受け取るウィンドウを下記のように宣言して取得するのですが、アプリケーションを立ち上げた直後の画面でショートカットを受け付けた場合(今回のサンプルコードのような場合)はWindowがIsActive == falseとなりNull例外が投げられてしまいます。

// MainWinodw.xamlに埋め込まれたUserControlでは IsActive==true にならない
var window = Application.Current.Windows.OfType<MainWindow>().SingleOrDefault(s => s.IsActive);
var handle = new WindowInteropHelper(window).Handle;

SingleOrDefault(s => s.IsEnable)として強引にWindowを取得してもハンドルのバイナリ値は0になってしまいGetWndProc()が回ることはありません。

正直このあたり、なぜ起動前からウィンドウのハンドルをとれないのかよくわかりません。
どなたかヒントを教えて頂きたいです。

ちなみに回避方法として、アプリを立ち上げた後に画面遷移した先であればウィンドウハンドルを取得できます。
そうなればViewModelに記述をまとめられあっちこっちから参照を取ってくる必要がなくスッキリしますね。

今後の調査予定...

どうもキー入力だけでなくどこに「何の」フォーカスがあたっているかという問題もあるそうです。

WPF では、キーボードフォーカスと論理フォーカスという主な概念が2つあります。 キーボード フォーカスはキーボード入力を受け取る要素を指し、論理フォーカスはフォーカスを持つフォーカス範囲内の要素を指します

MSDNの中でも

ただF10を2回以上押下すると2回目以降はイベントが取れるという、いやらしい動きをしますね。

とありこれはメニューバーがあるアプリでUserControl_PreviewKeyDownにキーイベントを仕込んだ時に発生したので何かアリそう…(今回の小さいアプリではそもそもUserControl_PreviewKeyDownでイベントが発火しなかったので再現条件がよくわからないのですが…)。

まとめ

正直F10キーのためにここまで七面倒なことな記述を書いて他のショートカットキーと違う実装をしなければならないのか甚だ疑問です。

コードビハインドでの実装で書いたようにKeyEventArgs.key == systemなだけなのでUserControlでもそれができればいい…それだけのことなのですが……。

もっと簡潔な方法があればどなたかご教授くださいm(_ _)m

3
5
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
3
5