Click here to Skip to main content
15,885,365 members
Articles / Desktop Programming / Windows Forms

Subclassing ComboBox List Control

Rate me:
Please Sign up or sign in to vote.
4.82/5 (7 votes)
31 Dec 2016CPOL4 min read 20.1K   574   7   4
Totally customize the WinForms Combobox by painting the Non-Client area in the combo box's listbox.

Introduction

The most annoying thing about the ComboBox in WinForms is when you change the UI colors of the control, the drop-down list box ends up looking terrible. The reason this happens is because the .NET library never paints the non-client area of the control.

There are many folks looking for answers on how to fix this, and the general response has been pretty mixed. The code examples people share are usually half-finished and extremly buggy.

Background

When using a combo box, you have a few options as far as color cusomization. After changing them, you end up with something that looks pretty good, but one big annoying quirk. Here is what I am talking about:

 

The one on the left has a Control-colored border around the list box that can not be changed using properties! The one on the right is my code running with a custom BorderColor property set to HotPink.

Using the code

The trick to making this happen is to get your hands on the list box handle of the combo box. The only logical, simple way to do this is wait for Windows to tell us about it.

We do this by implementing a ComboBox object with our own class, overriding WndProc and waiting for the WM_CTLCOLORLISTBOX notification to be sent.

The LPARAM parameter of that notificaiton is the handle to the window of the listbox. That's it! That's all we need now to subclass the list box.

C#
public partial class ColoredComboBox : ComboBox
{
    // ...

    public Color BorderColor { get; set; }

    private const Int32 WM_CTLCOLORLISTBOX = 0x0134;
    private SubclassCBListBox m_cbLBSubclass = null;

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_CTLCOLORLISTBOX)
        {
            base.WndProc(ref m);
            if (m_cbLBSubclass == null)
            {
                m_cbLBSubclass = new SubclassCBListBox(m.LParam);
                m_cbLBSubclass.CBListBoxDestroyedHandler += (s, e) => m_cbLBSubclass = null;
            }
            m_cbLBSubclass.BorderColor = BorderColor;
            return;
        }
        base.WndProc(ref m);
    }
}

Obviously this is just half the battle. Now that we have access to the window handle, we have to actually do something with it. That's where the SubclassCBListBox class comes in.


SubclassCBListBox is a class derrived from .NET's NativeWindow object. It simply calls NativeWindow.AssignHandle(), which subclasses the target window (in this case, the list box) then watches for messages in the overridden WndProc method. Simple! The hard part is knowing what messages to watch for and what to do when we recieve them.


After much research, I found that WM_NCPAINT and WM_PRINT are the two things we need to target. WM_PRINT is used to paint the window when the listbox is animating, WM_NCPAINT is used when the selection changes in the listbox or anything else that might cause the listbox to invalidate.

With WM_NCPAINT, we only get a handle to a Window to work with. So we use GetWindowDC(), then paint to that. With WM_PRINT, they give us an HDC to paint to. We basically do the same exact thing in each notification which is find the Non-Client area and paint it. We also look for WM_NCDESTROY since that is the last thing to be destroyed when a window is being disposed of. In this notification, we un-subclass and notifiy the parent.

C#
protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case Win32Native.WM_NCDESTROY: OnWmNcDestroy(ref m); break;
        case Win32Native.WM_NCPAINT: OnWmNcPaint(ref m); break;
        case Win32Native.WM_PRINT: OnWmPrint(ref m); break;
        default: base.WndProc(ref m); break;
    }
}

WM_NCPAINT

First, we let windows do it's painting, then we just simply paint over the top of it. In this case, we just call Graphics.Clear(). The preparation is done in PrepareNCPaint(). PrepareNCPaint() basically does all the rectangle calculations and region clipping giving us a safe place to paint properly.

C#
private void OnWmNcPaint(ref Message m)
{
    // let windows do it's thing...
    base.WndProc(ref m);

    // get a window DC with the client area clipped out
    Rectangle rcWnd, rcClient;
    IntPtr hDC = PrepareNCPaint(m.HWnd, out rcWnd, out rcClient);

    // fill in the area with our color
    using (var g = Graphics.FromHdc(hDC))
    {
        g.Clear(BorderColor);
    }

    // clean up
    FinishNCPaint(m.HWnd, hDC);
}

WM_PRINT

Again, we let Windows do it's thing first, then we just paint over the top of what they did. With WM_PRINT, we get an HDC from Windows. We only use PrepareNCPaint() here to get the rectangles of the window. We don't use the DC it returns. But, that means we have to do the region clipping ourselves. That's why you see the ExcludeClipRect() call here and not in WM_NCPAINT. ExcludeClipRect() tells Windows that painting isn't allowed inside the rectangle coordinates passed to it. This lets us go crazy when painting and we don't have to worry about painting in the client area.

C#
private void OnWmPrint(ref Message m)
{
    if (FlagSet(m.LParam, Win32Native.PRF_NONCLIENT))
    {
        bool bCheckVisible = FlagSet(m.LParam, Win32Native.PRF_CHECKVISIBLE);
        if (!bCheckVisible || Win32Native.IsWindowVisible(m.HWnd))
        {
            // let windows do it's thing...
            base.WndProc(ref m);

            // we are just going to paint to the passed in DC
            IntPtr hDC = m.WParam;

            // just Prepare/Finish cycle... we just want the rects
            Rectangle rcWnd, rcClient;
            FinishNCPaint(m.HWnd, PrepareNCPaint(m.HWnd, out rcWnd, out rcClient));

            // exclude the client rectangle so we can paint wherever we want
            Win32Native.ExcludeClipRect(hDC, rcClient.Left,
                                        rcClient.Top, rcClient.Right,
                                        rcClient.Bottom);

            // fill in the area with our color
            using (var g = Graphics.FromHdc(hDC))
            {
                g.Clear(BorderColor);
            }
        }
    }
}

WM_NCDESTROY

We use the WM_NCDESTROY notification as a time to say goodbye to the listbox. The last thing to go on a window is it's Non-Client area, so it's appropriate to get out at this point. We call NativeWindow.ReleaseHandle() which un-subclasses the window. Then, we notify anyone who cares by raising an event.

C#
private void OnWmNcDestroy(ref Message m)
{
    ReleaseHandle();
    base.WndProc(ref m);
    if (CBListBoxDestroyedHandler != null) {
        CBListBoxDestroyedHandler(this, new EventArgs()); }
}

Points of Interest

I am new to C# but am pretty proficient in native Win32 API using C++ and MFC. So, this was all just a learning experience. The NativeWindow object is probably the slickest thing I have seen from the .NET library so far and am enjoying this language more and more each day I use it.

History

v1.0: Initial Post

License

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


Written By
Software Developer (Senior)
United States United States
Been workin' in the IDE since before most of you were born.

Comments and Discussions

 
QuestionInteresting Pin
Member 115023481-Jan-17 20:30
Member 115023481-Jan-17 20:30 
AnswerRe: Interesting Pin
SonicMouse2-Jan-17 8:06
SonicMouse2-Jan-17 8:06 
GeneralRe: Interesting Pin
Member 115023482-Jan-17 11:10
Member 115023482-Jan-17 11:10 
AnswerRe: Interesting Pin
vdw2-Jan-17 20:19
vdw2-Jan-17 20:19 

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.