WPFアプリケーションを作成するにあたって、留意事項がありましたので、共有させていただきます。
XAMLデザイナーとは
XAMLデザイナーは、XAMLベースのアプリケーションのデザインを視覚的に行うための強力なツールであり、ユーザーインターフェイス(UI)を直感的に作成・編集するためのビジュアルインターフェイスを提供します。これにより、コードを直接編集することなく、ドラッグアンドドロップ操作でUI要素を配置したり、プロパティを設定することができます。
個人的によく使う機能としては以下3点ですが、他にもいろいろあります。
-
ビジュアルツリー
UI要素の階層構造を視覚的に表示し、要素の選択や編集を容易にします。 -
プロパティウィンドウ
選択したUI要素のプロパティを一覧表示し、値を編集することができます。 -
データバインディング
データソースとUI要素を簡単にバインドし、動的なデータ表示を実現します。
フック処理
フック処理とは、特定のイベントが発生した際に、システムやアプリケーションがそのイベントを捕捉し、独自の処理を行うためのメカニズムです。
主にWindowsプラットフォームで使用され、システム全体や特定のアプリケーションに対して、キーボードやマウスの入力、ウィンドウのメッセージなどを監視・制御するために利用される仕組みとなります。
フック処理は、以下のような用途で使用します。
-
キーボードやマウスの入力監視
キーロガーやマウスジェスチャーの実装。 -
ウィンドウメッセージの監視
ウィンドウの作成、破棄、移動、サイズ変更などのイベントを監視。 -
システム全体のイベント監視
システム全体のイベントを監視し、特定の条件に基づいて処理を行う。
フック処理は、通常、以下の手順で実装します。
- フックプロシージャの定義
イベントが発生した際に呼び出されるコールバック関数を定義します。 - フックの設定
SetWindowsHookEx関数を使用して、フックプロシージャをシステムに登録します。 - フックの解除
イベントの監視が不要になった場合、UnhookWindowsHookEx関数を使用してフックを解除します。
以下、キーボード入力を監視し、キーが押された際にそのキーコードをコンソールに出力するコード(C#)を記載します。
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;
class Program
{
private static LowLevelKeyboardProc _proc = HookCallback;
private static IntPtr _hookID = IntPtr.Zero;
public static void Main()
{
_hookID = SetHook(_proc);
Application.Run();
UnhookWindowsHookEx(_hookID);// [手順③]
}
private static IntPtr SetHook(LowLevelKeyboardProc proc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0); // [手順②]
}
}
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
// [手順①]
private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
{
int vkCode = Marshal.ReadInt32(lParam);
Console.WriteLine((Keys)vkCode);
}
return CallNextHookEx(_hookID, nCode, wParam, lParam);
}
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
}
WPFアプリケーションでキーフック処理を実行する時の注意
さて、ここから本題となります。
WPFにはDataContextというバインディングソースを指定するためのプロパティがあり、大抵はViewModelクラスを指定することが多いです。
このViewModelクラス内に、先ほどのキーフック処理を記載した状態で、XAMLのコードと画面を表示すると、デバッグやアプリ実行していないにも関わらず、その場でキーフック処理が適用されるようになります!
コードでは下記のようになります。
[MainWindow.xaml]
//viewModelのバインディング
<Label Content="WPFだよ" HorizontalAlignment="Center" VerticalAlignment="Center" Height="124" Width="300" FontSize="80"/>
[MainViewModel.cs]
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Input;
using System.Windows;
namespace WpfApp
{
public class MainViewModel
{
// フックプロシージャのデリゲート
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
private HookProc _proc;
private IntPtr _hookID = IntPtr.Zero;
// Win32 APIのインポート
[DllImport("user32.dll")]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string lpModuleName);
// フックの種類
private const int WH_KEYBOARD_LL = 13;
public MainViewModel()
{
_proc = HookCallback;
_hookID = SetHook(_proc);
}
~MainViewModel()
{
UnhookWindowsHookEx(_hookID);
}
private IntPtr SetHook(HookProc proc)
{
using (Process curProcess = Process.GetCurrentProcess())
using (ProcessModule curModule = curProcess.MainModule)
{
return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
}
}
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0 && wParam == (IntPtr)0x0100) // WM_KEYDOWN
{
return (IntPtr)1; // イベントを破棄 ⇒ キー入力を受け付けなくする。
}
return CallNextHookEx(_hookID, nCode, wParam, lParam);
}
}
}
フック処理の内容としては、キー入力されても受け付けないようにするというシンプルなものです。
このフック処理を記載したViewModelをDataContextに設定し、XAMLのコードと画面を表示してビルドを行うとアプリ画面が再構築されるのですが、バインドしているViewModelの処理も動くようになってしまうのです。上記のコードであれば、キー入力が受け付けなくなってしまうので、*コードの編集ができなくなる。。。という異常事態が発生してしまいます。
当時、開発中にこのような現象が起きたので、システムが壊れたのでは!?と内心あたふたしておりました。幸いマウスは動いていたので修正できましたが、マウス操作も受け付けなくしていたら、と思うと。。。
対策としては、条件付きコンパイルを使用して、開発中はキー入力を受け付けるようにし、ビルドされたアプリでは受け付けなくさせるような仕組みを作れば問題ありません。
また、XAMLコード上でViewModelのデータバインドをせず、xaml.csコード上でDataContextの設定をするでも問題ありません。XAMLコードとウィンドウの画面上で再構築された場合、xaml.csコード内に記載された処理は適用されないようです(つまり、ViewModel内の処理は適用されない)。
まとめ
WPFではXAMLデザイナーにより、画面のデザインを即座に表示されるという超便利!ではあるのですが、フック処理があるクラスをバインドしてしまうと、画面再構築された時にフック処理が適用されてしまうという現象が発生します。場合によっては詰む可能性があるので、お気を付けくださいませ。
以上です。よろしくお願いいたします。

