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

ColorListBox

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.48/5 (14投票s)

2005 年 4 月 24 日

4分钟阅读

viewsIcon

111690

downloadIcon

1408

带有图标的彩色ListBox。

引言

另一个彩色列表框?有很多关于为ListBox控件着色以及代码示例的文章。好吧,本文与其他文章的区别在于,所有这些文章及其提供的代码都只是演示。自己判断吧

  • 水平滚动条消失了。只有小于控件宽度的固定长度字符串才能显示。如果调整控件大小会怎样?
  • 如果您尝试使用鼠标滚轮,您可能已经注意到,当移动滚轮时,所选项目会不规则地上下移动。
  • 可重写方法OnPaint()OnPaintBackGround()根本不起作用。它们没有连接到事件。背景仅通过Windows消息绘制。

令人沮丧的结论是,作为商业质量控件,它们完全没用。.NET ListBox控件工作正常,但是作为进一步派生的基类,它存在根本缺陷。这种问题的根源在于Windows API ListBox。.NET ListBox只是此控件的包装器。解决方案是什么?我们可以从头编写控件,或者尝试使用现有控件并尝试解决这些问题。基本上,本文是关于如何克服这些问题的指南。

代码

该控件派生自UserControl。它具有ListBox和水平滚动条作为其成员。为了使其成为自绘控件,我们需要一种绘制项目的方法。在此之前,必须将原始控件的DrawMode设置为DrawMode.OwnerDrawVariable。这将禁用项目的原始绘制,并且将激活MeasureItem()方法。DrawItem的代码如下所示。除了几行代码外,它或多或少是直截了当的。

protected void DrawListBoxItem(Graphics g, 
            Rectangle bounds, int Index, bool selected)
{    
    if (Index == -1)
        return;

    if (bounds.Top < 0)
        return;

    if (bounds.Bottom > (listBox.Bottom + listBox.ItemHeight))
        return;

    Graphics gr = null;

    if (UseDoubleBuffering)
    {
        gr = DoubleBuff.BuffGraph;
    }
    else
    {
        gr = g;
    }

    int IconIndex;
    Color TextColor;
    string Text = GetObjString(Index, out IconIndex, out TextColor);

    Image img = null;

    if (selected)
    {
        if (listBox.Focused)
        {
            using(Brush b = new SolidBrush(_HighLightColor)) 
            {
                gr.FillRectangle(b, 0, 
                    bounds.Top + 1, listBox.Width, bounds.Height - 1);
            }
        }
        else
        {
            using(Brush b = new SolidBrush(Color.Gainsboro))
            {
                gr.FillRectangle(b, 0, 
                    bounds.Top, listBox.Width, bounds.Height);
            }
        }

        if (listBox.Focused)
        {
            using(Pen p = new Pen(Color.RoyalBlue))
            {
                gr.DrawRectangle(p, new Rectangle(0, 
                    bounds.Top, listBox.Width, bounds.Height));
            }
        }
    }

    if (IconIndex != -1 && imageList1 != null) 
    {
        img = imageList1.Images[IconIndex];
        Rectangle imgRect = new Rectangle(bounds.Left - DrawingPos, 
                            bounds.Top , img.Width, img.Height);
        gr.DrawImage(img, imgRect, 0, 0, img.Width, 
                            img.Height, GraphicsUnit.Pixel); 
    }

    using(Brush b = new SolidBrush(TextColor))
    {
        gr.DrawString(Text, this.Font, b, 
            new Point(bounds.Left - DrawingPos + 
            XOffset_forIcon + 2, bounds.Top + 2));    
    }
}

以下是激活和调整滚动条大小的代码

private void ResizeListBoxAndHScrollBar()
{
    listBox.Width = this.Width;

    if (listBox.Width > (MaxStrignLen + XOffset_forIcon + 15))
    {
        hScrollBar1.Visible = false;
        listBox.Height = this.Height;
    }
    else
    {
        hScrollBar1.Height = 18;
        listBox.Height = this.Height - this.hScrollBar1.Height;
        hScrollBar1.Top = this.Height - this.hScrollBar1.Height - 1;
        hScrollBar1.Width = this.Width;    

        hScrollBar1.Visible = true;
        hScrollBar1.Minimum = 0;
        hScrollBar1.Maximum = MaxStrignLen  + XOffset_forIcon + 15;
        hScrollBar1.LargeChange = this.listBox.Width; 
        hScrollBar1.Value = 0;
    }        
}

就这些吗?现在我们有了项目绘制和滚动条的代码。不幸的是,它更复杂,这就是为什么其他ColorListBox的实现没有在商业应用程序中使用的原因。我们刚刚创建的控件在调整大小或有时水平滚动条移动时会闪烁。无论您的应用程序有多好,GUI 上的一个闪烁控件都会使产品看起来不专业。它破坏了整体画面。

如何解决这个问题?有一种众所周知的消除闪烁的技术。它被称为双缓冲。其思想是在内存中进行实际绘制,完成后,将图像复制到GUI。让我们使用这种技术。为此,编写了类DoubleBuff。它根据控件创建位图图像,并在需要时刷新它。

public class DoubleBuffer : IDisposable
{
    private    Graphics    graphics;
    private Bitmap        bitmap;
    private Control        _ParentCtl;
    private Graphics    CtlGraphics;

    public DoubleBuffer(Control ParentCtl)
    {
        _ParentCtl = ParentCtl;
        bitmap    = new Bitmap(_ParentCtl.Width , _ParentCtl.Height);
        graphics = Graphics.FromImage(bitmap);
        CtlGraphics = _ParentCtl.CreateGraphics();
    }

