Search code examples
cwinapiedithwndwndproc

Pure Win32 C(++) - any other way to disable buttons "as-you-type" except replacing window procedures of controls?


Currently, I am using custom window-procedures on edit-boxes in a modeless dialog to have real-time feedback to the user, i.e. disabling a command button if a required field is empty/invalid or enable it otherwise as well as filtering user input (i.e. allowing only HEX digits, ...)

This should happen "as you type", not when loosing or gaining focus.

For filtering user input in real-time (including clipboard pastes), window prodecures are the way to go, but is there any other elegant way to at least handle the common case "If edit box is empty, disable the OK button" so I can avoid writing a custom window procedure for every edit box, that would otherwise have a specific, but re-usable type of filtering procedure?

        oldWindowProc = (WNDPROC)GetWindowLongA(hPassword, GWL_WNDPROC);
        custom_user_data3 = (WNDPROC*)HeapAlloc(GetProcessHeap(), 0, sizeof(WNDPROC) + 4 * sizeof(HWND));
        custom_user_data3[0] = oldWindowProc;
        custom_user_data3[1] = (WNDPROC)hTestButton;
        custom_user_data3[2] = (WNDPROC)hUrl;
        custom_user_data3[3] = (WNDPROC)hUsername;
        custom_user_data3[4] = (WNDPROC)hPassword;

        SetWindowLongA(hPassword, GWL_USERDATA, (LONG)custom_user_data3);

The other control's HWND are transferred (cast to WNDPROC, but really HWNDs) to the custom window procedure, so it can act on them using EnableWindow.

Inside the window prodecure I then do this:

    if (msg == WM_KEYUP || msg == WM_KEYDOWN || msg == WM_CHAR)
    {
        // IsWindow: Precaution to not break functionality in case of renaming of controls
        if (IsWindow(hwnd_testbutton) && IsWindow(hwnd_username_edit) && IsWindow(hwnd_password_edit) &&
            IsWindow(hwnd_url_edit) &&
            ((GetWindowTextLengthA(hwnd_url_edit) <= 0) || (GetWindowTextLengthA(hwnd_username_edit) <= 0) || (GetWindowTextLengthA(hwnd_password_edit) <= 0)))
        {
            EnableWindow(hwnd_testbutton, false);
        }
        else
        {
            EnableWindow(hwnd_testbutton, true);
        }
    }

Solution

  • Everything you need1 is already implemented in the dialog manager: Whenever the contents of a child edit control change it2 sends an EN_CHANGE notification to the parent. The parent is the dialog and its dialog box procedure is under your control.

    The remaining challenge is transliterating the cryptogram posed by the documentation into human-readable:

    The parent window of the edit control receives this notification code through a WM_COMMAND message.

    The dialog box procedure needs to handle WM_COMMAND messages. Decoding the arguments depends on the message source as outlined in this table. EN_CHANGE notifications fall into the row designated by Control:

    • HIWORD(wParam) is the notification code (i.e., EN_CHANGE)
    • LOWORD(wParam) holds the control ID of the sender
    • lParam is the window handle (HWND) of the sender

    The following example illustrates how to respond to change notifications. It implements a dialog box procedure (DlgProc) that handles EN_CHANGE notifications from an edit control (IDC_EDIT) to toggle the enabled state of a button (IDC_BTN).

    main.c:

    #include <Windows.h>
    
    #include "resources.h"
    
    #include <stdbool.h>
    
    #pragma comment(linker, "\"/manifestdependency:type='win32' \
                             name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
                             processorArchitecture='*' publicKeyToken='6595b64144ccf1df' \
                             language='*'\"")
    
    LRESULT CALLBACK DlgProc(HWND hDlg, UINT Msg, WPARAM wParam, LPARAM lParam)
    {
        switch (Msg)
        {
        case WM_CLOSE:
            // End the modal dialog loop; a modeless dialog would call
            // `DestroyWindow()` instead
            EndDialog(hDlg, 0);
            return TRUE;
    
        case WM_COMMAND:
            // Filter on `EN_CHANGE` notifications sent from the `IDC_EDIT` control
            if (LOWORD(wParam) == IDC_EDIT && HIWORD(wParam) == EN_CHANGE)
            {
                // Determine whether the edit control contains any text
                HWND hwnd_edit = (HWND)lParam;
                bool non_empty = GetWindowTextLengthW(hwnd_edit) > 0;
                // En-/disable `IDC_BTN` accordingly
                HWND hwnd_btn = GetDlgItem(hDlg, IDC_BTN);
                EnableWindow(hwnd_btn, non_empty);
    
                // Message handled
                return TRUE;
            }
            break;
    
        default:
            break;
        }
    
        // Default processing
        return FALSE;
    }
    
    int APIENTRY wWinMain(HINSTANCE hInst, HINSTANCE hInstPrev, PWSTR cmdline, int cmdshow)
    {
        return (int)DialogBoxW(hInst, MAKEINTRESOURCEW(IDD_MAIN), NULL, DlgProc);
    }
    

    resources.h:

    #pragma once
    
    #define IDD_MAIN 101
    
    #define IDC_EDIT 1001
    #define IDC_BTN 1002
    

    resources.rc:

    #include "winres.h"
    
    #include "resources.h"
    
    IDD_MAIN DIALOGEX 0, 0, 160, 40
    STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
    EXSTYLE WS_EX_APPWINDOW
    CAPTION "EN_CHANGE Demo"
    FONT 9, "Segoe UI", 0, 0, 0x0
    BEGIN
        EDITTEXT IDC_EDIT 10, 10, 100, 20
        PUSHBUTTON "Test", IDC_BTN 115, 10, 35, 20, WS_DISABLED
    END
    

    This is fairly straightforward: The entry point spins up a modal dialog declared in a resource script. The only point worth mentioning is that the button (IDC_BTN) has the WS_DISABLED window style set. This correlates with the edit control's (IDC_EDIT) default of holding no text.


    1 Not "everything". Filtering input requires a different solution. That makes for a good question.

    2 I don't know what "it" is here. I assume it's the dialog manager, but the edit control is special.