Click here to Skip to main content
15,868,016 members
Articles / Desktop Programming / Win32

Custom Controls in Win32 API: Standard Messages

Rate me:
Please Sign up or sign in to vote.
4.94/5 (35 votes)
17 Mar 2014CPOL23 min read 68.4K   2.3K   68   5
Make your control answer to the questions system or application may ask.

Articles in this series

Introduction

In previous articles we engaged in painting the controls. Today, we are going to pay some attention to handling many messages which mediate interaction between the control, its dialog or other parent window, and also between the control and the operating system itself.

Note that, as all the articles in this series, this is not a reference documentation. This article is not meant as a replacement of MSDN. It is meant as complementary to it. MSDN often describes what the message is about, how its parameters are passed to it via WPARAM and LPARAM and I cannot see any sense in repeating it here. Instead, this article provides just an overview of messages controls often need to implement, with some hints how to do that from point of view of the control, not application attempting to just send the message to a control.

Unicode and ANSI Strings

Before we start talking about the number of messages, lets take a little talk on strings and how standard controls handle the Unicode vs. ANSI duality of the Win32 API, as that obviously has some impact how the custom controls should handle string parameters too.

Note: Unicode in context of Win32 API means UTF-16 encoding in little-endian byte order. (On Windows 2000, it was only UCS-2, i.e. subset of UTF-16 without the support for surrogate pairs.) Windows ANSI, or just ANSI in Win32 API context, is a misnomer meaning a byte-oriented encoding determined by current code page. Unlike in Unicode, the same sequence of bytes has different meaning in different code pages and, often, may be invalid. In general, all new code should prefer to use Unicode, at least internally.

Most control-specific messages which take string as a parameter (or a pointer to structure having string as a member) are actually two distinct messages. The Unicode-flavored message has a suffix W, the ANSI-flavored one has a suffix A. The preprocessor macro UNICODE then in public header controls which one is used when application source uses the message name without either of the suffices.

However that is mainly true about control-specific messages. You can find many examples of this approach in commctrl.h for messages responsible to insert or set items of controls like list view or tree view.

The standard messages, in particular WM_NCCREATE, WM_CREATE, WM_SETTEXT and WM_GETTEXT, work a bit differently. There are no Unicode/ANSI flavors of these messages. Instead the system remembers whether the window class was registered with RegisterClassW() or RegisterClassA() (or their RegisterClassEx() counterparts). Depending on this, we call the resulted HWNDs to be Unicode windows or ANSI windows.

All the relevant functions like CreateWindow(), SetWindowText() or GetWindowText() have the Unicode or ANSI flavor (again, suffix W or A respectively in the function name). Whenever the function is called, it makes automatically the required conversion to pass the string to the window procedure in the corresponding form.

As globalization advances, internationalization and localization are natural demands on any modern software. Hence Unicode rules more and more this world and the standard controls internally always work in Unicode (at least since Windows 2000). I recommend following this rule of thumb in your controls, unless you have very strong reason to do otherwise.

That, in general, means to follow these steps:

  • Register your control with Unicode flavor of RegisterClass().
  • Interpret strings in the standard messages as Unicode.
  • Hold strings in control's structures in Unicode.
  • When implementing new control-specific message, implement it either for Unicode, or implement two distinct messages. The code below demonstrates this.

Note: The first three points of the list can be achieved by using explicitly the W-flavors of the functions like RegisterClassW() etc., or with using the Unicode resolution macros (the name without any suffix) and building the project as Unicode one by using appropriate compiler options. In Visual Studio, this can be configured in project properties, gcc compiler has the option -municode for this purpose.

C
/* custom.h
 * (public control interface) */

...

#define XXM_SOMESTRINGMESSAGEW     (WM_USER+100)
#define XXM_SOMESTRINGMESSAGEA     (WM_USER+101)
#ifdef UNICODE
    #define XXM_SOMESTRINGMESSAGE     XXM_SOMESTRINGMESSAGEW
#else
    #define XXM_SOMESTRINGMESSAGE     XXM_SOMESTRINGMESSAGEA
#endif

...


/* custom.c
 * (control implementation) */
static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg) {

        ...

        case XXM_SOMESTRINGMESSAGEW:
            ... // Handle input (WPARAM/LPARAM) as Unicode.
            return 0;

        case XXM_SOMESTRINGMESSAGEA:
            ... // Handle input (WPARAM/LPARAM) as ANSI.
                // (Assuming the control holds the strings in Unicode,
                // this usually involves converting the string with
                // MultiByteToWideChar() (when string is input parameter)
                // or WideCharToMultiByte() (output parameter).
            return 0;

        ...

    }
}

