子类化 ComboBox 列表控件






4.82/5 (7投票s)
通过绘制组合框列表框的非客户区,完全自定义 WinForms 组合框。
引言
关于 WinForms 中的 ComboBox,最烦人的事情是,当您更改控件的 UI 颜色时,下拉列表框最终看起来很糟糕。发生这种情况的原因是 .NET 库从不绘制控件的非客户区。
有很多人正在寻找如何解决这个问题的答案,但通常的回答褒贬不一。 人们分享的代码示例通常是半成品,而且非常容易出错。
背景
使用组合框时,您可以在颜色自定义方面有几个选项。 更改它们后,您最终会得到看起来不错的东西,但有一个很大的烦人的怪癖。 这就是我所说的
左边的那个列表框周围有一个 Control 颜色的边框,无法使用属性更改! 右边的是我的代码,运行的是一个自定义的 BorderColor
属性,设置为 HotPink
。
使用代码
实现此目的的技巧是获得组合框的列表框句柄。 唯一合乎逻辑、简单的方法是等待 Windows 告诉我们。
我们通过使用我们自己的类实现 ComboBox 对象,重写 WndProc 并等待发送 WM_CTLCOLORLISTBOX
通知来做到这一点。
该通知的 LPARAM
参数是列表框的窗口句柄。 就是这样! 现在我们需要做的就是子类化列表框。
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);
}
}
显然,这只是战斗的一半。 既然我们可以访问窗口句柄,我们实际上必须用它做一些事情。 这就是 SubclassCBListBox
类的作用。
SubclassCBListBox
是一个从 .NET 的 NativeWindow
对象派生的类。 它只是调用 NativeWindow.AssignHandle()
,它对目标窗口(在本例中为列表框)进行子类化,然后监视重写的 WndProc
方法中的消息。 简单! 困难的部分是知道要监视哪些消息以及收到消息后该怎么做。
经过大量研究,我发现 WM_NCPAINT
和 WM_PRINT
是我们需要定位的两个东西。 WM_PRINT
用于在列表框动画时绘制窗口,WM_NCPAINT
用于在列表框中选择更改或任何其他可能导致列表框无效的情况。
使用 WM_NCPAINT
,我们只能获得一个要处理的 Window 句柄。 所以我们使用 GetWindowDC()
,然后绘制到它。 使用 WM_PRINT
,他们给我们一个 HDC 来绘制。 我们基本上在每个通知中执行完全相同的事情,即找到非客户区并绘制它。 我们还寻找 WM_NCDESTROY
,因为这是窗口被销毁时最后被销毁的东西。 在此通知中,我们取消子类化并通知父级。
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
首先,我们让 Windows 完成它的绘制,然后我们只是简单地在其顶部绘制。 在这种情况下,我们只需调用 Graphics.Clear()
。 准备工作在 PrepareNCPaint()
中完成。 PrepareNCPaint()
基本上完成了所有的矩形计算和区域裁剪,为我们提供了一个安全的地方来正确绘制。
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
同样,我们让 Windows 先做它的事情,然后我们只是在他们所做的事情的顶部绘制。 使用 WM_PRINT
,我们从 Windows 获得一个 HDC。 我们只使用 PrepareNCPaint()
在这里获取窗口的矩形。 我们不使用它返回的 DC。 但是,这意味着我们必须自己进行区域裁剪。 这就是为什么你在这里看到 ExcludeClipRect()
调用,而不是在 WM_NCPAINT
中。 ExcludeClipRect()
告诉 Windows 不允许在传递给它的矩形坐标内进行绘制。 这让我们在绘画时可以疯狂,我们不必担心在客户区域绘画。
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
我们使用 WM_NCDESTROY
通知作为告别列表框的时间。 窗口上的最后一件事是它的非客户区,所以在这一点上退出是合适的。 我们调用 NativeWindow.ReleaseHandle()
,它取消了窗口的子类化。 然后,我们通过引发一个事件来通知任何关心的人。
private void OnWmNcDestroy(ref Message m)
{
ReleaseHandle();
base.WndProc(ref m);
if (CBListBoxDestroyedHandler != null) {
CBListBoxDestroyedHandler(this, new EventArgs()); }
}
关注点
我是 C# 的新手,但在使用 C++ 和 MFC 的本机 Win32 API 方面非常精通。 所以,这只是一次学习经历。 到目前为止,NativeWindow 对象可能是我从 .NET 库中看到的最好的东西,而且我每天使用它都越来越喜欢这门语言。
历史
v1.0:首次发布