Как отменить редактирование метки элемента в древовидном представлении после нажатия клавиши ESC в WinAPI

У меня есть диалоговое окно с элементом управления Tree-View, где пользователь может редактировать метки элемента. Я хочу, чтобы пользователь мог отменить редактирование метки, нажав клавишу ESC.

Проблема в том, что нажатие ESC немедленно закрывает диалоговое окно.

Я попытался получить дескриптор элемента управления EditBox TreeView_GetEditControl() вызовите сообщение TVN_BEGINLABELEDIT и подклассифицируйте его, чтобы перехватить клавишу ESC, но когда я это сделаю, ввод в поле редактирования становится невозможным.

В чем проблема?

Вот соответствующий код:

INT_PTR CALLBACK DlgProc(HWND hWnd, UINT message, 
                         WPARAM wParam, LPARAM lParam) {
    switch(message) {
        //...

        case WM_NOTIFY:
        {
            LPNMHDR pNmHdr = (LPNMHDR)lParam;
            switch(pNmHdr->code) {
                case TVN_BEGINLABELEDIT:
                {
                    HWND hwndTV = (HWND)GetWindowLongPtr(hWnd, GWLP_USERDATA); // stored handle to Tree-View ctl
                    HWND hWndEditBox = TreeView_GetEditControl(hwndTV);

                    // subclass edit box
                    TreeViewGlobals::g_wpOrigEditBoxProc =
                        (WNDPROC)SetWindowLongPtr(hWndEditBox, 
                                                  GWLP_WNDPROC, (LONG_PTR)EditBoxCtl_SubclassProc);
                    break;
                }
                case TVN_ENDLABELEDIT:
                {
                    SetWindowLongPtr(hWnd, DWLP_MSGRESULT, (LONG)TRUE); // accept edit
                    return TRUE;
                }
                default:
                    break;
            }
        }
        default:
            break;
    }

    return FALSE;
}

INT_PTR CALLBACK EditBoxCtl_SubclassProc(HWND hWndEditBox, UINT message,
                                         WPARAM wParam, LPARAM lParam) {
    switch(message) {
        HANDLE_MSG(hWndEditBox, WM_GETDLGCODE, EditBoxCtl_OnGetDlgCode);
        HANDLE_MSG(hWndEditBox, WM_KEYDOWN, EditBoxCtl_OnKey); // does not receive WM_KEYDOWN for ESC unless I handle WM_GETDLGCODE above
        default:
            break;
    }

    return CallWindowProc(TreeViewGlobals::g_wpOrigEditBoxProc, 
                          hWndEditBox, message, wParam, lParam);
}

UINT EditBoxCtl_OnGetDlgCode(HWND hWndEditBox, LPMSG lpmsg) {
    if(lpmsg) {
        if(lpmsg->message == WM_KEYDOWN && lpmsg->wParam == VK_ESCAPE) {
            return DLGC_WANTMESSAGE;
        }
    }

    return 0;
}

void EditBoxCtl_OnKey(HWND hWndEditBox, UINT vk, BOOL fDown, 
                      int cRepeat, UINT flags) {
    switch(vk) {
        case VK_ESCAPE:
                Beep(4000, 150); // never beeps
            break;
        default:
            break;
    }
}

PS Я заметил, что когда я удаляю обработчик WM_GETDLGCODE в EditBoxCtl_SubclassProc(), становится возможным снова ввести текст в поле редактирования, но тогда я не могу перехватить WM_KEYDOWN для ESC-ключа из этой процедуры.

3 ответа

Решение

Ниже приведено решение, которое я нашел. Похоже, уловка заключается в том, чтобы вызывать исходный процесс управления с WM_GETDLGCODE, перехваченным в подклассе proc, сохраняя возвращаемое значение и затем возвращая его с установленным флагом DLGC_WANTALLKEYS или DLGC_WANTMESSAGE, чтобы предотвратить дальнейшую обработку нажатия клавиши системой.

