ARTICLE

.NET Color ListBox

Posted by Alex Articles | Windows Controls April 26, 2005
.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.
Download Files:
 
Reader Level:

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 ownerdrawn 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 Sub DrawListBoxItem(ByVal g As Graphics, ByVal bounds As Rectangle, ByVal Index As Integer, ByVal selected As Boolean)
If Index = -1 ThenReturn
End
If
If
bounds.Top < 0 Then
Return
End
If
If
bounds.Bottom > (listBox.Bottom + listBox.ItemHeight) Then
Return
End
If
Dim
gr As Graphics = Nothing
If
UseDoubleBuffering Then
gr = DoubleBuff.BuffGraph
Else
gr = g
End If
Dim
IconIndex As Integer
Dim
TextColor As Color
Dim Text As String = GetObjString(Index, IconIndex, TextColor)
Dim img As Image = Nothing
If
selected Then
If
listBox.Focused Then
Dim b As Brush = New SolidBrush(_HighLightColor)
Try
gr.FillRectangle(b, 0, bounds.Top + 1, listBox.Width, bounds.Height - 1)
Finally
CType(b, IDisposable).Dispose()
End Try
Else
Dim b As Brush = New SolidBrush(Color.Gainsboro)
Try
gr.FillRectangle(b, 0, bounds.Top, listBox.Width, bounds.Height)
Finally
CType(b, IDisposable).Dispose()
End Try
End If
If
listBox.Focused Then
Dim p As Pen = New Pen(Color.RoyalBlue)
Try
gr.DrawRectangle(p, New Rectangle(0, bounds.Top, listBox.Width, bounds.Height))
Finally
CType(p, IDisposable).Dispose()
End Try
End If
End
If
If
IconIndex <> -1 AndAlso Not imageList1 Is Nothing Then
img = imageList1.Images(IconIndex)
Dim imgRect As Rectangle = New Rectangle(bounds.Left - DrawingPos, bounds.Top, img.Width, img.Height)
gr.DrawImage(img, imgRect, 0, 0, img.Width, img.Height, GraphicsUnit.Pixel)
End If
Dim b As Brush = New SolidBrush(TextColor)
Try
gr.DrawString(Text, Me.Font, b, New Point(bounds.Left - DrawingPos + XOffset_forIcon + 2, bounds.Top + 2))
Finally
CType(b, IDisposable).Dispose()
End Try
End Sub

Here is the code for activation and resizing of the scrollbar

