Color ListBox
Advanced ListBox with color items and icons. Full source code provided.
| DotNetRemoting | |
| Intermediate | |
| Language: | C#, VB.NET |
| Platform: | Windows |
| VS.NET |
.NET Color ListBox
Background
Yet another color list box? There are many articles about coloring the of 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 disappeared. Only fixed length strings smaller than the control width can be displayed. What if the control 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() OnPaintBackGround() do not work at all. Simply they are not hooked to the events. Background is painted only via Windows messages.
The sad
conclusion is that as the commercial quality controls they are entirely
useless.
.Net ListBox control itself works fine, however as a base class for further
derivation it is fundamentally flawed. The root of 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 the scratch or to try to use the existing control
and try to work around the problems. Basically this article is the manual how
to overcome these problems.
The Code
The control is derived from UserControl. It has a ListBox and
a Horizontal scrollbar as members.
For making it ownedrawn 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 also the method
MeasureItem() will be activated.
The code of DrawItem is below. Apart from a couple of lines it is more or less
straightforward.
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 activation and resizing of the scrollball
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 scroll bar. Unfortunately it is more complicated and that is the reason why other implementations of ColorListBox are not used in the commercial applications.The control we just created flickers when it is resized or the horizontal scrollbar moved.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 be that fixed? There is a well known technique to
eliminate the flickering. It is called Doublebuffering. 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.
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 GUI directly. We draw them on
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, the overridable method OnPaintBackGround() is not hooked
to the events and overriding them will do nothing.
In the view of the above the only way to block the original painting of the
background is to block the WM_ERASEBKGND event in
WndProc() method.
We have to override WndProc() in specifically created for that class.
The wheel scrolling
The mouse wheel event processing has also to be fixed. The most sensible way is to block WM_MOUSEWHEEL event and convert it to vertical scroll bar event. Newly created events are sent directly using 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);
}
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. Any class that has ToString() method.
You may create you own class and override the method ToString() or you can
simply use strings.
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 an access to them.
Final touch
New control now works fine. Though one last thing that is left - the appearance of the control. On Window 2000 it looks pretty normal but on WinXP it looks like the screenshot below.
Vertical scroolbar has XP style but the horizontal one has
standard appearance.To change this the manifest has to be applied.
Basically manifest file specifies the DLL where the common controls reside.
<?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 ToolBox section here as well as DotNetManifester.exe .
User Reviews
Total of 1 reviewMemory leak
Written by Linning Hu on January 25, 2010I have tried the sample app and tried to insert 300000 items and clear after that. The application did consume alot of memory it seems the clear method did not actual release all resources. Thanks