Notifications handle the Unicode and ANSI strings yet differently and we will talk about it later by the end of this article.

Control Creation and Destruction

We already touched the topic of control creation and destruction in the 1st part of this series. However lets take a look on it a little bit closer.

The message WM_NCCREATE is the very first message the control gets during its life time. Correspondingly, the message WM_NCDESTROY is the last one. For custom control implementation, these messages are appropriate for allocation and setup of resources the control needs (e.g. some structure to hold the control's internal data and state), or freeing it respectively.

Note that for WM_NCCREATE, DefWindowProc() sets window text propagated from CreateWindow() via LPARAM as CREATESTRUCT::lpszName. If your control works with window text, you should propagate the message to DefWindowProc(). More about window text will be discussed later in a dedicated section later in this article.

During window creation, i.e. in the context of CreateWindow() (and assuming nothing fails during the creation), the messages WM_NCCREATE and WM_CREATE are sent. But some more messages can be sent between the two (typically, WM_NCCALCSIZE) and also after WM_CREATE (e.g. WM_SIZE, WM_MOVE, WM_SHOWWINDOW). Therefore, WM_NCCREATE should do the minimal work to have its data in a consistent state (that usually means allocating the structure for the state, and initializing it to some default values. That guarantees other message handlers can use the data without any problem.

As a rule of thumb, most of the other (less critical) setup of the control should be handled in WM_CREATE. So, for example, non-essential resources of the controls should be initialized to NULL in WM_NCCREATE and allocated thenafter in WM_CREATE. Especially if the resource allocation involves calling some other Win32API functions: They may resolve to sending another messages to the control and this design then guarantees the control data is in a consistent state and it helps to avoid subtle bugs. Some setup, can be deferred even further into other messages like WM_SIZE (e.g. if a complex control needs to compute some layout) and other appropriate message handlers.

When the window is being destroyed, i.e. in the context of DestroyWindow(), the control gets WM_DESTROY followed by WM_NCDESTROY. It is good practice to undo in WM_NCDESTROY what WM_NCCREATE did, and in WM_DESTROY what WM_CREATE did. I also recommend that WM_DESTROY keeps the control in consistent state, especially that all free'd pointers in CustomData structure are reset to NULL.

Note: WM_NCDESTROY is sent even if previously the message WM_NCCREATE failed (i.e. if it returned FALSE). Also WM_DESTROY is sent even if WM_CREATE failed (i.e. returned -1). It has impact on error paths, as the code excerpt below demonstrates.

Special care must be taken if the control is implemented by customizing an existing window class. Therefore good practice is to call propagate WM_NCCREATE and WM_CREATE to original window procedure before handling it locally in the derived one. However that is another topic and we will explore this another time.

C
/* custom.c
 * (control implementation) */

typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    HWND hwnd;
    HTHEME hTheme;
    // ...
};

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_NCCREATE:
            if(!DefWindowProc(hwnd, uMsg, wParam, lParam))
                return FALSE;
            pData = malloc(sizeof(CustomData));
            if(pData == NULL)
                return FALSE;
            ZeroMemory(pData, sizeof(CustomData));
            pData->hwnd = hwnd;
            // ... Set pData to consistent state (e.g. some reasnable default values)
            SetWindowLongPtr(hwnd, 0, (LONG_PTR)pData);
            return TRUE;

        case WM_CREATE:
        {
            LRESULT lres;
            lres = DefWindowProc(hwnd, uMsg, wParam, lParam);
            if(lres == -1)
                return -1;

            // Further setup, e.g. opening theme handle if the control supports themed painting.
            pData->hTheme = OpenThemeData(hwnd, L"...");
            // ...
            return lres;
        }

        case WM_DESTROY:
            if(pData->hTheme != NULL) {
                CloseThemeData(pData->hTheme);
                pData->hTheme = NULL;
            }
            break;

        case WM_NCDESTROY:
            if(pData != NULL) {  // pData is NULL if WM_NCCREATE failed before SetWindowLongPtr()!!!
                // ... free all the other resources we alloc'ed in WM_NCCREATE
                free(pData)
            }
            break;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Window Style and Extended Style

Every window has a style and extended style associated with it. Both are actually double-word (DWORD) bitmasks, which augment look or behavior of the control. Initially the style and extended style are set accordingly to parameters of CreateWindow() or CreateWindowEx(). The style is propagated into WM_NCCREATE and WM_CREATE as members of CREATESTRUCT.

Later, during the control's life time, they can be changed with the function SetWindowLong(), using the indeces GWL_STYLE or GWL_EXSTYLE respectively.

All bits of the extended style and 16 higher bits of the style have a meaning defined by system. Lower 16 bit of the style are available for control-specific purposes.

The system-defined style and extended style bits are usually handled by appropriate messages passed to DefWindowProc(). For example, if the control has the style WM_BORDER, then handling of WM_NCCALCSIZEreserves the space for the border by making a client area of the control a bit smaller, and WM_NCPAINT paints into the space the border.

(Actually the border is then painted unthemed. We will eventually cover painting into non-client area and related stuff in some sequel article.)

Still, at least for the control-specific styles, the control implementation must know when the styles are changed to reflect it. Monitoring the changes in the style and extended style bits is performed with messages WM_STYLECHANGING and WM_STYLECHANGED. WM_STYLECHANGING is sent before the change is performed and the control may, if it desires so, deny the change or force a different change.

Although the control can always ask for the current style with GetWindowLong(), it can be often better and more convenient to have the style cached locally in our control's data structure, at least the low 16-bits of the style we (arguably) can need to access quite often.

C
/* custom.c
 * (control implementation) */

typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    DWORD style : 16;
    // ...
};

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_NCCREATE:
        {
            CREATESTRUCT* cs = (CREATESTRUCT*) lParam;
            // ...
            pData->style = cs->style;
            return TRUE;
        }

        case WM_STYLECHANGING:
        {
            /* Sometimes, for example, a control has to allow setting some style only
             * during its creation, and it needs to block the change of it thenafter.
             * It can be done by augmenting STYLESTRUCT::styleNew as we wish. */
            if(wParam == GWL_STYLE) {
                STYLESTRUCT* ss = (STYLESTRUCT*) lParam;

                /* Prevent change in some styles we do not allow to change during control's life: */
                if((ss->styleOld ^ ss->styleNew) & (XXS_CONSTSTYLE1 | XXS_CONSTSTYLE2)) {
                    ss->styleNew &= ~(XXS_CONSTSTYLE1 | XXS_CONSTSTYLE2);
                    ss->styleNew |= ss->styleOld & (XXS_CONSTSTYLE1 | XXS_CONSTSTYLE2);
                }
            }
            break;
        }

        case WM_STYLECHANGED:
            if(wParam == GWL_STYLE) {
                STYLESTRUCT* ss = (STYLESTRUCT*) lParam;

                /* Remember new style in our data structure for simple use */
                pData->style = ss->styleNew;

                /* If a style affecting look of the control changes, we may need to repaint it. */
                if((ss->styleOld ^ ss->styleNew) & (XXS_ANOTHERLOOK | XXS_MORECOLOROUS)) {
                    if(!pData->bNoRedraw)
                        InvalidateRect(hwnd, NULL, TRUE);
                }
            }
            break;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Window Geometry

