SetWindowSubclass によるウィンドウプロシージャーのカスタム
はじめに
標準のイベント等では処理できないウィンドウメッセージ(WM_*)を処理したい場合、自前のウィンドウプロシージャー(WndProc)を作る必要があります。
C# では、WinForms(フォーム)や WPF なら、ウィンドウプロシージャーをカスタムする仕組みが用意されている(HwndSource)ので簡単なのですが、WinUI 3(Windows App SDK)には残念ながら今のところ(1.2 時点)そのような仕組みはないようです。
結局ネイティブ Windows API を使うしかないようですので、その方法をまとめました。Windows API なので WinUI 3 専用ではありませんが、出番としては WinUI 3 で開発するときくらいかと思います。
はやいとこ WinUI 3 にも簡単な仕組みが用意されることを祈っています……。
サンプルプログラムのソースコードは GitHub に上げてあります。
使用する API
ウィンドウプロシージャーをカスタムするのを「サブクラス化」と呼んでいるようですが(C# でいう派生クラスではありません)、マイクロソフトのドキュメントによれば、サブクラス化は新旧 2 種類あります。
旧バージョンでは SetWindowLong() or SetWindowLongPtr() と CallWindowProc() を使います。欠点は先のドキュメントにいろいろ書いてありますが、個人的にはそれよりも、元のウィンドウプロシージャーを自前で管理する分、手間が増えるのかなと思います。
新バージョンでは SetWindowSubclass() と DefSubclassProc() を使用します。元のウィンドウプロシージャーは気にしなくて済みます。
ここでは新バージョンを使います。
というか、新バージョンの情報が少なくて苦労したというのもあってここにまとめたというのもあります。
やりかた
ウィンドウメッセージを扱うので、NuGet で PInvoke.User32 を入れておきます。
カスタムウィンドウプロシージャーの型(SUBCLASSPROC コールバック関数)をデリゲートとして宣言しておきます。型の詳細はこちらに書いてあります。
internal delegate IntPtr SubclassProc(IntPtr hWnd, User32.WindowMessage msg, IntPtr wPalam, IntPtr lParam, IntPtr idSubclass, IntPtr refData);
カスタムウィンドウプロシージャーを作成します。
自分が処理したいメッセージの部分だけ書いて、あとは元のウィンドウプロシージャーに丸投げします。
private IntPtr CustomSubclassProc(IntPtr hWnd, User32.WindowMessage msg, IntPtr wPalam, IntPtr lParam, IntPtr idSubclass, IntPtr refData)
{
switch (msg)
{
case User32.WindowMessage.WM_HogeHoge:
(処理)
return IntPtr.Zero;
default:
// 関心のあるメッセージ以外は元のウィンドウプロシージャーにお任せ
return DefSubclassProc(hWnd, msg, wPalam, lParam);
}
}
カスタムウィンドウプロシージャーを登録します。
_subclassProc = new SubclassProc(CustomSubclassProc);
SetWindowSubclass(hWnd, _subclassProc, IntPtr.Zero, IntPtr.Zero);
注意点の 1 つめは、SetWindowSubclass() に渡すのはカスタムウィンドウプロシージャー本体を「インスタンス化」したもの、ということです。インスタンス化していない CustomSubclassProc も SetWindowSubclass() に渡せてしまいますが罠です。
注意点の 2 つめは、そのインスタンスをずっと(カスタムウィンドウプロシージャーが不要となるまで)持ち続ける、ということです。
これらを守らないと、特に 64 ビットバイナリ(x64)において、一見うまく動くようですが、メッセージ処理をこなしているうちにアプリが強制終了します。私はこれでハマりました。
強制終了時は集約エラーハンドラーにすら引っかかりませんでした。デバッガーでは以下の箇所で落ちていました。
global::Microsoft.UI.Xaml.Application.Start((p) => {
var context = new global::Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(global::Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
global::System.Threading.SynchronizationContext.SetSynchronizationContext(context);
new App();
});
サンプルプログラム
サンプルプログラム(GitHub に上げてあります)では、ウィンドウのタイトルバーにヘルプボタンを表示しています。
このコンテキストヘルプは WinUI 3 標準ではハンドルできないため、カスタムウィンドウプロシージャーで処理しています。
ヘルプボタンをクリックする度に、ウィンドウのサイズが変化します(アプリとしてはありえない動きですが……)。
ちなみに、SetWindowSubclass() にインスタンス化していない CustomSubclassProc を直接渡すようにコードを変更すると、うちの環境で x64 バイナリを実行すると、チェックボックス連打で 1 分もしないうちにアプリが落ちました。
AOT の場合
Native AOT(以降 AOT)を使用する場合、CsWin32 の allowMarshaling
を false
にする必要があり、その影響で記述を変更する必要があります。
1 つめの変更点は、サブクラス化部分(サンプルプログラムでは MainWindow コンストラクター内)でアンセーフコードを使う必要があります。
unsafe
{
delegate* unmanaged[Stdcall]<HWND, UInt32, WPARAM, LPARAM, nuint, nuint, LRESULT> subclassProc = &CustomSubclassProc;
PInvoke.SetWindowSubclass(hWnd, subclassProc, UIntPtr.Zero, UIntPtr.Zero);
}
2 つめの変更点は、CustomSubclassProc を static にして、かつ属性を付けることです。
[UnmanagedCallersOnly(CallConvs = new Type[] { typeof(CallConvStdcall) })]
private static LRESULT CustomSubclassProc(HWND hWnd, UInt32 msg, WPARAM wPalam, LPARAM lParam, nuint _1, nuint _2)
{
}
PInvoke.SetWindowSubclass() が Stdcall
なので、それに合わせて CallConvStdcall
を付けています。その際、static にする必要が生じます。
static にすると MainWindow のメンバーにはアクセスできなくなるため、static で MainWindow のインスタンスを保持しておくなどの一工夫が必要になってきます。
AOT でも本質は変わらないものの、記載は複雑になります。
サンプルプログラムは native-aot ブランチをどうぞ。
主な改訂履歴
- 2022/12/11:ブログ掲載。
- 2023/04/21:Zenn 掲載。
- 2023/08/19:サンプルプログラム更新(CsWin32 導入)。
- 2024/01/01:微修正。
- 2025/03/19:AOT の場合を記載。
Discussion