Custom Combobox

How to customize the appearance of a combobox.

Introduction

It's time for a new tutorial, something that has been overdue for a long time. The first new tutorial I want to write is going to explore how to implement a flat combo-box. The same technique will also be suitable for making combo-boxes with customized borders as well.

Most of the tutorials I've written so far (which concerned changing a window's appearance) did so by modifying a window's non-client area. This is a nice way of doing this type of thing because the window's frame is separated from the content it is displaying. Any custom-control that you may also decide to write should also follow this same design principle - keep the window borders in the non-client area, and the user-data inside the client area.

The ComboBox is different because it does not have a non-client area, and hence has no "real" window borders. The ComboBox's 3D appearance is achieved by painting the entirety of the control within it's client area. This means that it will be extremely difficult to completely remove the borders from the control. Whichever Microsoft employee who decided to design the ComboBox in this manner should be ashamed of themselves, quite frankly, because every other control within Windows follows the same design-rules, except the ComboBox.

Subclassing a ComboBox

Fortunately I know of a nice way to modify a ComboBox's appearance which is very suited to what we want to do. Yet again we will need to subclass the ComboBox window and intercept one or two window-messages to add the functionality we need.

The first step is therefore to perform the subclassing, this is exactly the same as for any other window:

VOID MakeFlatCombo(HWND hwndCombo)
{
    LONG OldComboProc;
    
    // save the current window procedure before we subclass
    OldComboProc = GetWindowLong(hwndCombo, GWL_WNDPROC);

    SetWindowLong(hwndCombo, GWL_USERDATA, OldComboProc);

    // subclass the window procedure
    SetWindowLong(hwndCombo, GWL_WNDPROC, (LONG)FlatComboProc);
}

Subclassing a window is easy and I've covered this many times before so there's no need to explain any further. The next step is to now write a replacement window procedure which preserves the combo-box's original functionality, but also removes it's 3D-borders from the client area.

The method I have used to achieve this is actually a bit of a cheat, because we are not going to remove the 3D borders. Instead we will leave the 2-pixel area around the control intact, but we'll make it look different by painting it in a solid colour to make it look flat.

LRESULT CALLBACK FlatComboProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    // get the previous window procedure so we can still call it
    WNDPROC OldComboProc = (WNDPROC)GetWindowLong(hwnd, GWL_USERDATA);
    
    switch(msg)
    {
    case WM_PAINT:
        // custom painting will go here
    }

    // preserve original window's functionality
    return CallWindowProc(OldComboProc, hwnd, msg, wParam, lParam);
}

Above is the very simplest form of subclass possible. This replacement window-procedure simply uses CallWindowProc to call the old window procedure. Our custom painting logic will be implemented inside the WM_PAINT handler. Note that this is different to previous tutorials which always used WM_NCPAINT - this is because we are going to modify the client area of the combo-box.

Changing a ComboBox's client area

OK, the boring bit is out of the way. The only thing left to do is modify the painting operation of a combo box.

HDC          hdc;
RECT         rect;
PAINTSTRUCT  ps;

hdc = BeginPaint(hwnd, &ps);

// Find coordinates of client area
GetClientRect(hwnd, &rect);

// Deflate the rectangle by the size of the borders
InflateRect(&rect, -GetSystemMetrics(SM_CXEDGE), -GetSystemMetrics(CM_CYEDGE));

// Make a mask from the rectangle, so the borders aren't included
IntersectClipRect(hdc, rect.left, rect.top, rect.right, rect.bottom);

// Draw the combo-box into our DC
CallWindowProc(OldComboProc, hwnd, msg, (WPARAM)hdc, lParam);

Let's walk through the code line-by-line. The first thing we have to do is obtain a Device Context to draw into, this is obtained using the standard BeginPaint API call. The next step is to work out the coordinates of the combo-box area that we want to draw - i.e. the region not including the outer borders. This is obtained using a combination of GetClientRect and InflateRect. The final stage is to call the original window procedure to paint the combo-box normally, but before we do this we make sure that the "normal" borders don't get drawn by masking them out with the IntersectClipRect call.

It's actually pretty simple. The only thing left to do now is to draw our own custom border around the control, otherwise this area won't ever get updated and will appear "dirty".

// Remove the clipping region
SelectClipRgn(hdc, NULL);

// now mask off the inside of the control so we can paint the borders
ExcludeClipRect(hdc, rect.left, rect.top, rect.right, rect.bottom);

// paint a flat colour
GetClientRect(hwnd, &rect);
FillRect(hdc, &rect, GetSysColorBrush(COLOR_3DSHADOW));

EndPaint(hwnd, &ps);

The code above is very simple. All it does is invert the clipping rectangle so that instead of the borders being masked off and the inside being "exposed", the inside is masked off and the borders are exposed. The borders are then painted a dark colour to show that they have been customized. You have two options here really. To remove the appearance of the borders altogether you should choose a colour that blends into the combo's parent window. For example, if the combo box was in a dialog box you would paint the combo's borders the same colour as the dialog background. The alternative is to make your own custom 3D effect, again this is totally up to you how you decide to do this.

Making a custom button

The difficult part is out of the way. The rest of this tutorial is now only concerned with additional effects for the combo box. The next logical step for combo-box customization is to customize the appearance of the 3D button on the right of the control. Again, it is not possible to remove the button, only to change it's appearance to something else.

This stage is very simple, and only requires one extra line of code to what we've already got. By simply adding the button's region to the clipping rectangle when we do the painting we can prevent the button from being drawn "normally" in the exact same way we did for the 3D borders.

// Find coordinates of client area
GetClientRect(hwnd, &rect);

// Deflate the rectangle by the size of the borders
InflateRect(&rect, -GetSystemMetrics(SM_CXEDGE), -GetSystemMetrics(CM_CYEDGE));

// Remove the drop-down button as well
rect.right -= GetSystemMetrics(CM_CXSCROLL);

As you can see only one line as been added - an adjustment of the right-hand-side of the clipping rectangle so that it excludes the drop-down button.

Conclusion

I'm going to stop here because the important bits have been done. The source-code download does a little more work than I've presented here and shows how to draw a completely custom combo-box which also responds to mouse-messages correctly. Hopefully this tutorial has answered any questions you might of had regarding ComboBox customization. Stay tuned for more tutorials coming soon!