Whenever the control is created, resized, its (normal or extended) style is changed, or state of its status bars changes, the system sends WM_NCCALCSIZE to the control so that it can rearrange what portion of the control area is non-client or client one. The default implementation in DefWindowProc() reserves some space for borders, client edges and also scrollbars, depending on the style, extended style, and space needed for scrollbars (if any).

Custom implementation of this message may be quite complicated to all combinations of its parameters and return values it expects, but it is rarely needed, and the default implementation is almost always sufficient. The only exception may be when we want to say the system, what parts of the control's client area stays valid after the resize in cases when the rough control with class style CS_HREDRAW and CS_VREDRAW is not sufficient. In such cases the implementation may look like this:

C
/* custom.c
 * (control implementation) */

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_NCCALCSIZE:
            if(wParam) {
                // Return the desired combination of WVR_xxx constants, to
                // specify which part of client should be preserved after the
                // resize. For example:
                return WVR_ALIGNTOP | WVR_ALIGNRIGHT;
            }

            // In other cases, pass the message into DefWindowProc().
            break;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

After system knows what part of the control belongs to the non-client area, it sends WM_MOVE (if position of client area changed) and WM_SIZE (if size of it changed) to the control, and invalidates portions of client area accordingly to the return value of WM_NCCALCSIZE.

Whenever the control is to change its visibility, the control gets WM_SHOWWINDOW. That includes whenever ShowWindow() is called, but also when parent window gets minimized/restored, or when other window is maximized and hence it obscures the control completely. The cases can be distinguished by analyzing its parameters.

Window Text

For setting and retrieving a text associated with a window, Win32 API provides messages WM_SETTEXT and WM_GETTEXT. However applications usually do not use these messages directly and they call functions SetWindowText() and GetWindowText() which can convert the strings between Unicode and ANSI if caller and window opinion on type of strings does not match.

When passed into DefWindowProc(), the text is stored or retrieved directly from window manager. This allows Windows to get the text from a window belonging to another process, even if that other process is not responding. Raymond Chen described this nicely in his famous blog, The Old New Thing: The secret life of GetWindowText.

