🐥

SetWindowSubclass によるウィンドウプロシージャーのカスタム

2023/04/21に公開

はじめに

標準のイベント等では処理できないウィンドウメッセージ(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 分もしないうちにアプリが落ちました。

主な改訂履歴

  • 2022/12/11:ブログ掲載。
  • 2023/04/21:Zenn 掲載。
  • 2023/08/19:サンプルプログラム更新(CsWin32 導入)。
  • 2024/01/01:微修正。

Discussion