Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

Custom Controls in Win32 API: The Basics

4.97/5 (70 votes)
12 Dec 2014CPOL13 min read 148.6K   3.8K  
A quick introduction into the development of custom controls in plain Win32 API.

Articles in this series

Introduction

A large portion (if not all) of textbooks and tutorials about Win32 API programming touch also the topic of implementing a custom control. However AFAIK, there is no really comprehensive source (or at least none known to me) covering more than just the very basics of this topic.

Most of such sources just provide some information on how to register a new window class, a trivial information about a few messages, most notably WM_PAINT, and then they switch to another topic. If you have ever tried to implement your own non-trivial control, I'm pretty sure you can agree with me that the implementor of the control must know much more, and that many traps are awaiting on the way.

This article is intended as a first part of a series which will eventually try to cover the topic more thoroughly, including best practices, which you will hopefully find useful in order to implement good custom controls, which are easy to use by application developers, which fit into the Windows look and feel and which contribute to good applications.

As this is the first part of the series, it will summarize the basics, and hence it will eventually be just a variant of the other sources I criticized in the previous sentences. I can only hope that the dear readers will forgive me for the slow start.

Few Notes

This article (and the whole series) is intended for Windows developers who know the C language and who are familiar at least with the basics of the Windows API. You should know terms like "message loop", "window procedure", or "window class" and be familiar with these concepts.

Note that the articles will be accompanied by some C code samples. The code is created more for better illustration rather than for completeness of implementation. Especially note that to improve readability, error handling is mostly omitted. Real applications should of course handle errors more seriously.

Throughout the series, the code samples will follow a few code conventions about identifiers related to the control being implemented:

  • XXS_STYLE: We will use the prefix XXS_ for custom control specific style bits.
  • XXM_MESSAGE: We will use the prefix XXM_ for custom control specific messages.
  • XXN_NOTIFICATION: We will use the prefix XXN_ for custom control specific notification codes.

Trivial Control

We will start with a code implementing a very trivial custom control. It can also serve as a skeleton for your experiments. It consists of three source files. The first file is a header describing the control interface. So far it is very simple as it actually does not provide any interesting functionality.

The code below roughly corresponds to the state of things where the other sources I criticized in the first few lines, so I believe no detailed comments are needed.

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

#ifndef CUSTOM_H
#define CUSTOM_H

#include <tchar.h>
#include <windows.h>


/* Window class */
#define CUSTOM_WC   _T("CustomControl")

/* Register/unregister the window class */
void CustomRegister(void);
void CustomUnregister(void);


#endif  /* CUSTOM_H */

Here is the custom control implementation:

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

#include "custom.h"