Private Sub ResizeListBoxAndHScrollBar()
listBox.Width =
Me.Width
If listBox.Width > (MaxStrignLen + XOffset_forIcon + 15) Then
hScrollBar1.Visible = False
listBox.Height = Me.Height
Else
hScrollBar1.Height = 18
listBox.Height =
Me.Height - Me.hScrollBar1.Height
hScrollBar1.Top =
Me.Height - Me.hScrollBar1.Height - 1
hScrollBar1.Width =
Me.Width
hScrollBar1.Visible =
True
hScrollBar1.Minimum = 0
hScrollBar1.Maximum = MaxStrignLen + XOffset_forIcon + 15
hScrollBar1.LargeChange =
Me.listBox.Width
hScrollBar1.Value = 0
End If
End
Sub

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 : Implements IDisposable
Private graphics As Graphics
Private bitmap As Bitmap
Private _ParentCtl As Control
Private CtlGraphics As Graphics
Public Sub New(ByVal ParentCtl As Control)
_ParentCtl = ParentCtl
bitmap =
New Bitmap(_ParentCtl.Width, _ParentCtl.Height)
graphics = Graphics.FromImage(bitmap)
CtlGraphics = _ParentCtl.CreateGraphics()
End Sub
Public
Sub CheckIfRefreshBufferRequired()
If (_ParentCtl.Width <> bitmap.Width) OrElse (_ParentCtl.Height <> bitmap.Height) Then
RefreshBuffer()
End If
End
Sub
Public
Sub RefreshBuffer()
If _ParentCtl Is Nothing Then
Return
End
If
If
_ParentCtl.Width = 0 OrElse _ParentCtl.Height = 0 Then ' restoring event
Return
End
If
If
Not bitmap Is Nothing Then
bitmap.Dispose()
bitmap =
Nothing
End
If
If
Not graphics Is Nothing Then
graphics.Dispose()
graphics =
Nothing
End
If
bitmap = New Bitmap(_ParentCtl.Width, _ParentCtl.Height)
graphics = Graphics.FromImage(bitmap)
If Not CtlGraphics Is Nothing Then
CtlGraphics.Dispose()
End If
CtlGraphics = _ParentCtl.CreateGraphics()
End Sub
Public
Sub Render()
CtlGraphics.DrawImage(bitmap, _ParentCtl.Bounds, 0, 0, _ParentCtl.Width, _ParentCtl.Height, GraphicsUnit.Pixel)
End Sub
Public
ReadOnly Property BuffGraph() As Graphics
Get
Return
graphics
End Get
End
Property
#Region
"IDisposable Members"
Public Sub Dispose() Implements IDisposable.Dispose
If Not bitmap Is Nothing Then
bitmap.Dispose()
End If
If
Not graphics Is Nothing Then
graphics.Dispose()
End If
If
Not CtlGraphics Is Nothing Then
CtlGraphics.Dispose()
End If
End
Sub
#End Region
End
Class

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 Sub GetXY(ByVal Param As IntPtr, <System.Runtime.InteropServices.Out()> ByRef X As Integer, <System.Runtime.InteropServices.Out()> ByRef Y As Integer)
Dim byts As Byte() = System.BitConverter.GetBytes(CInt(Param))
X = BitConverter.ToInt16(byts, 0)
Y = BitConverter.ToInt16(byts, 2)
End Sub
Protected
Overrides Sub WndProc(ByRef m As Message)
Select Case m.Msg
Case CInt(Msg.WM_ERASEBKGND)
If _BlockEraseBackGnd Then
Return
End
If
Case
CInt(Msg.WM_MOUSEWHEEL)
Dim X As Integer
Dim
Y As Integer
_BlockEraseBackGnd = False
GetXY(m.WParam, X, Y)
If Y >0 Then
SendMessage(Me.Handle, CInt(Msg.WM_VSCROLL),CType(SB_LINEUP, IntPtr),IntPtr.Zero)
Else
SendMessage(Me.Handle, CInt(Msg.WM_VSCROLL),CType(SB_LINEDOWN, IntPtr),IntPtr.Zero)
End If
Return
Case
CInt(Msg.WM_VSCROLL), CInt(Msg.WM_KEYDOWN)
_BlockEraseBackGnd =
False
If
Not UpdateEv Is Nothing Then
UpdateEv(Nothing, Nothing)
End If
End
Select
MyBase
.WndProc (m)
End Sub

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 Sub AddItem(ByVal Item As Object, ByVal IconIndex As Integer, ByVal TxtColor As Color)
Dim oh As ObjectHolder = New ObjectHolder(IconIndex, Item, TxtColor)
UseDoubleBuffering =
False
listBox.Items.Add(oh)
ResizeListBoxAndHScrollBar()
End Sub

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 scrollbar 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.

Login to add your contents and source code to this article
share this article :
post comment
 
Team Foundation Server Hosting
Become a Sponsor
PREMIUM SPONSORS
  • ceTE software specializes in components for dynamic PDF generation and manipulation. The DynamicPDF™ product line allows you to dynamically generate PDF documents, merge PDF documents and new content to existing PDF documents from within your applications.
    ceTE software specializes in components for dynamic PDF generation and manipulation. The DynamicPDF™ product line allows you to dynamically generate PDF documents, merge PDF documents and new content to existing PDF documents from within your applications. Visit DynamicPDF here
Team Foundation Server Hosting
Become a Sponsor