It's also worth of note that for some utilities (e.g. various accessibility tools), the window text may ver useful even if no particular text is displayed by the control, so setting it to a reasonable value can improve an experience to users with some disabilities or limitations.

If you decide to handle the messages differently and (for example) store the text directly in your control's structure, then you should also consistently handle messages WM_NCCREATE and WM_GETTEXTLENGTH:

C
/* custom.c
 * (control implementation) */

#include <tchar.h>
#include <strsafe.h>

typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    TCHAR* lpszText;
    // ...
};

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_SETTEXT:
        {
            TCHAR* lpszText = (TCHAR*) lParam;
            TCHAR* lpszTmp;
            size_t sz;

            if(lpszText != NULL)
                StringCchLength(lpszText, STRSAFE_MAX_CCH, &sz);
            else
                sz = 0;
            lpszTmp = (TCHAR*) malloc(sizeof(TCHAR) * (sz+1));
            if(lpszTmp == NULL)
                return FALSE;
            StringCchCopyEx(lpszTmp, sz+1, lpszText, NULL, NULL, STRSAFE_IGNORE_NULLS);
            if(pData->lpszText != NULL)
                free(pData->lpszText);
            pData->lpszText = lpszTmp;
            return TRUE;
        }

        case WM_GETTEXT:
        {
            size_t sz = (size_t) wParam;
            TCHAR* lpBuffer = (TCHAR*) lParam;
            TCHAR* lpEnd;

            if(pData->lpszText == NULL)
                return 0;
            StringCchCopyEx(lpBuffer, sz, pData->lpszText, &lpEnd, NULL, 0);
            return (lpEnd - lpBuffer);
        }

        case WM_GETTEXTLENGTH:
        {
            size_t sz;

            if(pData->pszText == NULL)
                return 0;
            StringCchLength(pData->lpszText, STRSAFE_MAX_CCH, &sz);
            return sz;
        }

        case WM_NCCREATE:
        {
            CRERATESTRUCT* cs = (CREATESTRUCT*) lParam;
            size_t sz;
            // ...

            if(cs->lpszText != NULL)
                StringCchLength(cs->lpszText, STRSAFE_MAX_CCH, &sz);
            else
                sz = 0;
            pData->lpszText = (TCHAR*) malloc(sizeof(TCHAR) * (sz+1));
            if(pData->lpszText == NULL)
                return FALSE;
            StringCchCopyEx(pData->lpszText, sz+1, cs->lpszText, NULL, NULL, STRSAFE_IGNORE_NULLS);
            return TRUE;
        }

        case WM_NCDESTROY:
            // ...
            if(pData->lpszText != NULL)
                free(lpszText);
            // ...
            break;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Note: In most cases, relying on DefWindowProc() and calling GetWindowText() where neded (e.g. in painting code) fully suffices (and it is definitely less work). You should generally prefer this way unless you have good reason to do the stuff manually as the code excerpt shows.

Window Font

Every control which draws a text during its painting, should allow application to specify what font it should use. For this purpose the API provides the messages WM_SETFONT and WM_GETFONT. Their implementation is simple enough so there is no need for any further explanation:

C
/* custom.c
 * (control implementation) */

typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    HFONT hFont;
    // ...
};

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_SETFONT:
            pData->hFont = (HFONT) wParam;
            if((BOOL) lParam  && !pData->bNoRedraw)
                InvalidateRect(hwnd, NULL, TRUE);
            return 0;

        case WM_GETFONT:
            return (LRESULT) pData->hFont;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

All what remains is the painting code to select the font into a device context when drawing a text. If the hFont is NULL, the control should use the font returned by GetStockObject(SYSTEM_FONT). Note this font is pre-selected in new device contexts.

When painting text with DrawThemeText(), keep in mind the function paints with the font specified in the theme definition only if it defines one for the theme class/part/state combination. If it does not, the function uses the font selected into the device context. Hence, it is good idea to select the font into the device context even for the themed code path.

Painting

Window painting is based on handling WM_ERASEBKGND, WM_PAINT and WM_PRINTCLIENT we already described in one of the previous articles in the series.

Aside that, there is also a message WM_SETREDRAW which is supported by many standard controls. The message allows application to temporarily disable the redrawing of the control when its state changes. This is, for example, often used for controls like list view and tree view. Often, application may need to populate such controls with huge number of items, and the control refreshes itself after each such insert to reflect the data it holds.

If the application knows it inserts hundreds of items at once, it may disable the repaint, insert all the items, re-enable the repaint and force the repaint:

/* app.c
 * (some application) */

...

