問題
自作アプリの設定ダイアログで、ショートカットキーの設定をしようとしていました。
エディットコントロール上で ESC を押した時に、WM_KEYDOWN を処理して "Escape" という文字列を表示したいのです。
ですが、ESC や Enter を押した瞬間、ダイアログが閉じてしまいます。
原因
ESC や Enter の WM_KEYDOWN メッセージがシステム標準のアクセラレータテーブルによって IDCANCEL/IDOK の WM_COMMAND に変換されてしまっています。
システム標準のアクセラレータテーブルを操作する方法はありません。
TranslateAccelerator によって WM_COMMAND に変換される前に WM_KEYDOWN を処理する必要があります。
ダメな方法
MFC の CDialog::OnOK() や OnCancel() を実装しろというのが一番多く見つかりますが、これはだいたい嘘っぱちです。
ダイアログを閉じないだけで、エディットコントロールにWM_KEYDOWN が問題は解決していません。
また、OnOK() はともかく、OnCancel() は ESC のほか、Ctrl-Break でも送られてくるので見分けがつきません。
解法1
MFC の場合、CDialog::PreTranslateMessage が TranslateAccelerator の呼び出し前に呼ばれています。
このメソッドを実装して、ESC の WM_KEYDOWN を処理してしまえば良いはずです。
解法2
モーダルダイアログを Win32API の DialogBox(), DialogBoxParam() で表示している場合、解法1では解決できません。
DialogBox 関数の中でメッセージ処理が走っていて、標準的な方法では PreTranslateMessage のタイミングに割り込むことができないからです。MFC でも DialogBox を利用している所があれば同じでしょう。
結論から言うと、フックを使います。
アクセラレータテーブルでメッセージが処理されているということは、WM_KEYDOWN がメッセージ・キューに溜まっているということです。このメッセージキューからメッセージを取り出す瞬間に割り込んでやることで、TranslateAccelerator の前に WM_KEYDOWN を処理してやることができます。
HWND sm_hwndKeyEdit;
HHOOK hHook;
static LRESULT CALLBACK kbHookProc(int code, WPARAM wParam, LPARAM lParam) {
if (-1 < code) {
switch(wParam) {
case VK_RETURN:
case VK_CANCEL: // Ctrl-Break.
case VK_ESCAPE:
if ((lParam & (3 << 30)) == 0) { // on )key down...
if (GetFocus() == sm_hwndKeyEdit) {
SendMessage(sm_hwndKeyEdit, WM_KEYDOWN, wParam, lParam);
// cancel TranslateAccelerator call.
return TRUE;
}
}
}
}
return CallNextHookEx(nullptr, code, wParam, lParam);
}
static INT_PTR CALLBACK procAcceleratorEditDlg(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
HWND hwndKeyEdit = GetDlgItem(hDlg, IDC_EDIT1);
switch (message) {
case WM_INITDIALOG:
if (hwndKeyEdit) {
#pramga region hwndKeyEditのサブクラス化
略
#pragma endregion
sm_hwndKeyEdit = hwndKeyEdit;
hHook = SetWindowsHookEx(WH_KEYBOARD, kbHookProc, nullptr, GetCurrentThreadId());
}
break;
case WM_DESTROY:
if (hHook) {
UnhookWindowsHookEx(pThis->hHook);
hHook = nullptr;
}
break;
// etc...
}
}
解法 3 (追記)
今回のような、特定コントロールでのみ ESC/Return を処理したい場合は、もっと簡単な方法がありました。
ESC/Return を処理したいコントロールで、WM_GETDLGCODE を処理します。
static LRESULT CALLBACK keyEditProc(HWND hwndKeyEdit, UINT message, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData) {
switch (message) {
case WM_GETDLGCODE: // ★
if (lParam) { // ★
return DLGC_WANTALLKEYS; // ★
} // ★
break; // ★
// etc.
}
return DefSubclassProc(hwndKeyEdit, message, wParam, lParam);
}
static INT_PTR CALLBACK procAcceleratorEditDlg(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) {
HWND hwndKeyEdit = GetDlgItem(hDlg, IDC_EDIT1);
switch (message) {
case WM_INITDIALOG:
if (hwndKeyEdit) {
#pramga region hwndKeyEditのサブクラス化
const UINT_PTR uIdSubclass = 0;
if (SetWindowSubclass(hwndKeyEdit, keyEditProc, uIdSubclass, (DWORD_PTR)this) == FALSE) {
...
}
#pragma endregion
// フックは使いません
}
case WM_NCDESTROY:
if (RemoveWindowSubclass(hwndEdit, keyEditProc, uIdSubclass) == FALSE) {
...
}
break;
// etc...
}
}
解法 2 はダイアログに、対象のコントロールが複数あるケースの時用ですね。