3

I have a dialog box with a Tree-View control where the user can edit the item labels. I want the user to be able to cancel the label edit by pressing ESC key.

The problem is that pressing ESC closes the dialog window immediately.

I have tried getting the handle to the EditBox control by a TreeView_GetEditControl() call upon TVN_BEGINLABELEDIT message and subclassing it to trap the ESC key, but when I do that, typing in edit box becomes impossible.

What is the problem?

Here is the relevant code:

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. I noticed that when I remove WM_GETDLGCODE handler in EditBoxCtl_SubclassProc(), it becomes possible to type in the edit box again, but then I can't trap WM_KEYDOWN for ESC key from that procedure.

Jabberwocky
  • 48,281
  • 17
  • 65
  • 115
Kemal
  • 849
  • 5
  • 21
  • 2
    I think in `EditBoxCtl_OnGetDlgCode()` you should call the original window proc first and store the result. Then return `result | DLGC_WANTMESSAGE` so you keep whatever bits the original window proc wants to have set. On a side note you should use [`SetWindowSubClass()`](https://blogs.msdn.microsoft.com/oldnewthing/20031111-00/?p=41883) instead of `SetWindowLongPtr()` to subclass the window proc. – zett42 Sep 13 '17 at 09:42
  • @zett42 : If this is what you mean, it didn't work: `auto result = CallWindowProc(TreeViewGlobals::g_wpOrigEditBoxProc, hWndEditBox, lpmsg->message, lpmsg->wParam, lpmsg->lParam); return result | DLGC_WANTMESSAGE; ` – Kemal Sep 13 '17 at 09:53
  • 1
    It should be `auto result = CallWindowProc(TreeViewGlobals::g_wpOrigEditBoxProc, hWndEditBox, WM_GETDLGCODE, wParam_from_EditBoxCtl_SubclassProc, lParam_from_EditBoxCtl_SubclassProc); return result | DLGC_WANTMESSAGE;` – zett42 Sep 13 '17 at 10:14
  • I think my understanding of how WM_GETDLGCODE message works is flawed. I thought that if I intercepted a WM_GETDLGCODE in a subclass procedure and returned DLGC_WANTMESSAGE, the subclass procedure was supposed to receive the message pointed to by lParam, which doesn't seem correct. I found a dusty windows document while searching for "how to use DLGC_WANTMESSAGE". Says to always call the original ctl proc with WM_GETDLGCODE, store the result, then handle the case, and return `result | DLGC_WANTMESSAGE`. Just like you pointed out. – Kemal Sep 13 '17 at 11:35
  • I recommend to [search OldNewThing for WM_GETDLGCODE](https://social.msdn.microsoft.com/search/en-US?rq=site%3Ablogs.msdn.microsoft.com%2Foldnewthing&rn=oldnewthing&ral=1&query=wm_getdlgcode). My understanding of this message got much better after that than by reading MSDN reference only. – zett42 Sep 13 '17 at 12:36

3 Answers3

2

Below is the solution that I found. The trick seems to be calling the original control proc with WM_GETDLGCODE intercepted in subclass proc, storing the return value and then returning it with DLGC_WANTALLKEYS or DLGC_WANTMESSAGE flag set to prevent system from further processing the keystroke.

The upside to this approach is that pressing ESC cancels editing and reverts the item label to its original text, and pressing ENTER while editing no longer just closes the dialog(which was another problem) without any additional code to handle those cases.

Here is the code that works:

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);
}
Kemal
  • 849
  • 5
  • 21
  • A small tweak: I would replace `return ret | DLGC_WANTALLKEYS` with `ret |= DLGC_WANTALLKEYS`, and then let `return ret` handle the actual return. – Remy Lebeau Sep 13 '17 at 18:47
0

The problem is that the modal dialog has its own message loop and its own translation with IsDialogMessage. Using the MFC I would say, just use PreTranslateMessage but this isn't available in plain WinApi. You don't have access to the internal message loop and the keyboard interface.

So the Escape key is handled inside the message loop. And causes a WM_COMMAND message with IDCANCEL to be sent. (See the MSDN specs about dialogs)

Maybe the easiest way is to interrcept the WM_COMMAND message sent to the dialog, check if who has the focus and if the inplace edit control has the focus you just set the focus back to the tree control and eat forget the IDCANCEL and don't close the dialog.

xMRi
  • 14,982
  • 3
  • 26
  • 59
  • 1
    Here is what I don't understand(apart from who sends what when to whom in windows event messaging universe): If I intercept a WM_GETDLGCODE in a subclass procedure and return DLGC_WANTMESSAGE, isn't subclass procedure supposed to receive the message pointed to by `lParam`? This seems to work as expected for keys other than ESC. IOW, when ESC is pressed, `lParam` of DLGC_WANTMESSAGE indeed contains a MW_KEYDOWN for VK_ESCAPE, but returning DLGC_WANTMESSAGE does not send that MW_KEYDOWN message back to the subclass procedure. – Kemal Sep 13 '17 at 10:46
  • FRanky: I don't know. The message isn't delivered, maybe because it is handled first with IsDialogMessage.... Set a breakpoint and look into the call stack. – xMRi Sep 13 '17 at 11:07
