如何在 WinAPI 中按下 ESC 键时取消树视图控件中的项目标签编辑
How to cancel item label editing in Tree-View control upon ESC keydown in WinAPI
我有一个带有树视图控件的对话框,用户可以在其中编辑项目标签。我希望用户能够通过按 ESC 键取消标签编辑。
问题是按 ESC 会立即关闭对话框 window。
我曾尝试通过 TreeView_GetEditControl()
调用 TVN_BEGINLABELEDIT 消息获取 EditBox 控件的句柄并将其子类化以捕获 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;
}
}
P.S。我注意到当我删除 EditBoxCtl_SubclassProc()
中的 WM_GETDLGCODE 处理程序时,可以再次在编辑框中键入,但是我无法从该过程中捕获 WM_KEYDOWN 的 ESC 键。
问题是模态对话框有自己的消息循环和自己的 IsDialogMessage 翻译。使用 MFC 我会说,只需使用 PreTranslateMessage 但这在普通 WinApi 中不可用。您无权访问内部消息循环和键盘界面。
因此 Escape 键是在消息循环内处理的。并导致发送带有 IDCANCEL 的 WM_COMMAND 消息。 (见MSDN specs about dialogs)
也许最简单的方法是截取发送到对话框的 WM_COMMAND 消息,检查是否有焦点,以及就地编辑控件是否有焦点,您只需将焦点设置回树控件,然后忘记 IDCANCEL 并且不要关闭对话框。
当您收到 TVN_BEGINLABELEDIT
(在 class 成员中,与对话框关联)时,您需要记住树视图 hwnd,并在收到 TVN_ENDLABELEDIT
时将其归零。当用户在模态对话框中按 esc 或 enter - 你会收到 WM_COMMAND
和 IDCANCEL
(在 esc) 或 IDOK
(在 enter 上)。您需要检查保存的树视图 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;
}
以下是我找到的解决方案。诀窍似乎是用 WM_GETDLGCODE 在子类 proc 中拦截调用原始控制 proc,存储 return 值 然后 return 用 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);
}
我有一个带有树视图控件的对话框,用户可以在其中编辑项目标签。我希望用户能够通过按 ESC 键取消标签编辑。
问题是按 ESC 会立即关闭对话框 window。
我曾尝试通过 TreeView_GetEditControl()
调用 TVN_BEGINLABELEDIT 消息获取 EditBox 控件的句柄并将其子类化以捕获 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;
}
}
P.S。我注意到当我删除 EditBoxCtl_SubclassProc()
中的 WM_GETDLGCODE 处理程序时,可以再次在编辑框中键入,但是我无法从该过程中捕获 WM_KEYDOWN 的 ESC 键。
问题是模态对话框有自己的消息循环和自己的 IsDialogMessage 翻译。使用 MFC 我会说,只需使用 PreTranslateMessage 但这在普通 WinApi 中不可用。您无权访问内部消息循环和键盘界面。
因此 Escape 键是在消息循环内处理的。并导致发送带有 IDCANCEL 的 WM_COMMAND 消息。 (见MSDN specs about dialogs)
也许最简单的方法是截取发送到对话框的 WM_COMMAND 消息,检查是否有焦点,以及就地编辑控件是否有焦点,您只需将焦点设置回树控件,然后忘记 IDCANCEL 并且不要关闭对话框。
当您收到 TVN_BEGINLABELEDIT
(在 class 成员中,与对话框关联)时,您需要记住树视图 hwnd,并在收到 TVN_ENDLABELEDIT
时将其归零。当用户在模态对话框中按 esc 或 enter - 你会收到 WM_COMMAND
和 IDCANCEL
(在 esc) 或 IDOK
(在 enter 上)。您需要检查保存的树视图 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;
}
以下是我找到的解决方案。诀窍似乎是用 WM_GETDLGCODE 在子类 proc 中拦截调用原始控制 proc,存储 return 值 然后 return 用 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);
}