WPFアプリでF10に独自処理割り当て

# WPFアプリでF10に独自処理を割り当てたい
だけなら特に難しいこともなかったんですが。

- IMEをONにした状態
- テキストがフル選択状態

という条件下でF10キーが反応しないとダメって言われました。

「はっはっはっ、そんなの簡単にできるだろと」

**・・・できませんでした。**

## 普通に考えれば
WindowのPreviewKeyDownイベントで普通にF1～F12までファンクションキーが押されたイベントって取れますよね。
F10だけはシステムキー扱いなのでちょっと細工が必要ですが、こんな感じでいけます。

```c#:
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
    var key = e.SystemKey == Key.F10 ? Key.F10 : e.Key;
    switch (key)
    {
        case Key.F1:
            break;
        case Key.F2:
            break;
        case Key.F3:
            break;
        case Key.F10:
            break;
    }
}
```
ただ業務アプリなんか作ってるとこれじゃダメなパターンがあるんですよお客さん。
そんなニッチに要求知らんとか言わないで見てって。

## どんな時にダメなのか？
こんな状態です。
![001.png](https://qiita-image-store.s3.amazonaws.com/0/286138/dfa38c0f-cf75-1768-f7d7-b37a844463da.png)
テキストボックスにフォーカスが当たってて、尚且つテキストが全選択状態になってて、しかもIMEがONになってる状態。:fearful:

この状態だとF5～F9まではIMEにイベントを取られちゃってPreviewKeyDownに来てくれません。
ついでにF10も反応しなくなります。

**「Windowsの仕様です、ファンクションキー使うのやめましょう」**
**「・・・ダメ」**

そんな感じになったのでなんとかしてみました。

## RawInput APIを使って取られる前に取る
IMEにイベント横取りされちゃってるので.NETからでは手も足もでません。(なぜ)
なのでデバイスから直接入力を受け取れる[RawInput API](https://msdn.microsoft.com/ja-jp/library/windows/desktop/ms645536(v=vs.85).aspx)を使います。

まずxamlはこんな感じになってます。

```xml:
<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="150" Width="380" 
        Loaded="Window_Loaded"
        PreviewKeyDown="Window_PreviewKeyDown">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBox Grid.Row="0" Name="Text1" Width="300" Margin="10"
                 Text="あいうえおかきくけこ"
                 InputMethod.PreferredImeState="On" 
                 InputMethod.PreferredImeConversionMode="FullShape,Native" 
                 FontSize="24"/>
        <TextBlock Grid.Row="1" Name="Label1"
                   FontSize="24" TextAlignment="Center"/>
    </Grid>
</Window>
```

xaml.csはちょっと長いですがこんな感じです。

```c#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;

namespace WpfApp1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private HwndSource _hwndSource;
        private HwndSourceHook _hwndSourceHook;

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // RawInput登録
            IntPtr handle = new WindowInteropHelper(this).Handle;
            RawInputDevice[] devices = new RawInputDevice[1];
            devices[0].UsagePage = 0x01;    // Generic Desktop Controls
            devices[0].Usage = 0x06;        // Keyboard
            devices[0].Flags = 0x00000100;  // RIDEV_INPUTSINK
            devices[0].Target = handle;
            RegisterRawInputDevices(devices, 1, Marshal.SizeOf(typeof(RawInputDevice)));
            // WndProcフック
            _hwndSource = PresentationSource.FromVisual(this) as HwndSource;
            _hwndSourceHook = new HwndSourceHook(WndProc);
            _hwndSource.AddHook(_hwndSourceHook);

            Text1.Focus();
            Text1.SelectAll();
        }

        private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            List<Key> FuncKeys = new List<Key>() {
                Key.F1, Key.F2, Key.F3, Key.F4, Key.F5, Key.F6,
                Key.F7, Key.F8, Key.F9, Key.F10, Key.F11, Key.F12
            };
            if (FuncKeys.Contains(e.Key))
            {
                Label1.Text = e.Key.ToString();
                e.Handled = true;
            }
        }

        private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            switch (msg) {
                case WM_INPUT:
                    WMInput(hwnd, wParam, lParam);
                    break;
            }
            return IntPtr.Zero;
        }

        private static void WMInput(IntPtr hwnd, IntPtr wParam, IntPtr lParam)
        {
            MainWindow window = Application.Current.Windows.OfType<MainWindow>().SingleOrDefault(s => s.IsActive);
            if (window == null)
                return;
            IntPtr handle = new WindowInteropHelper(window).Handle;
            int headerSize = Marshal.SizeOf(typeof(RawInputHeader));
            int size = Marshal.SizeOf(typeof(RawInput));
            GetRawInputData(lParam, 0x10000003, out RawInput input, ref size, headerSize);
            RawKeyboard keyboard = input.Keyborad;

            if (VkKeys.ContainsKey(keyboard.VKey)) {
                Int32 imeRet = ImmAssociateContext(handle, 0);        // IMEを一時的に無効化
                RoutedEvent routedEvent = null;
                if (keyboard.Message == WM_KEYDOWN)
                    routedEvent = Keyboard.PreviewKeyDownEvent;
                else if (keyboard.Message == WM_KEYUP)
                    routedEvent = Keyboard.PreviewKeyUpEvent;
                if (routedEvent != null) {
                    // WPFのイベント生成
                    window.RaiseEvent(
                        new KeyEventArgs(
                            Keyboard.PrimaryDevice, PresentationSource.FromVisual(window), 0, VkKeys[keyboard.VKey]) { RoutedEvent = routedEvent });
                }
                ImmAssociateContext(handle, imeRet);                  // IMEを復元
            }
        }

        const int WM_INPUT = 0xFF;
        const int WM_KEYDOWN = 0x100;
        const int WM_KEYUP = 0x101;

        const int VK_F1 = 0x70;
        const int VK_F2 = 0x71;
        const int VK_F3 = 0x72;
        const int VK_F4 = 0x73;
        const int VK_F5 = 0x74;
        const int VK_F6 = 0x75;
        const int VK_F7 = 0x76;
        const int VK_F8 = 0x77;
        const int VK_F9 = 0x78;
        const int VK_F10 = 0x79;
        const int VK_F11 = 0x7A;
        const int VK_F12 = 0x7B;
        static Dictionary<int, Key> VkKeys = new Dictionary<int, Key>
        {
            { VK_F1, Key.F1 },
            { VK_F2, Key.F2 },
            { VK_F3, Key.F3 },
            { VK_F4, Key.F4 },
            { VK_F5, Key.F5 },
            { VK_F6, Key.F6 },
            { VK_F7, Key.F7 },
            { VK_F8, Key.F8 },
            { VK_F9, Key.F9 },
            { VK_F10, Key.F10 },
            { VK_F11, Key.F11 },
            { VK_F12, Key.F12 },
        };

        [DllImport("imm32.dll", SetLastError = true)]
        public static extern Int32 ImmAssociateContext(
            IntPtr hWnd,
            Int32 hIMC);

        [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 Keyborad;
        }
        private struct RawKeyboard
        {
            public short MakeCode;
            public short Flags;
            public short Reserved;
            public short VKey;
            public int Message;
            public long ExtrInformation;
        }
    }
}
```

F1～F12までのイベントはPreviewKeyDownに送っています。
普通のKeyDownイベントとWMInputから独自に送ってるイベントで二重に送られてきてしまいますが、その辺は努力しだいで如何様にもできるんじゃないかと思います。
テキストボックスもIMEの状態も変化させずにIMEの機能を殺しつつF5～F10までのイベントもちゃんと取れるようになりました。

![002.png](https://qiita-image-store.s3.amazonaws.com/0/286138/686ecd7d-133d-1e91-c74f-5eb66ee39a22.png)

しかし無駄に長い。もっとスマートにならないのか。
やりたいことは単純なのに、この回り道感がWPFから人を遠ざける原因なんじゃないのか。