SendMessage(hwnd, WM_SETREDRAW, FALSE, 0);  // Disable repaint
...  // Huge number of messages which change control's state
SendMessage(hwnd, WM_SETREDRAW, TRUE, 0);   // Re-enable repaint
RedrawWindow(hwnd, NULL, NULL, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN);

Note: Complex controls may need to repaint also its child windows or their non-client area, hence we used RedrawWindow() with all the flags instead of simple InvalidateRect(hwnd, NULL, TRUE). However if it is known there are no children or frame, InvalidateRect() would suffice.

If you want to support this message (which you should if setting up or populating the control might lead to too many repaints), then on the control's implementation side, it usually means just remembering a single flag in control's data and invalidating the control's area only when the flag is not set:

C
/* custom.c
 * (control implementation) */

typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    // Note the inverted logic of this meber.
    // I.e. ZeroMemory() in WM_NCCREATE sets it correctly to FALSE.
    BOOL bNoRedraw;

    // ...
};

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_PAINT:
        {
            // Adapted WM_PAINT handler from the article about painting.
            // We only added the test for pData->bNoRedraw.
            PAINTSTRUCT ps;
            BeginPaint(hwnd, &ps);
            if(!pData->bNoRedraw) {
                if(GetWindowLong(hwnd, GWL_STYLE) & XXS_DOUBLEBUFFER)
                    CustomDoubleBuffer(hwnd, &ps);
                else
                    CustomPaint(hwnd, ps.hdc, &ps.rcPaint, ps.fErase);
            }
            EndPaint(hwnd, &ps);
            return 0;
        }

        case WM_SETREDRAW:
            pData->bNoRedraw = !wParam;
            if(!pData->bNoRedraw)        // Repaint automatically on re-enabling.
                InvalidateRect(hwnd, NULL, TRUE);
            return 0;

        case XXM_CHANGESTATE:
            ...  // modify control's state by modifying data in pData
                 // (e.g. insert/set/delete items of any kind in it).

            if(!pData->bNoRedraw)        // Avoid repaint when disabled by the app.
                InvalidateRect(hwnd, NULL, TRUE);
            return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Note: According to MSDN, when repainting is re-enabled, application itself is then supposed to invalidate the control's area and repaint. However many standard controls do that automatically. I decided to follow the practice in the example above. The advantage of this approach also is the control may have better idea what exactly needs to repaint so it may be able to invalidate only relevant parts of its area.

I recommend another post on The Old New Thing blog as a further complementary reading on the topic of WM_SETREDRAW: There's a default implementation for WM_SETREDRAW, but you might be able to do better.

Yet another message related to painting is WM_NCPAINT. We will cover it in another article, which will talk about, so lets skip it for now.

Interaction with Dialog

Quite often, the parent window of the control is a dialog. The dialogs are quite popular also for simplicity of their indirect creation by using a dialog template (DIALOG or DIALOGEX resource), as it automates a lot of code for creation larger number of windows manually. Of course, the dialog template, or its items respectively, can refer to the control's window class.

Note: The dialog template can refer to the control's class name only if it has been registered with the same instance handle (HINSTANCE) where the template lives, or if the class is global (i.e. class style CS_GLOBAL has been used).

Normal behavior of dialog is determined by its dialog procedure, which is usually the same code which is externally available by calling DefDlgProc(), so customized dialogs can propagate messages to it in a similar way as window procedure do it with DefWindowProc().

Dialogs, in general, provide some features for user's comfort and effectiveness. Some of them need cooperation from the control to work well. These features are provided by DefDlgProc(), which is usually called from the dialog's procedure. Application can also emulate some of those features for normal (non-dialog) windows. For example refer to Raymond Chanin's another post: Using the TAB key to navigate in non-dialogs.

So lets take a look how make the control ready for such coopertion.

The dialog resource can specify the font to be used. If so, the dialog procedure sends WM_SETFONT to each control it creates. We already talked about this message earlier, so lets go on.

Another important feature of dialog is keyboard navigation. Users often rely on changing focus of their controls by pressing TAB or SHIFT+TAB hot keys. The dialog procedure can use also use other keys as space or enter. Therefore it is important to tell the dialog what keys your control needs to get for its own operation, so the dialog knows whether it may use it for the navigation or whether he should feed the control with it. This is done by handling WM_GETDLGCODE.

The handler of this messages is usually just one-liner which specifies bitmask describing the control's needs:

C
/* custom.c
 * (control implementation) */

typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    BOOL bNoRedraw;  // inverted logic, i.e. ZeroMemory() WM_NCCREATE leaves this on the right default.
    // ...
};

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_GETDLGCODE:
            return DLGC_flag1 | DLGC_flag2;
            // For illustrative purpose only. Refer to MSDN for all possible flags and their meaning.
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Yet another feature is a visual feedback of focus and accelerator keys in the dialog's appearance. Historically these were always shown. But since Windows XP, Microsoft has decided it may be disturbing for users who use mostly mouse and rarely use keyboard. So, Windows started to hide the focus rect and accelerator key mark by default, unless user starts to navigate by the keyboard. As an end user you may like it or dislike it, as a developer you should make the control to follow it. Users who do not like it, can then disable the hiding in Control Panels.

The logic is based on handling of the message WM_UPDATEUISTATE by every control which can have focus or a shortcut. When handled manually, it may be implemented like this:

C
/* custom.c
 * (control implementation) */

typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    DWORD dwUiState;
    // ...
};

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_UPDATEUISTATE:
            switch(LOWORD(wParam)) {
                case UIS_CLEAR:       pData->dwUiState &= ~HIWORD(wp); break;
                case UIS_SET:         pData->dwUiState |= HIWORD(wp); break;
                case UIS_INITIALIZE:  pData->dwUiState = HIWORD(wp); break;
            }
            if(!pData->bNoRedraw)
                InvalidateRect(hwnd, NULL, FALSE);
            break;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

The rest is then just following the flags in your painting code. For example, if the condition (pData->dwUiState & UISF_HIDEACCEL) stands, you should reflect it by using DT_HIDEPREFIX flag in DrawText().

Or, you may rely on implementation of the message in DefWindowProc() and only query the current state by sending the control WM_QUERYUISTATE in the painting code.

Once again, further recommended complementary reading consists of some posts on The Old New Thing:

Interaction with System

The control also may need to handle some messages informing it about changes in some Windows settings which may have impact on controls and their look or behavior.

One such message is WM_SYSCOLORCHANGE sent by system to all top-level windows whenever the colors as returned by GetSysColor() change. The top-level windows should distribute the message to all child windows, including our control. (Gotcha: it is on app's developers. AFAIK, DefWindowProc() does not do that.) If the control's painting depends on system colors, it should repaint itself with new ones.

Another message with a bit similar purpose is WM_THEMECHANGED which is sent when the theme preferences are changed. Again the control should repaint itself with new theme data, which requires reopening the theme handles. We already discussed this in the previous article.

Monitoring Focus

Often, the control needs to repaint itself when it gets or loses focus. This is very simply done by tracking WM_SETFOCUS and WM_GETFOCUS messages.

C
/* custom.c
 * (control implementation) */

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_SETFOCUS:
        case WM_GETFOCUS:
            if(!pData->bNoRedraw)
                InvalidateRect(hwnd, NULL, FALSE);
            break;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Keyboard Messages

There is a lot of messages for handling a keyboard, so lets introduce them.

At the low level, there are WM_KEYDOWN and WM_KEYUP messages. The former of them, as the name of it suggests, is sent when you press a key on your keyboard, but also when you keep holding it down and key auto-repeat kicks in (you can distinguish the two cases by inspecting its params). The latter one is sent when the key is released.

On top of that, there is WM_CHAR (or WM_DEADCHAR). Some of them may be sent between WM_KEYDOWN and WM_KEYUP, with translated identification of virtual key code to a real character. The translation is done in the application's message loop by calling the function TranslateMessage(). When the function sees WM_KEYDOWN, it just inserts corresponding WM_CHAR which is then retrieved later by another call of GetNextMessage().

Note: It really matters whether you handle the lower level or higher level messages. The message translation involves a configuration of keyboard and its layout. It is also affected by state of caps lock, shift or other functional keys. If you are developing a game, you probably are more interested in WM_KEYDOWN and WM_KEYUP. If it is a text editor, then it is more likely WM_CHAR for handling normal text typing and WM_KEYDOWN and WM_KEYUP to handle some functional keys like <INSERT> or cursor arrow keys.

Most of the keyboard messages mentioned above also have their SYS counterpart: WM_SYSKEYDOWN, WM_SYSKEYUP and WM_SYSCHAR. Actually these are sent in the same situations, but for key combinations usually used for tasks defined by Windows. E.g. combinations with <ALT> to enter window menu etc. Handling them allows you to override the default handling (assuming you do not pass them into DefWindowProc()). Under normal circumstances, this separation allows you to focus on handling the non-SYS messages and keeping these on DefWindowProc(), without a fear of any conflict.

Additionally there is a message WM_UNICHAR which is capable to handle any UTF-32 Unicode code point. As Windows internally works in UTF-16 (with support of surrogate pairs since Windows XP), the system never sends this message to you. It is rather for your own comfort: If it is beneficial to support the message you can implement it in your window procedure and you as well as 3rd party applications (typically various Input Method Editors for e.g. Asian languages) can send it to you.

Some more in-depth reading about handling keyboard messages can be found in the article What's broken in the WM_KEYDOWN-WM_CHAR input model?, despite its pessimistic title.

Mouse Messages

Mouse handling involves even more messages. There are messages for handling move of a mouse pointer (WM_MOVE), actions with mouse buttons (e.g. WM_LBUTTONDOWN for left button down), and messages for composed actions like click or double click (e.g. WM_LBUTTONDBLCLK). Most messages also have two flavors: normal (client) and non-client whose names begin with WM_NC (e.g. WM_NCMOUSEMOVE, WM_NCNCBUTTONUP).

The normal messages are sent when the mouse event occurs while the mouse pointer is in a client area of the control, or while mouse capture is in an effect: The control may, depending on its logic, ask the system to send all mouse messages to it with the function SetCapture(), until it releases it with ReleaseCapture() (or until another window steals the capture with the SetCapture(). When the control loses the capture, it is notified with WM_RELEASEDCAPTURE.) The "NC" messages are sent when the mouse operation happens in non-client area (and the capture is not in effect).

The mouse capture is frequently used, for example, when implementing a button-like control (or a control whose part is of button-like nature). When its button is pressed (WM_LBUTTONDOWN), you take the capture so subsequent button release goes to the control even when it occurs outside of the control's client area:

C
/* custom.c
 * (control implementation) */

typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    BOOL bLeftButtonPressed;
    // ...
};


