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.
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.
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.
private void OnWmNcPaint(ref Message m)
{
base.WndProc(ref m);
Rectangle rcWnd, rcClient;
IntPtr hDC = PrepareNCPaint(m.HWnd, out rcWnd, out rcClient);
using (var g = Graphics.FromHdc(hDC))
{
g.Clear(BorderColor);
}
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.
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))
{
base.WndProc(ref m);
IntPtr hDC = m.WParam;
Rectangle rcWnd, rcClient;
FinishNCPaint(m.HWnd, PrepareNCPaint(m.HWnd, out rcWnd, out rcClient));
Win32Native.ExcludeClipRect(hDC, rcClient.Left,
rcClient.Top, rcClient.Right,
rcClient.Bottom);
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.
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