How to Create Listbox with a Gradient Background in VB.Net
The Problem Definition:
A person on the MSDN Forum asked about adding a Gradient background to a list box. At first glance I figured I could do it from the DrawItem event handler. I actually presented code to the user that did, in fact, create a gradient background on the Listbox as the user had asked for. But it was plagued with redraw flicker and ghosts on some resize events. As I further looked at and played with the resulting code that I had offered to the person I realized that I had taken the easy (read lazy) way and hd provided code that was way less than professional. Hey, it was free advice!
But I couldn’t leave the issue alone so I decided to work this out into a professional solution with no flkickers and no ghosts. What follows is the result of my efforts to provide a quality solution to the MSDN requester.
The Solution
The solution turned out to require sub-classing the Listbox control and then overriding some of the methods and events and adding some custom properties.

Drawing the Background.
Drawing a Gradient background was a simple exercise in getting the client rectangle and filling it with a LinearGradientBrush. I added to properties to the sub-classed control named GradientColor1 and GradientColor2. This allowed the user of the control to select the colors to be used in the creation of the LinearGradientBrush. In the Set part of the property methods I added a call to the paint background so that the changes to the gradient colors would take effect immediately including during design time. Redrawing the background needs to be done during the Paint event, Resize event as well as on the OnPaintBackGround event.
Protected Overrides Sub OnCreateControl()
MyBase.OnCreateControl()
Me.DrawMode = Windows.Forms.DrawMode.OwnerDrawVariable
Me.EnableDoubleBuffering()
Me._BackGroundBrush = New LinearGradientBrush(New Point(0, 0), _
New Point(ClientRectangle.Width, 0), _
Me._GradientColor1, _
Me._GradientColor2)
End Sub
|
The DrawMode needs to be set to OwnerDrawVariable. I am using OwnerDrawVariable instead of OwnerDrawFixed because I intend to allow the height of list items to be variable to accommodate Images, custom fonts and other enhancements. The EnableDoubleBuffering() is a call to a custom method I added to the class that sets up the draw routines to doublebuffer the graphics. What this means is that a client rectangle is drawn off screen then moved to the control’s client rectangle so that the actual drawing action is not seen thus eliminating almost all flickering. I also create a default background brush for the initialization of the control.
Drawing the Items List
So now we have the background getting drawn. As long as we do not add any items to the ListBoz the background will remain nice and pretty as we intended. But since we are going to have to add items to the ListBox at some point, we have to take care of the actual drawing of those list Items to the Client Rectangle. I begin by creating a method that iterates through the Items list and calls the OnDrawItem event for each item passing a new System.Windows.Forms.DrawItemEventArgs object with each call . while I do supply a default Font and FontWeight in the argument, I actually do not use them in the OnDrawItem event, using instead some custom properties that we will get to later. Here is that code:
'calls List_DrawItem for all items in the listbox
Private Sub PaintItemsList(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs)
Dim MyRect As System.Drawing.Rectangle = Nothing
Try
For Icount = 0 To MyBase.Items.Count - 1
MyRect = Me.GetItemRectangle(Icount)
If e.ClipRectangle.IntersectsWith(MyRect) Then
If Me.SelectedItems.Contains(Me.Items(Icount)) _
Or Me.SelectedItem = (Me.Items(Icount)) Then
OnDrawItem(New System.Windows.Forms.DrawItemEventArgs(e.Graphics, _
New Font("Ariel", 10, FontStyle.Regular), _
MyRect, Icount, DrawItemState.Selected))
Else
OnDrawItem(New System.Windows.Forms.DrawItemEventArgs(e.Graphics, _
New Font("Ariel", 10, FontStyle.Regular), _
MyRect, Icount, DrawItemState.Default))
End If
End If
Next
Catch ex As Exception
Throw ex
End Try
End Sub |
This gets the ball rolling for drawing the items to the client rectangle. In the actual OnDrawItem event method I could have simply done a Graphics.DrawString() call to display the items and a call to DrawFocusRectangle() and I would be done. But like all programmers who like what they do, I could not avoid doing some Gold Plating of the solution. What is gold plating? It is adding features that where not originally called for in the specifications.
Gold Plating, or adding Images, Custom Fonts, and what not….
Looking at the Sub-classed ListBox at this point I came to the conclusion that adding images, selective fonts and font colors for each List Item might be fun. So I added some new properties for a ImageList object, a sorted list of a custom class I called ItemImageID and some housekeeping code to make it all work. Basically I set it up so that you could add an ImageList to the control and then select an image from that list and associate it with a ListItem. You can also select a Font for that list item, and a forecolor. So when I was all done, the OnDrawItem event method ending up looking like this:
'draws each item into the client rectangle
Private Sub List_DrawItem(ByVal sender As Object, _
ByVal e As System.Windows.Forms.DrawItemEventArgs) Handles Me.DrawItem
Dim MyImage As Image = Nothing
'default font...
Dim MYfont As Font = New Font("Ariel", 12, FontStyle.Regular)
'default forecolor
Dim MyBrush As Brush = Brushes.Black
e.Graphics.FillRectangle(Me._BackGroundBrush, e.Bounds)
Me._BackGroundBrush = New LinearGradientBrush(New Point(0, 0), _
New Point(ClientRectangle.Width, 0), _
Me._GradientColor1, Me.GradientColor2)
If Me._ImageIndex.Keys.Contains(e.Index) AndAlso Me._ImageIndex.Item(e.Index) IsNot Nothing Then
If Me._ImageList.Images.Count >= Me._ImageIndex(e.Index).ImageIndex Then
MyImage = Me._ImageList.Images(Me._ImageIndex(e.Index).ImageIndex)
ElseIf Me._ImageList.Images.ContainsKey(Me._ImageIndex(e.Index).ImageName) Then
MyImage = Me._ImageList.Images(Me._ImageIndex(e.Index).ImageName)
End If
If Me._ImageIndex(e.Index).Font IsNot Nothing Then
MYfont = Me._ImageIndex(e.Index).Font
End If
If Me._ImageIndex(e.Index).ForeColor IsNot Nothing Then
MyBrush = Me._ImageIndex(e.Index).ForeColor
End If
If MyImage IsNot Nothing Then
e.Graphics.DrawImage(MyImage, New Point(0, e.Bounds.Y))
End If
End If
e.Graphics.DrawString(MyBase.Items(e.Index).ToString, _
MYfont, MyBrush, _
e.Bounds.X + 20, e.Bounds.Y)
e.DrawFocusRectangle()
End Sub
|
That wraps up the article for this Gradient Background Listbox. You can download the code here!