static void
CustomPaint(HWND hwnd)
{
    PAINTSTRUCT ps;
    HDC hdc;
    RECT rect;

    GetClientRect(hwnd, &rect);

    hdc = BeginPaint(hwnd, &ps);
    SetTextColor(hdc, RGB(0,0,0));
    SetBkMode(hdc, TRANSPARENT);
    DrawText(hdc, _T("Hello World!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
    EndPaint(hwnd, &ps);
}


static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg) {
        case WM_PAINT:
            CustomPaint(hwnd);
            return 0;
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

void
CustomRegister(void)
{
    WNDCLASS wc = { 0 };

    wc.style = CS_GLOBALCLASS | CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = CustomProc;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.lpszClassName = CUSTOM_WC;
    RegisterClass(&wc);
}

void
CustomUnregister(void)
{
    UnregisterClass(CUSTOM_WC, NULL);
}

And finally a simple application using the control:

C
/* File main.c
 * (application doing actually nothing but creating a main window and
 *  the custom control as its only child)
 */

#include <tchar.h>
#include <windows.h>

#include "custom.h"


static HINSTANCE hInstance;
static HWND hwndCustom;

#define CUSTOM_ID     100
#define MARGIN          7

static LRESULT CALLBACK
MainProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg) {
        case WM_SIZE:
            if(wParam == SIZE_MAXIMIZED || wParam == SIZE_RESTORED) {
                WORD cx = LOWORD(lParam);
                WORD cy = HIWORD(lParam);
                SetWindowPos(hwndCustom, NULL, MARGIN, MARGIN,
                             cx-2*MARGIN, cy-2*MARGIN, SWP_NOZORDER);
            }
            break;

        case WM_CREATE:
            hwndCustom = CreateWindow(CUSTOM_WC, NULL, WS_CHILD | WS_VISIBLE,
                                  0, 0, 0, 0, hwnd, (HMENU) CUSTOM_ID, hInstance, NULL);
            break;

        case WM_CLOSE:
            PostQuitMessage(0);
            break;
    }

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

int APIENTRY
_tWinMain(HINSTANCE hInst, HINSTANCE hInstPrev, TCHAR* lpszCmdLine, int iCmdShow)
{
    WNDCLASS wc = { 0 };
    HWND hwnd;
    MSG msg;

    hInstance = hInst;

    CustomRegister();

    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = MainProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
    wc.lpszClassName = _T("MainWindow");
    RegisterClass(&wc);

    hwnd = CreateWindow(_T("MainWindow"), _T("App Name"), WS_OVERLAPPEDWINDOW,
                CW_USEDEFAULT, CW_USEDEFAULT, 350, 250, NULL, NULL, hInstance, NULL);
    ShowWindow(hwnd, iCmdShow);

    while(GetMessage(&msg, NULL, 0, 0)) {
        if(IsDialogMessage(hwnd, &msg))
            continue;

        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    CustomUnregister();

    return (int)msg.wParam;
}

(There is MS Visual Studio 2010 project with these sources attached at the top of this article.)

Window Class and CS_GLOBALCLASS

Assuming you are able to implement simple Windows applications, there is not much what should surprise you in the preceding code: The custom control is implemented in a very similar way as a typical application window of your first Win32 Hello World program. Its window class must be registered before creation and usage of the window belonging to that class and there is the well known window procedure.

Probably the only difference is the class style CS_GLOBALCLASS, which makes the window class global. Actually if the implemented control is registered and used in a single module (e.g., it is both registered and used in the EXE, or a single DLL) you do not need to use it (and actually you should not), and instead you should set WNDCLASS::hInstance to the handle of the module. It is the HINSTANCE handle passed into WinMain(), or DllMain() in case of a DLL. Then, when you create the control, you then have to specify the same instance handle.

However nowadays many applications consist of more then one program, and even more they often use many DLLs, perhaps even supporting some plug-ins (DLLs loaded ad-hoc) which may need to reuse the control, and in such cases the CS_GLOBALCLASS is very handy: When you use it the system finds the class even if the instance handle passed to CreateWindow() does not match the one when the control window class has been registered.

When CreateWindow() (or CreateWindowEx()) is called, the system first looks for the specified local class (i.e., a window class which was registered without the flag CS_GLOBALCLASS and with the matching HINSTANCE handle). If it is not found, it searches the global classes (i.e., classes with the style CS_GLOBALCLASS).

This means that classes with the style CS_GLOBALCLASS in the whole process share one common namespace, and they even can be shadowed locally by a local window class registered with a particular HINSTANCE handle. Also note all the standard controls from USER32.DLL and COMCTL32.DLL use this class style too so it is really wise to avoid those names for your local classes (unless the shadowing is what you really want to achieve, of course.)

Hence, when registering a global class, you should be careful when choosing its name to minimize the accidental risk of shadowing it in some DLL. I recommend using the reversed domain name to produce unique class names (e.g., "com.company.control") to minimize the risk of such name clash.

Control Data

A non-trivial custom control typically needs some memory where it can store its data and internal state. This is typically achieved by defining a structure which exists during the life time of the control. The pointer to a structure is usually stored within the extra bytes of the control window. The following code shows how to do that:

C
typedef struct CustomData_tag CustomData;
struct CustomData_tag {
    // ... the control data
};

void
CustomRegister(void)
{
    WNDCLASS wc = { 0 };

    wc.style = CS_GLOBALCLASS | CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = CustomProc;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.lpszClassName = CC_WINDOWCLASS;
    wc.cbWndExtra = sizeof(CustomData*);  // <-- Here we are
    RegisterClass(&wc);
}

The window procedure of the control then can use SetWindowLongPtr() and GetWindowLongPtr() to setup and access the structure. The code below shows creation and destruction of the structure in an extended version of the window procedure from the introduction code above:

C
static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    // Retrieve the pointer to the control data so it may be used in all message handlers below:
    CustomData* pData = (CustomData*) GetWindowLongPtr(hwnd, 0);

    switch(uMsg) {
        // ...

        case WM_NCCREATE:
            // Allocate, setupo and remember the control data:
            pData = malloc(sizeof(CustomData));
            if(pData == NULL)
                return FALSE;
            SetWindowLongPtr(hwnd, 0, (LONG_PTR)pData);
            // ... initialize CustomData members
            return TRUE;

        case WM_NCDESTROY:
            if(pData != NULL) { // <-- "If" required as we get here even when WM_NCCREATE fails.
                // ... free any resources stored in the data structure
                free(pData);
            }
            return 0;
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

I would like to highlight few things about this:

  • Many tutorials allocate (and free) the data in WM_CREATE (and WM_DESTROY). I recommend to use their "NC" counterparts as the code above shows. This is safer because during window creation WM_NCCREATE is the very first message sent and then several other messages are sent before the WM_CREATE eventually comes. If you would ever add a handler for such a message into your code, you might easily introduce a dereference of the NULL pointer, leading to well-known consequences: The application crashes. Similarly it is the message WM_NCDESTROY which is guaranteed to be the last message received when the control is being destroyed, and not WM_DESTROY.
  • Note that Windows here does not follow the common error handling pattern that release/terminate code is called only when the corresponding initialization has previously succeeded. If WM_NCCREATE fails (i.e., when it returns FALSE), Windows sends WM_NCDESTROY anyway. Hence the app has to make sure the pointer is not NULL befire trying to dereference the pointer in the WM_NCDESTROY handler. The same would stand for WM_CREATE and WM_DESTROY: if the former returns -1, the latter is called anyway.
  • Also many tutorials avoid using the "extra bytes" and use GWL_USERDATA as a parameter for SetWindowLongPtr() and GetWindowLongPtr(). However for general-purpose control this is a wrong idea as the control should leave the GWL_USERDATA for the application as it also may need to associate some additional data with the control for its own purpose.

Defining a Control Interface

Of course, all controls providing useful functionality have to provide it through some interface. The interface of standard controls mostly consists of definition of their styles, extended styles, controls-specific messages, and notifications. The interface may also use normal functions, but as a believer in principle of consistency, I will follow the way of standard controls and stick mainly to messages and notifications.

So acutally all that is needed is to specify the numerical codes for the control-specific constants in the header file exposing the control interface. Let's take a look at each category a bit closer.

Messages

The control may receive some standard (system) messages which allow smooth intergation of the control into an application. Control-specific messages are used to change its internal state, to trigger some of its functionality, or to ask it for some data it may hold.

Win32 API defines several ranges for message codes, and you really should respect them if you want to avoid troubles:

  • Range 0 through 0x03ff (WM_USER-1): The messages reserved by system. The control should respond to them as appropriate. Unless the control needs to do something special, it can generally pass them into DefWindowProc() and the function will care the message in a default way. Many system messages and how to handle them in your code will be of interest of some of sequel article.
  • Range 0x0400 (WM_USER) through 0x7fff (WM_APP-1): This range is intended for messages specific to the control. You, as a designer of the control, typically define these in a header describing the control interface. Additionally I recommend to avoid using message codes in the range 0x2000 (OCM_BASE) through 0x23ff (OCM_BASE+WM_USER-1) as those are often used for a technique known as message reflection. (Perhaps we will get a closer look at that in some following article.)
  • Range 0x8000 (WM_APP) through 0xbfff: You can use these if the custom control is only used internally by your application. Reusable general-purpose controls should avoid using this range and leave this range for control sub-classing by the application.
  • Range 0xc000 through 0xffff: These are only for use together with RegisterWindowMessage(), and they are irrelevant for our topic.
  • Messages above 0xffff are reserved by Microsoft.

I.e., in most cases the control specific messages should fall into the range based by WM_USER.

#define XXM_DOSOMETHING          (WM_USER + 100)
#define XXM_DOSOMETHINGELSE      (WM_USER + 101)

(The value 100 in the example above is just an arbitrary value to illustrate placing the messages somewhere in the control-scpecific message code range.)

To implement the message, you just add another branch into the window procedure:

static LRESULT CALLBACK
CustomProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg) {
        // ...

        case XXM_DOSOMETHING:
            // do something
            return TRUE;

        case XXM_DOSOMETHINGELSE:
            // do something else
            return FALSE;
    }

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

Last but not least, you should also make some documentation and describe what the messages actually do, how they interpret wParam and lParam propagated from SendMessage() (or other similar function), and how the application should interpret its return value.

Styles

The look and behavior of standard controls can also be determined by setting its style and extended styles. Both the style and the extended style are DWORD bitmasks associated with each window (HWND). Both bitmasks are set when the window is created: CreateWindow() sets the style as specified by one of its parameters and the extended style to zero, while CreateWindowEx() allows to specify both.

The application can also change both later with SetWindowLong() with GWL_STYLE and GWL_EXSTYLE, respectively.

The meaning of high word of the style, and all bits of the extended style is defined by the system and it is really a bad idea trying to override it. The custom control should respect those bits (as far as it makes sense to do in the context of the particular control) and behave in accordance to their meaning.

The low 16 bits of the style are available to the control implementation, and you can define them to augment the control with a behavior specific to your needs.

#define XXS_BEMOREFANCY             0x0001
#define XXS_HIGHESTCUSTOMSTYLE      0x8000

Notifications

If the custom control needs to notify the application about some events or changes of its state, or if it needs to ask the application about something, it may send the standard notification message (WM_NOTIFY). Of course it has to follow the contract the message provides to the application, i.e., WPARAM must specify the ID of the control, and LPARAM must point to NMHDR or a control-specific structure which begins with that.

The typical code of custom control sending the notifications may look like this:

C
static void
CommonSendSomeNotification(HWND hwnd)
{
    NMHDR hdr;

    hdr.hwndFrom = hwnd;  // The custom control handle
    hdr.idFrom = GetWindowLong(hwnd, GWL_ID);
    hdr.code = XXN_NOTIFICATIONCODE;
    SendMessage(GetParent(hwnd), WM_NOTIFY, hdr.idFrom, (LPARAM) &hdr);
}

As part of the control interface, the code XXN_NOTIFICATIONCODE should of course be defined in a header describing the control's interface. Note that unlike for messages, Win32 API does not define any ranges for the notifications. But if you take a look into the Windows SDK headers, you may see the standard notification codes have the most significant bit set (they are defined as a negative number cast to UINT), so if you define control-specific notifications in a range from 0 to 0x7fffffff, there is no danger of clash with system notification codes.

#define XXN_NOTIFICATIONCODE                   0x1

Real Life Code

Nothing can replace the possibility to study real-life code and indeed when implementing a full-featured custom control, you will probably need it more than once. I can provide you two pointers which (I hope) can serve as good examples. The first one is the Wine (http://www.winehq.org) project, especially its (re)implementation of USER32.DLL and COMCTL32.DLL:

In the context of custom controls the Wine project is valuable in two different ways: First, you can see the code as an implementation of any other custom control. Second, and perhaps even more importantly, as the Wine tries to be as much compatible with native Windows implementation as possible, its sources are also great if you want to design your custom control as consistent as possible with native Windows controls. They can serve as a good illustration for what those control can or cannot do. And believe me: The Wine code is in many regards much more complete than any documentation available on MSDN or elsewhere.

So, for example, if you implement a control "which should look a bit like tree-view" but "it is too much different to just customize the standard tree-view through subclassing", then studying the Wine's implementation of tree control may be the right thing where to start.

The second pointer is a soup served (mainly) by me. It is the mCtrl project (http://mctrl.org) which implements several custom controls. By no means is the project finished, but it can already serve well as an illustrative code for purposes of this article series. And it is also no accident as this article as well as the follow-up articles I plan to write are based on the experience I've got during its development. The project code is hosted on github and it provides both the implementation of the custom controls (subdirectory src) as well as simple example apps illustrating the usage (subdirectory examples):

Both projects are open source, and of course you may also reuse and modify the code in your project, assuming you will follow the license conditions of the project of your interest.

Stay Tuned

As already mentioned at the beginning of the article, this is just the first article of the series on the broad topic of custom control implementation. Next time, we will take a closer look at how to paint a custom control. Also (as it is quite a common trouble when implementing a new control) how to avoid flickering on frequent repainting, e.g., when the control is being resized.

License

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