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;
}
}
ただ業務アプリなんか作ってるとこれじゃダメなパターンがあるんですよお客さん。
そんなニッチに要求知らんとか言わないで見てって。
どんな時にダメなのか?
こんな状態です。
テキストボックスにフォーカスが当たってて、尚且つテキストが全選択状態になってて、しかもIMEがONになってる状態。
この状態だと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までのイベントもちゃんと取れるようになりました。
しかし無駄に長い。もっとスマートにならないのか。
やりたいことは単純なのに、この回り道感がWPFから人を遠ざける原因なんじゃないのか。