Click here to Skip to main content
15,878,959 members
Articles / Multimedia / GDI+
Article

ColorListBox

Rate me:
Please Sign up or sign in to vote.
3.48/5 (14 votes)
23 Apr 20054 min read 108.9K   1.4K   34   23
Color ListBox with icons.

Introduction

Yet another color list box? There are many articles about coloring the ListBox control and code samples. Well, the difference between this article and the rest is that all those articles and the code supplied with them are just demos. Judge for yourself:

  • The horizontal scrollbar disappears. Only fixed length strings smaller than the control width can be displayed. What if the control is resized?
  • If you tried to use a mouse wheel, you may have noticed that the selected item moves up and down erratically when the scroll wheel is moved.
  • The overridable methods OnPaint() and OnPaintBackGround() do not work at all. They are not hooked to the events. Background is painted only via Windows messages.

The sad conclusion is that as commercial quality controls they are entirely useless. .NET ListBox control works fine, however as a base class for further derivation, it is fundamentally flawed. The root of this evil is in the Windows API ListBox. .NET ListBox is just a wrapper for this control. What is the solution? We can write the control from scratch or try to use the existing control and try to workaround these problems. Basically this article is a manual on how to overcome these problems.

The code

The control is derived from UserControl. It has a ListBox and a Horizontal scrollbar as its members. For making it owner drawn, we need a method for drawing the item. Prior to that the DrawMode of the original control has to be set to DrawMode.OwnerDrawVariable. This will disable the original drawing of the item and the method MeasureItem() will be activated. The code of DrawItem is given below. Apart from a couple of lines it is more or less straightforward.

C#
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));    
    }
}

Here is the code for activating and resizing of the scrollbar:

C#
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;
    }        
}

Is that all? Now we have the code for item drawing and the scrollbar. Unfortunately, it is more complicated and that is the reason why other implementations of ColorListBox are not used in commercial applications. The control we just created flickers when it is resized or sometimes the horizontal scrollbar moves. No matter how good your application is, just one flickering control on the GUI makes the product look unprofessional. It spoils the whole picture.

How can that be fixed? There is a well known technique to eliminate flickering. It is called double buffering. The idea is that the actual drawing occurs in the memory and when it is completed, the image is copied to the GUI. Let’s use this technique. For that the class DoubleBuff has been written. It creates a bitmap image from the control and refreshes it when required.

C#
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
}

Now we do not draw the items on the GUI directly. We draw them on a memory based bitmap. But our control still keeps flickering. Why? One more step is left – the original control repaints the background. But how to stop doing that? As I mentioned earlier, the overridable method OnPaintBackGround() is not hooked to the events and overriding them will do nothing. In view of the above, the only way to block the original painting of the background is to block the WM_ERASEBKGND event in the WndProc() method. We have to override WndProc() specifically created for that class.

The wheel scrolling

The mouse wheel event processing should also be fixed. The most sensible way is to block the WM_MOUSEWHEEL event and convert it into vertical scroll bar event. Newly created events are sent directly using the Windows API SendMessage().

C#
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);

}

Populating the control

The main method for populating the control is public void AddItem(object Item, int IconIndex, Color TxtColor). Where the Item can be anything and any class that has ToString() method. You may create you own class and override the method ToString() or you can simply use strings.

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

The internal ListBox and HScrollBar were made public to have access to them.

Final touch

The new control now works fine. The last thing that is left - is the appearance of the control. On Windows 2000, it looks pretty normal, but on Windows XP, it looks like the screenshot below:

Vertical scrollbar has XP style but the horizontal one has standard appearance. To change this the manifest has to be applied. Basically, the manifest file specifies the DLL where the common controls reside.

XML
<?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>

The manifest file has to be in the same directory where the applications starts. To avoid having this inconvenient file in the run directory, the manifest can be injected directly into the executable assembly. For automating this task, the utility program DotNetManifester.exe has been written. You can use this utility to inject the manifest directly into the program.

Full source code and the latest binaries of the control can be found in the ToolBox section here as well as in DotNetManifester.exe.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionZip file damaged - Can you please upload a new working zip file Pin
Member 102995912-May-19 1:49
professionalMember 102995912-May-19 1:49 
AnswerRe: Zip file damaged - Can you please upload a new working zip file Pin
OriginalGriff2-May-19 1:53
mveOriginalGriff2-May-19 1:53 
QuestionColorListBox Pin
Shaikh Javed Aryan23-Oct-17 4:49
Shaikh Javed Aryan23-Oct-17 4:49 
Questionstudio C# 2008 Pin
FeniksReborn3-Jun-11 1:51
FeniksReborn3-Jun-11 1:51 
QuestionUpdate an item? Pin
takigabi16-Apr-10 11:20
takigabi16-Apr-10 11:20 
GeneralSelectionMode Error Pin
takigabi16-Apr-10 11:19
takigabi16-Apr-10 11:19 
GeneralMemory leak [modified] Pin
hulinning25-Jan-10 2:06
hulinning25-Jan-10 2:06 
I have tried the example and I tried to load 300000 items and The process consume a lot of memory about 100000K.

I did clear the listbox but the memory did not drop back to normal.

Do you know how to resolve this issue?

Thanks

modified on Monday, January 25, 2010 8:12 AM

QuestionIs reuse in Commercial products allowed? Pin
2twotango9-Feb-09 8:41
2twotango9-Feb-09 8:41 
AnswerRe: Is reuse in Commercial products allowed? Pin
Alex_19-Feb-09 9:46
Alex_19-Feb-09 9:46 
GeneralMouse Click and double click events Pin
MohanBV20-Jul-08 23:45
MohanBV20-Jul-08 23:45 
QuestionSelectionMode.MultiSimple Pin
NameYou15-May-06 6:34
NameYou15-May-06 6:34 
GeneralA little bit slow Pin
wolfsecond2-Jan-06 19:07
wolfsecond2-Jan-06 19:07 
GeneralRTF Pin
Roman Lerman10-Nov-05 3:00
Roman Lerman10-Nov-05 3:00 
GeneralRe: RTF Pin
Alex_110-Nov-05 21:37
Alex_110-Nov-05 21:37 
GeneralRe: RTF Pin
Roman Lerman13-Nov-05 2:11
Roman Lerman13-Nov-05 2:11 
GeneralRe: RTF Pin
Roman Lerman14-Nov-05 6:20
Roman Lerman14-Nov-05 6:20 
GeneralRe: RTF Pin
Roman Lerman15-Nov-05 0:50
Roman Lerman15-Nov-05 0:50 
GeneralPainting Issue Pin
kcgirl143515-Aug-05 12:15
kcgirl143515-Aug-05 12:15 
GeneralRe: Painting Issue Pin
Alex_116-Aug-05 0:27
Alex_116-Aug-05 0:27 
GeneralRe: Painting Issue Pin
kcgirl143517-Aug-05 15:29
kcgirl143517-Aug-05 15:29 
GeneralRe: Painting Issue Pin
Alex_110-Nov-05 21:41
Alex_110-Nov-05 21:41 
Generalproject code Pin
Anonymous6-Aug-05 6:30
Anonymous6-Aug-05 6:30 
GeneralRe: project code Pin
Alex_16-Aug-05 13:41
Alex_16-Aug-05 13:41 

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.