Multiline EDIT dialog control doesn't take tabs despite DLGC_WANTTAB

Viewed 231

I want a multiline EDIT control, which is a child of a dialog, to take tabs as regular text input (instead of switching to the next control).

According to multiple resources, the correct way of doing this is to handle WM_GETDLGCODE and returning DLGC_WANTTAB (or checking + DLGC_WANTMESSAGE).

See:

Raymond's article suggests that what I want is in fact the default behavior, which is not what I'm observing.

Reproduction:

This is based on Visual Studio's default Windows Desktop Application C++ template. If you don't have that for some reason, the contents aren't that important; what's important is that there is an "About" dialog box being shown with the DialogBox function.

Create an EDIT control in the dialog, and subclass that:

WNDPROC OriginalEditWndProc;

// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
        case WM_INITDIALOG:
        {
            HWND hEdit = CreateWindowExW(0, L"EDIT", NULL, WS_VISIBLE | WS_CHILD | ES_MULTILINE, 0, 0, 100, 100, hDlg, NULL, NULL, NULL);
            OriginalEditWndProc = (WNDPROC)GetWindowLongPtrW(hEdit, GWLP_WNDPROC);
            SetWindowLongPtrW(hEdit, GWLP_WNDPROC, (LONG_PTR)EditSubclassProc);
            return (INT_PTR)TRUE;
        }
    }

    return (INT_PTR)FALSE;
}

Using this subclass WNDPROC:

static LRESULT CALLBACK EditSubclassProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    if (message == WM_GETDLGCODE)
    {
        OutputDebugStringA("WM_GETDLGCODE\r\n");
        return OriginalEditWndProc(hWnd, message, wParam, lParam) | DLGC_WANTTAB /* this should make it work */;
    }
    else if (message == WM_KEYDOWN)
    {
        if (wParam == VK_TAB)
        {
            OutputDebugStringA("got VK_TAB\r\n");
            return OriginalEditWndProc(hWnd, message, wParam, lParam);
        }
    }

    return OriginalEditWndProc(hWnd, message, wParam, lParam);
}

(Note that it makes no difference to use comctl32's SetWindowSubclass. Note also that this dialog is not functional because it does not handle a WM_COMMAND to be able close it, but that's irrelevant.)

Observations:

  • We do get "got VK_TAB" every time tab is pressed.
  • If ES_MULTILINE is present, then the keyboard focus goes to the OK button of the About dialog.
  • If ES_MULTILINE is removed, then nothing happens upon pressing tab.
  • Returning DLGC_WANTMESSAGE instead of merely DLGC_WANTTAB doesn't change anything.

Furthermore: If the About dialog is not displayed as a dialog (in other words, IsDialogMessage is not called), but as a regular window, then the behavior is different.

Change the way the dialog box is shown by modifying IDM_ABOUT handler:

case IDM_ABOUT:
{
    //DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
    HWND hDlg = CreateDialogParamW(NULL, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About, 0);
    ShowWindow(hDlg, SW_SHOW);
    break;
}

Observations:

  • There is one WM_GETDLGCODE when the dialog is first displayed (for reasons unknown), and then no more of these messages (as expected).
  • We do get "got VK_TAB" every time tab is pressed.
  • If ES_MULTILINE is present, a tab is inserted into the text (desired behavior).
  • If ES_MULTILINE is removed, then nothing happens upon pressing tab (same as before).
  • Returning DLGC_WANTMESSAGE instead of merely DLGC_WANTTAB doesn't change anything.

Firstly, these observations clash with Raymond's article. As written before, that article suggests that I should get tab characters inserted into the text by default, without doing anything. That's not what happens.

Secondly, WM_GETDLGCODE does in fact work as advertised, we do get WM_KEYDOWN with VK_TAB. I painfully debugged through the disassembly, and found out that:

  • There are 2 different WNDPROC in the EDIT control, one for single line and one for multiline.
  • Upon receiving the WM_KEYDOWN with VK_TAB, the multiline version (MLWndProc iirc) eventually calls MLKeyDown. That function then sends a WM_NEXTDLGCTL message. This is the culprit:
...
765D6DEA  push        ebx  
765D6DEB  push        0Dh  
765D6DED  push        100h  
765D6DF2  push        esi  
765D6DF3  jmp         MLKeyDown+1B6h (765D6D4Bh)  
765D6DF8  cmp         edx,1  
765D6DFB  jne         MLKeyDown+270h (765D6E05h)  
765D6DFD  push        ecx  
765D6DFE  push        9  
765D6E00  jmp         MLKeyDown+3C0h (765D6F55h)  
765D6E05  test        dword ptr [edi+68h],40000h  
765D6E0C  je          MLKeyDown+648h (765D71DDh)  
765D6E12  xor         eax,eax  
765D6E14  cmp         edx,2  
765D6E17  push        0  
765D6E19  sete        al  
765D6E1C  push        eax  
765D6E1D  push        28h  ; WM_NEXTDLGCTL
765D6E1F  push        dword ptr [edi+58h]  
765D6E22  call        SendMessageW (7658BB20h)  

So it seems that the EDIT control insists on this behavior.

Something is not right here though. I cannot possibly be the first person to have that problem, and, again, Raymond's article suggests that this should not be happening in the first place.

Lastly:

Using v6 of the Common Controls library (which is used to get a "modern" visual style [for very modest definitions of "modern"]) doesn't change anything:

#pragma comment(linker,"\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")

Here is a repository with the entire project

1 Answers

if you want that multi-line edit control process VK_TAB as if it is not inside a dialog - need not pass WM_GETDLGCODE to edit control, but process it by self. so solution can be next -

on WM_INITDIALOG call

SetWindowSubclass(GetDlgItem(hwndDlg, IDC_EDIT1), sSubclassProc, 0, 0);

(of course in place IDC_EDIT1 your actual edit id)

and sSubclassProc can be very simply:

static LRESULT CALLBACK sSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam,
    LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR /*dwRefData*/)
{
    switch (uMsg)
    {
    case WM_GETDLGCODE:
        return DLGC_WANTCHARS|DLGC_HASSETSEL|DLGC_WANTALLKEYS|DLGC_WANTARROWS;
    case WM_NCDESTROY:
        RemoveWindowSubclass(hWnd, sSubclassProc, uIdSubclass);
        break;
    }

    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

after this edit process VK_TAB key.


really it got VK_TAB (inside WM_KEYDOWN and WM_CHAR) in any case. but how process this - depend from some internal flag. it can insert tab-stop positions xor send WM_NEXTDLGCTL message to parent. this flag (you view it in assembly code - this is test dword ptr [edi+68h],40000h line is set during handle WM_GETDLGCODE message (dialog send this message to controls when process WM_ACTIVATE)

so your error was in line

return OriginalEditWndProc(hWnd, message, wParam, lParam) | DLGC_WANTTAB ;

you call OriginalEditWndProc and it set flag (40000 in [this+68h])

Related