10
11

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.

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

Posted at

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

だけなら特に難しいこともなかったんですが。

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

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

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

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

普通に考えれば

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

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
テキストボックスにフォーカスが当たってて、尚且つテキストが全選択状態になってて、しかもIMEがONになってる状態。:fearful:

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

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

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

RawInput APIを使って取られる前に取る

IMEにイベント横取りされちゃってるので.NETからでは手も足もでません。(なぜ)
なのでデバイスから直接入力を受け取れるRawInput APIを使います。

まず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="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はちょっと長いですがこんな感じです。

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

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

10
11
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
10
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?