65.9K
CodeProject 正在变化。 阅读更多。
Home

子类化 ComboBox 列表控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (7投票s)

2016年12月31日

CPOL

4分钟阅读

viewsIcon

21455

downloadIcon

605

通过绘制组合框列表框的非客户区,完全自定义 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_NCPAINTWM_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:首次发布

© . All rights reserved.