モーダルダイアログでの ESC/Return の押下を処理したい


## 問題
自作アプリの設定ダイアログで、ショートカットキーの設定をしようとしていました。
エディットコントロール上で 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 を処理してやることができます。

```cpp
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 を処理します。

```cpp
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 はダイアログに、対象のコントロールが複数あるケースの時用ですね。