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 を受信した場合の動作を仮想的な関数と見立てて 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_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 については、前処理をそのコントロールで行うものの(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" であり、オーバーライドは可能です。