Плюсом этого подхода является то, что нажатие ESC отменяет редактирование и возвращает метку элемента к его исходному тексту, а нажатие клавиши ENTER во время редактирования больше не просто закрывает диалоговое окно (что было другой проблемой) без какого-либо дополнительного кода для обработки этих случаев.

Вот код, который работает:

INT_PTR CALLBACK EditBoxCtl_SubclassProc(HWND hWndEditBox, UINT message,
                                         WPARAM wParam, LPARAM lParam) {
    switch(message) {
        //HANDLE_MSG(hWndEditBox, WM_GETDLGCODE, EditBoxCtl_OnGetDlgCode);  // can't use this: need wParam and lParam for CallWindowProc()

        case WM_GETDLGCODE: {   
            INT_PTR ret = CallWindowProc(TreeViewGlobals::g_wpOrigEditBoxProc, 
                                         hWndEditBox, message, wParam, lParam);
            MSG* lpmsg = (MSG*)lParam;  
            if(lpmsg) {
                if(lpmsg->message == WM_KEYDOWN && 
                  (lpmsg->wParam == VK_ESCAPE || lpmsg->wParam == VK_RETURN) ) 
                {
                    return ret | DLGC_WANTALLKEYS;
                }
            }

            return ret;
        }

        default:
            break;
    }

    return CallWindowProc(TreeViewGlobals::g_wpOrigEditBoxProc, 
                          hWndEditBox, message, wParam, lParam);
}

Вам нужно помнить древовидное представление hwnd, когда вы получаете TVN_BEGINLABELEDIT (в элементе класса, связанном с диалоговым окном) и обнулять его при получении TVN_ENDLABELEDIT, когда пользователь нажимает esc или вводит в модальном диалоговом окне - вы получаете WM_COMMAND с IDCANCEL (на esc) или IDOK(на входе). Вам нужно проверить сохраненное древовидное представление hwnd и, если оно не 0 - позвонить TreeView_EndEditLabelNow

    switch (uMsg)
    {
    case WM_INITDIALOG:
        m_hwndTV = 0;
        break;
    case WM_NOTIFY:
        switch (reinterpret_cast<NMHDR*>(lParam)->code)
        {
        case TVN_BEGINLABELEDIT:
            m_hwndTV = reinterpret_cast<NMHDR*>(lParam)->hwndFrom;
            return TRUE;
        case TVN_ENDLABELEDIT:
            m_hwndTV = 0;
            //set the item's label to the edited text
            SetWindowLongPtrW(hwndDlg, DWLP_MSGRESULT, TRUE);
            return TRUE;
        }
        break;
    case WM_CLOSE:
        EndDialog(hwndDlg, 0);
        break;
    case WM_COMMAND:
        switch (wParam)
        {
        case IDCANCEL:
            if (m_hwndTV)
            {
                TreeView_EndEditLabelNow(m_hwndTV, TRUE);
            }
            else
            {
                EndDialog(hwndDlg, IDCANCEL);
            }
            break;
        case IDOK:
            if (m_hwndTV)
            {
                TreeView_EndEditLabelNow(m_hwndTV, FALSE);
            }
            else
            {
                EndDialog(hwndDlg, IDOK);
            }
            break;
        }
        break;
    }

Проблема в том, что модальное диалоговое окно имеет свой собственный цикл сообщений и собственный перевод с IsDialogMessage. Используя MFC, я бы сказал, просто используйте PreTranslateMessage, но это не доступно в обычном WinApi. У вас нет доступа к внутренней петле сообщений и интерфейсу клавиатуры.

Таким образом, клавиша Escape обрабатывается внутри цикла сообщений. И вызывает отправку сообщения WM_COMMAND с IDCANCEL. (См. Спецификации MSDN о диалогах)

Возможно, самый простой способ - перехватить сообщение WM_COMMAND, отправленное в диалоговое окно, проверить, есть ли у кого фокус, и имеет ли фокус элемент управления редактирования на месте, вы просто устанавливаете фокус обратно на древовидный элемент управления и хотите забыть IDCANCEL и не закрывать диалог.

Другие вопросы по тегам