    public void CheckIfRefreshBufferRequired()
    {
        if ((_ParentCtl.Width != bitmap.Width) || 
            (_ParentCtl.Height != bitmap.Height))
        {
            RefreshBuffer();
        }
    }

    public void RefreshBuffer()
    {
        if (_ParentCtl == null)
            return;

        if (_ParentCtl.Width == 0 || _ParentCtl.Height == 0)// restoring event
            return;

        if (bitmap != null)
        {
            bitmap.Dispose();
            bitmap = null;
        }

        if (graphics != null)
        {
            graphics.Dispose();
            graphics = null;
        }

        bitmap    = new Bitmap(_ParentCtl.Width, _ParentCtl.Height);
        graphics = Graphics.FromImage(bitmap);

        if (CtlGraphics != null)
        {
            CtlGraphics.Dispose(); 
        }
        
        CtlGraphics = _ParentCtl.CreateGraphics();
    }

    public void Render()
    {
        CtlGraphics.DrawImage(
            bitmap, 
            _ParentCtl.Bounds,
            0, 
            0, 
            _ParentCtl.Width, 
            _ParentCtl.Height, 
            GraphicsUnit.Pixel);
    }

    public Graphics BuffGraph 
    {
        get 
        { 
            return graphics; 
        }
    }    

    #region IDisposable Members

    public void Dispose()
    {
        if (bitmap != null)
        {
            bitmap.Dispose(); 
        }

        if (graphics != null)
        {
            graphics.Dispose();  
        }

        if (CtlGraphics != null)
        {
            CtlGraphics.Dispose(); 
        }
    }

    #endregion
}

现在我们不直接在GUI上绘制项目。我们将其绘制在基于内存的位图上。但是我们的控件仍然会闪烁。为什么?还剩下一个步骤——原始控件重新绘制背景。但是如何停止这样做呢?正如我前面提到的,可重写方法OnPaintBackGround()没有连接到事件,重写它们将没有任何作用。鉴于上述情况,阻止原始背景绘制的唯一方法是阻止WndProc()方法中的WM_ERASEBKGND事件。我们必须为此类专门创建的WndProc()重写。

滚轮滚动

还应修复鼠标滚轮事件处理。最明智的方法是阻止WM_MOUSEWHEEL事件并将其转换为垂直滚动条事件。使用Windows API SendMessage()直接发送新创建的事件。

private void GetXY(IntPtr Param, out int X, out int Y)
{
    byte[] byts = System.BitConverter.GetBytes((int)Param);
    X = BitConverter.ToInt16(byts, 0);
    Y = BitConverter.ToInt16(byts, 2);
}

protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case (int)Msg.WM_ERASEBKGND:

            if (_BlockEraseBackGnd)
            {
                return;
            }
        
            break;

        case (int)Msg.WM_MOUSEWHEEL:

            int X;
            int Y;

            _BlockEraseBackGnd = false;

            GetXY(m.WParam, out X, out Y);

            if (Y >0)
            {
                SendMessage(this.Handle, (int)Msg.WM_VSCROLL, 
                                  (IntPtr)SB_LINEUP,IntPtr.Zero);
            }
            else
            {
                SendMessage(this.Handle, (int)Msg.WM_VSCROLL, 
                                  (IntPtr)SB_LINEDOWN,IntPtr.Zero);
            }
            return;

        case (int)Msg.WM_VSCROLL:                
        case (int)Msg.WM_KEYDOWN:
        
            _BlockEraseBackGnd = false;

            if (UpdateEv != null)
            {
                UpdateEv(null, null); 
            }
            break;
    }

    base.WndProc (ref m);

}

填充控件

填充控件的主要方法是public void AddItem(object Item, int IconIndex, Color TxtColor)。其中Item可以是任何内容,以及任何具有ToString()方法的类。您可以创建自己的类并重写ToString()方法,或者您可以简单地使用字符串。

public void AddItem(object Item, int IconIndex, Color TxtColor)
{
    ObjectHolder oh = new ObjectHolder(IconIndex, Item, TxtColor);
    
    UseDoubleBuffering = false;
    listBox.Items.Add(oh);    
    ResizeListBoxAndHScrollBar();
}

internal ListBoxHScrollBar设为public以便可以访问它们。

最后的润色

新的控件现在工作正常。剩下的最后一件事是控件的外观。在Windows 2000上,它看起来很正常,但在Windows XP上,它看起来像下面的屏幕截图

垂直滚动条具有XP样式,但水平滚动条具有标准外观。要更改此设置,必须应用清单。基本上,清单文件指定包含公共控件的DLL。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> 
<assemblyIdentity 
    version="1.0.0.0" 
    processorArchitecture="X86" 
    name=""
    type="win32" 
/> 
<description>Your app description here</description> 
<dependency> 
    <dependentAssembly> 
        <assemblyIdentity 
            type="win32" 
            name="Microsoft.Windows.Common-Controls" 
            version="6.0.0.0" 
            processorArchitecture="X86" 
            publicKeyToken="6595b64144ccf1df" 
            language="*" 
        /> 
    </dependentAssembly> 
</dependency> 
</assembly>

清单文件必须位于应用程序启动的同一目录中。为了避免在运行目录中出现此不方便的文件,可以将清单直接注入到可执行程序集中。为了自动化此任务,已经编写了实用程序DotNetManifester.exe。您可以使用此实用程序将清单直接注入程序。

可以在此处工具箱部分以及在DotNetManifester.exe中找到控件的完整源代码和最新二进制文件。

© . All rights reserved.