C# で作る Windows アプリケーションにおけるキーイベントの流れ

前置き

C# (.NET Framework) で作る Windows アプリケーションのキーイベント処理の流れを、自分にしか分からない可能性が高い擬似コードで書き残した備忘録です。

Windows システムにおけるキーイベントは複数のウィンドウメッセージで構成されます。たとえば短く A のキーを押した場合(CapsLockはOFFとする)、フォーカスを持ったウィンドウに対して WM_KEYDOWN、WM_CHAR、WM_KEYUP の順でメッセージが送信されます。

.NET Framework の Windows アプリケーションにおけるキーボード入力の処理は、まず前処理を PreProcessMessage で行い、続いて WndProc を実行、その後 ProcessKeyMessage で実処理を行う、という流れになっています。この一連の流れは WM_KEYDOWN、WM_CHAR、WM_KEYUP のそれぞれに対して行われます。

WM_KEYDOWN に対する処理

フォームに追加されたコントロールが WM_KEYDOWN を受信した場合の動作を仮想的な関数と見立てて C# 風の擬似コードで表してみます。関数の定義を呼び出し文の直後に展開しているような感じです。

// WM_KEYDOWN を受信した時の動作
bool ProcessKeyDownEvent()
{
    // キー入力を前処理
    bool done = PreProcessMessage()
    {
        // アクセレレータやショートカットキーに割り当てられていればそれを実行
        bool done = ProcessCmdKey();
        if( done )
        {
            // 割り当てられており実行したので、キー処理を終了
            return true;
        }
        
        // このコントロールへの入力として処理されるキーか確認
        if( IsInputKey() )
        {
            // 入力として扱う=前処理で他のウィンドウに横取りされるのは困る。
            // ということで、前処理をキャンセル
            return false;
        }
        
        // フォーカス移動などが実行される可能性があるため、
        // 親へさかのぼってフォームへ伝える
        bool done = ProcessDialogKey();
        if( done )
        {
            // フォーカス移動などをフォームが実行したので、キー処理を終了
            return true;
        }
    };
    if( done )
    {
        // 前処理が実行されたため、これ以降は何もしない
        return;
    }
    
    // ウィンドウプロシージャを実行
    WndProc()
    {
        // キー入力を実処理
        ProcessKeyMessage()
        {
            // キー処理をフォームにプレビューさせる
            // (本当は親のProcessKeyPreviewがその親のProcessKeyPreviewを呼ぶ形で
            //   さかのぼってフォームまで伝えるが、通常はフォームまでの過程で何もしない)
            bool done = OwnerForm.ProcessKeyPreview()
            {
                OwnerForm.ProcessKeyEventArgs()
                {
                    OwnerForm.OnKeyDown();
                };
            };
            if( done )
            {
                // フォームがキーイベントを発生させたため、
                // コントロールは何もしない
                return;
            }
            
            // 今度はこのコントロールがキーイベントを適宜発生させる
            ProcessKeyEventArgs()
            {
                OnKeyDown();
            };
        };
    };
}

WM_CHAR に対する処理

前処理が WM_KEYDOWN と違うと思っていたのですが、整理して見直すと、やっている事は同じですね。

ProcessWmCharEvent()
{
    // キー入力を前処理
    bool done = PreProcessMessage()
    {
        // このコントロールへの入力として処理されるキーか確認
        if( IsInputChar() )
        {
            // 入力として扱う=前処理で他のウィンドウに横取りされるのは困る。
            // ということで、前処理をキャンセル
            return false;
        }
        
        // フォーカス移動などが実行される可能性があるため、
        // 親へさかのぼってフォームへ伝える
        bool done = ProcessDialogChar()
        {
            // このコントロールがニーモニックとして扱えるキーなら処理
            bool done = ProcessMnemonic();
            if( done )
            {
                // ニーモニックとして処理したので、キー処理を終了
                return true;
            }
            
            // 親へとさかのぼっていく
            Parent.ProcessDialogChar()
            {
                ...
            };
        };
        if( done )
        {
            // フォーカス移動などをフォームが実行したので、キー処理を終了
            return true;
        }
    };
    if( done )
    {
        // 前処理が実行されたため、ここ以降は何もしない
        return;
    }
    
    // ウィンドウプロシージャを実行
    WndProc()
    {
        // キー入力を実処理
        ProcessKeyMessage()
        {
            // キー処理をフォームにプレビューさせる
            // (本当は親のProcessKeyPreviewがその親のProcessKeyPreviewを呼ぶ形で
            //   さかのぼってフォームまで伝えるが、通常はフォームまでの過程で何もしない)
            bool done = OwnerForm.ProcessKeyPreview()
            {
                OwnerForm.ProcessKeyEventArgs()
                {
                    OwnerForm.OnKeyPress();
                };
            };
            if( done )
            {
                // フォームがキーイベントを発生させたため、
                // コントロールは何もしない
                return;
            }
            
            // 今度はこのコントロールがキーイベントを適宜発生させる
            ProcessKeyEventArgs()
            {
                OnKeyPress();
            };
        };
    };
}

WM_KEYUP に対する処理

WM_KEYUP については、前処理をそのコントロールで行うものの(PreProcessMessageが呼ばれる)、親ウィンドウへは一切イベントを伝えないようです。

ProcessKeyUpEvent()
{
    // キー入力を前処理
    PreProcessMessage()
    {
        // PreProcessMessage は呼ばれるものの、
        // 親へは伝えない
        return false;
    };
    
    // ウィンドウプロシージャを実行
    WndProc()
    {
        // キー入力を実処理
        ProcessKeyMessage()
        {
            // キー処理をフォームにプレビューさせる
            // (本当は親のProcessKeyPreviewがその親のProcessKeyPreviewを呼ぶ形で
            //   さかのぼってフォームまで伝えるが、通常はフォームまでの過程で何もしない)
            bool done = OwnerForm.ProcessKeyPreview()
            {
                OwnerForm.ProcessKeyEventArgs()
                {
                    OwnerForm.OnKeyUp();
                };
            };
            if( done )
            {
                // フォームがキーイベントを発生させたため、
                // コントロールは何もしない
                return;
            }
            
            // 今度はこのコントロールがキーイベントを適宜発生させる
            ProcessKeyEventArgs()
            {
                OnKeyUp();
            };
        };
    };
}

参考

本記事は自分で検証した他、@IT の記事を参考にして書きました。ちなみに、同記事にはProcessKeyMessage メソッドはアクセス修飾子が internal のため、(中略)オーバーライドすることができないとあるのですが、これは正しくありません。実際には、同メソッドのアクセス修飾子は "protected internal" であり、オーバーライドは可能です。