0

you need remember the tree-view hwnd when you receive TVN_BEGINLABELEDIT (in class member, associated with dialog) and zero it when you receive TVN_ENDLABELEDIT. when user press esc or enter in modal dialog box - you receive WM_COMMAND with IDCANCEL (on esc) or IDOK( on enter). you need check saved tree-view hwnd and if it not 0 - call 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;
    }
RbMm
  • 31,280
  • 3
  • 35
  • 56
  • Yes, this is one way of doing it. I would use something like `m_isEditingLabel` instead of `m_hwndTV `, though. In the end, it is used to determine whether editing is in progress. – Kemal Sep 13 '17 at 11:45
  • @Kemal `m_isEditingLabel instead of m_hwndTV` - what will be if you have more than 1 tree-view in dialog ? call `TreeView_EndEditLabelNow` with which window ? special for this i and save `HWND` and this code will be worked with multiple TVs in dialog as well. – RbMm Sep 13 '17 at 11:49
  • You are correct. I didn't think of that scenario. `m_hWndTV_lbl_edited` then. :-) – Kemal Sep 13 '17 at 12:00
  • This implementation shows how to best fight the system. Handling `WM_GETDLGCODE` instead to control input handling in a dialog is how the system is meant to be used. – IInspectable Sep 13 '17 at 12:46
  • @IInspectable - where you view `control input handling in a dialog` here ? – RbMm Sep 13 '17 at 12:58
  • *"I have a dialog box with a Tree-View control [...]"* - First sentence in the question. – IInspectable Sep 13 '17 at 13:28
  • @IInspectable - and so what(this is not i wrote) ? what is incorrect in my solution ? – RbMm Sep 13 '17 at 13:43
  • 2
    As always, your English is incomprehensible, so I can only comment on the second part. What's wrong? It doesn't scale: Add a second treeview control and see your code duplicate. It also quickly turns into a maintenance disaster by adding redundant state. There is a standard infrastructure built into the system to allow controls to participate in dialog manager business: The powerful `WM_GETDLGCODE` message. Contrast your proposed solution with the real solution, that doesn't duplicate any code for additional treeview controls, nor does it store redundant state information. – IInspectable Sep 13 '17 at 14:32
  • @IInspectable - `Add a second treeview control and see your code duplicate` - absolute no. this code can handle any count of tree-view controls in dialog. here you mistake. i special test my code with 2 tv controls. can you explain why you decide that this not handle this ?! – RbMm Sep 13 '17 at 14:37
  • Now add a third treeview control, that **doesn't** need any special input handling. You'll find yourself writing the same `if`-statements in two places, praying to always keep them in sync. Besides all that, you are burdening the dialog author with writing code, that is the responsibility of a control. Implementing control behavior in two different places does not lead to maintainable code. There's really so much wrong with this clunky approach, that I cannot even begin to fathom, why you are arguing *for* it. All the less, since you already know the real solution to the problem. – IInspectable Sep 13 '17 at 14:53
  • @IInspectable - again - add third or +N treeview controls nothing changed in this code. not need any additional `if` (i at not understand which concrete `if` you mean). `doesn't need any special input handling` - code also absolute not depend from this. all what i do - remember the `m_hwndTV` which begin edit label. this is always single window at time (if we switch no another control - current editing ended). and in cancel/ok handle - i simply check - are some tree view in editing process - end the editing of this tree view, instead end of dialog. this code not need extend – RbMm Sep 13 '17 at 15:01
  • [Those who do not understand the dialog manager are doomed to reimplement it, badly](https://blogs.msdn.microsoft.com/oldnewthing/20070627-00/?p=26243/): *"It's the difference between ordering a hamburger without pickles and ordering a hamburger with pickles, and then carefully picking the pickles off the burger when you get it."* – IInspectable Sep 13 '17 at 15:17
  • @IInspectable - your link is unrelated to my code. you wrong say that my code need duplicates if add additional treeview (or which not need `special input handling.`) - are you still insist in this ? my solution is correct for asked task - correct handle *esc* and *enter* and possible less code compare implement control subclassing - which not need here. in what your problem ? code correct, and simply. – RbMm Sep 13 '17 at 15:25
  • The link is 100% spot-on. It's just using a different example of someone, who did not understand the dialog manager, and went on to fight it. Just like your code does, for the same reason: You did not understand the dialog manager. And yes, I still insist on everything I said. Once you can comprehend my previous comments, you may even agree. – IInspectable Sep 13 '17 at 15:36
  • @IInspectable - I still insist that you wrong. my code handle any count of any treeviews in dialog and not need any extend. you can not event explain - what is need be extended, added, duplicated . which `if` you mean. and this is absolute normal and correct practice handle `wm_command` (`idcancel`, idok`) - this is not called `fight it` – RbMm Sep 13 '17 at 15:53