static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_LBUTTONDOWN:
            pData->bLeftButtonPressed = TRUE;
            SetCapture(hwnd);
            break;

        case WM_LBUTTONUP:
        {
            POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
            RECT rc;
            GetClientArea(hwnd, &rc);
            if(PtInRect(&rc, pt))
                SendMessage(GetParent(hwnd), WM_COMMAND, GetWindowLong(hwnd, GWL_ID), 0);
            ReleaseCapture();
        }
            // No break here, fall through

        case WM_RELEASEDCAPTURE:
            pData->bLeftButtonPressed = FALSE;
            break;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Note: When the mouse capture is in effect, the window it holds it gets all mouse messages. So depending on its logic, for some of them, the control may need to manually check whether a mouse message really occurred inside it or not.

It is also important to remember that many messages are sent only when you explicitly ask for them: Double-click messages (WM_LBUTTONDBLCLK, WM_RBUTTONDBLCK etc.) are sent only if the window class was registered with the class style CS_DBLCLKS.

Messages WM_MOUSELEAVE and WM_MOUSEHOVER are sent only when their tracking is enabled with TrackMouseEvent(). Note the function enables one shot of the messages, and that WM_MOUSELEAVE disables the tracking altogether, so, usually, the function needs to be called again:

C
/* custom.c
 * (control implementation) */

typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    BOOL fTrackingMouse;
    // ...
};

static void
TrackMouse(HWND hwnd)
{
    TRACKMOUSEEVENT tme;
    tme.cbSize = sizeof(TRACKMOUSEEVENT);
    tme.dwFlags = TME_LEAVE | TME_HOVER;
    tme.hwndTrack = hwnd;
    tme.dwHoverTime = HOVER_DEFAULT;
    TrackMouseEvent(&tme);
}

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_MOUSEMOVE:
            if(!pData->fTrackingMouse) {
                TrackMouse();
                pData->fTrackingMouse = TRUE;
            }
            break;

        case WM_MOUSEHOVER:
            ... // handle mouse hover (often used for example to show a tooltip)
            TrackMouse();
            break;

        case WM_MOUSELEAVE:
            pData->fTrackingMouse = FALSE;
            ...  // handle mouse leave
            break;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Also note there is no WM_MOUSEENTER message. You can use WM_MOUSEMOVE for this purpose: Whenever WM_MOUSEMOVE comes and the flag fTrackingMouse is not set, then the mouse entered the client area of the window.

Scrolling Messages

There are some message that allow controls to support scrolling: WM_HSCROLL, WM_VSCROLL, WM_MOUSEWHEEL and WM_MOUSEHWHEEL. However we'll take a closer look on scrolling in some sequel article.

Accessibility

Actually, the controls should also support cooperation with accessiblity and automation tools. It is done by handling WM_GETOBJECT, which is supposed to return pointer to a COM objects implementing certain COM interface (depending on WPARAM and LPARAM).

This is, however, quite complex topic. In addition, I admit it is a topic I cannot say much about currently due my limited knowledge about it. Hopefully we will return to it sometime later.

Notifications

So far, we mainly talked about situation when system or application needs to talk to the control. But often, the control also needs to tell something about its change to the application. That is usually done through some of notification message.

There are two main notification messages: WM_COMMAND and WM_NOTIFY. The first one is unable to bear more data then just an identification of control it is sending it (i.e. our control) and a 16-bit value which can be packed to it. Therefore it is typical use case is to trigger some action. For example, standard buttons fire WM_COMMAND when clicked.

The latter message, WM_NOTIFY, bears more data, and additionally it is further extensible. The parameter LPARAM is actually a pointer to NMHDR structure, or any other (control and notification-code specific) structure with any additionally data. The control just has to guarantee any such custom structure begins with NMHDR:

C
/* custom.h
 * (public control interface) */


// Notification code
#define XXN_MYNOTIFICATION    101

typedef struct XXNM_NOTIFICATIONDATA {
    NMHDR hdr; // NMHDR has to be the 1st member of the struct.
    ...        // Other data.
} XXNM_NOTIFICATIONDATA;

/* custom.c
 * (control implementation) */

static void
SendNotification(HWND hwnd)
{
    XXNM_NOTIFICATIONDATA nd;

    nd.hdr.hwndFrom = hwnd;
    nd.hdr.idFrom = GetWindowLong(hwnd, GWL_ID);
    nd.hdr.code = XXN_MYNOTIFICATION;
    ... // setup the other struct members
    SendMessage(GetParent(hwnd), WM_NOTIFY, nd.hdr.idFrom, (LPARAM) &nd);
}

However, there is one more complication in case when the structure contains strings. The control is responsible to provide the strings in encoding the parent window expects. If the parent window is not under your control, i.e. when you cannot be sure what the parent wants, you have to ask it with the message WM_NOTIFYFORMAT, either each time before sending such message, or when the control is created and then every time we are asked to refresh the info:

C
/* custom.c
 * (control implementation) */

typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    BOOL fUnicodeNotifications;
    // ...
};

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);
    switch(uMsg) {
        // ...

        case WM_NOTIFYFORMAT:
        {
            LRESULT format;
            if(lParam == NF_REQUERY) {
                format = SendMessage(cs->hwndParent, WM_NOTIFYFORMAT, (WPARAM) hwnd, NF_QUERY);
                pData->fUnicodeNotifications = (format == NFR_UNICODE);
            } else {
                format = (pData->fUnicodeNotifications ? NFR_UNICODE : NFR_ANSI);
            }
            return format;
        }

        case WM_NCCREATE:
        {
            CREATESTRUCT* ss = (CREATESTRUCT*) lParam;
            LRESULT format;
            // ...

            format = SendMessage(cs->hwndParent, WM_NOTIFYFORMAT, (WPARAM) hwnd, NF_QUERY);
            pData->fUnicodeNotifications = (format == NFR_UNICODE);
            return TRUE;
        }
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

Downloadable Example

The attached example project (at the top of this page) implements a button. It roughly corresponds to standard button with BS_PUSHBUTTON or BS_DEFPUSHBUTTON style. Sure, such code is not useful for reuse in any application as you can simply stick with the standard button.

But still, the code of the custom control demonstrates handling of good portion of the messages described in this article as well as the stuff what we already learnt in the previous articles about painting and visual styles, so it can serve as a code for your further experimenting.

The example application is made of a simple dialog with two buttons: one standard button and one of the custom control implementation, so you can compare them live. The only main difference is that (with default Aero Theme) on Windows Vista and later the real button uses transition animation when changing state (e.g. between hot and normal). Some of the sequel articles will be dedicated to several methods of control animations so surely we will fill this gap later.

Real World Code

Today's real world code for further studying consists from few Wine sources. It may give you good insight what those function expect from various standard messages.

Note: Keep in mind that some messages not essential for running Windows applications are not (yet) implemented in Wine. For example, the code above does not play with WM_UPDATEUISTATE and its relatives.

Next Time: Adapting Existing Controls

Often, creating new control by adapting an existing one can be much less work then implementing it from scratch. Next time, we will explore few techniques how to do that and what advantages or limitations they bear.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader
Czech Republic Czech Republic
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionThis is a absolutely fantastic series Pin
steveb16-Jun-18 9:34
mvesteveb16-Jun-18 9:34 
QuestionGood One Pin
Saurabh_Saxena20-Mar-14 5:51
Saurabh_Saxena20-Mar-14 5:51 
GeneralExcellent Pin
Paul20132-Aug-13 1:36
Paul20132-Aug-13 1:36 
BugNice Pin
Klaus-Werner Konrad31-Jul-13 2:54
Klaus-Werner Konrad31-Jul-13 2:54 
GeneralRe: Nice Pin
Martin Mitáš31-Jul-13 2:59
Martin Mitáš31-Jul-13 2:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.