Introduction
Given the flexibility built into most WinForms controls, you'd think the CheckedListBox
would provide certain basic features like item background color. But for whatever reason, it was and remains a simple and limited control. An internet search turns up lots of discussions about setting colors, with suggestions ranging from overriding OnDrawItem
to ditching the control and using a ListBox
instead. These are partial solutions at best.
Background
I needed a custom CheckedListBox
in the interface for an integrated circuit wafer processing machine. Twenty-five IC wafers are placed in a plastic cassette, and up to four cassettes can be loaded into the machine. The interface displays the status of each wafer (Unprocessed
, Processed
, etc.), as well as certain pathological conditions such as cross- or double-slotted wafers. A cassette slot might also be empty. Background colors are used to make the overall status easy to ascertain at a glance.
The image shown below is taken from the actual interface, although the contents of the CheckedListBox
is randomly generated test data used simply to verify the correct operation of the interface.
The Wish List
A nice CheckedListBox
control would allow you to:
- Set the item background color dynamically
- Set the item foreground color dynamically
- Set the item font dynamically
- Set the item height to a non-standard value
- Control the focus/selection visual attributes
- Integrate with Visual Studio
Unexpected Problems
It shouldn't be that hard to override OnDrawItem
to handle changing the foreground and background colors, and the font, or so I thought. The DrawItemEventArgs
object has ForeColor
, BackColor
, and Font
properties. Just implement whatever logic you want to set those three properties in a new DrawItemEventArgs e2
, then call base.OnDrawItem(e2)
. CheckedListBox
has an ItemHeight
property, so that should be easy to manage as well if you want the height independent of the font. Nothing to it. Except it didn't work. The colors changed correctly but nothing I did would make the control use the font I specified, and item height had a mind of its own.
Luke, Use the Source
Well, that was annoying. Time to dig a little deeper. The source for CheckedListBox
is available online from dotnetframework.org. OnDrawItem(DrawItemEventArgs e)
confirmed what I suspected: items were drawn using e.ForeColor
and e.BackColor
, but it completely ignored e.Font
, instead using the control's Font
. Item height was the focus of some arcane calculations.
At this point, let me just say that attempting to copy the source for this control into your own project and then modify it to do what you want is doomed to fail. But fear not, for there are workable, if imperfect, solutions to all these problems.
It Ain't Perfect, But It's Progress
This article describes a C# class named CustomCheckedListBox
, subclassed from CheckedListBox
, that provides all the features in the wish list, more or less. The class and a test driver program in a VS2015 solution can be downloaded using the link at the top of this page. Let's begin with a look at the delegates and properties:
public class CustomCheckedListBox : CheckedListBox
{
public delegate Color GetColorDelegate(CustomCheckedListBox listbox, DrawItemEventArgs e);
public delegate Font GetFontDelegate(CustomCheckedListBox listbox, DrawItemEventArgs e);
[Description("Supply a foreground color for each item")]
public event GetColorDelegate GetForeColor = null;
[Description("Supply a background color for each item")]
public event GetColorDelegate GetBackColor = null;
[Description("Supply a font for each item")]
public event GetFontDelegate GetFont = null;
[Description("Set this if you don't like the standard selection appearance")]
public bool DrawFocusedIndicator { get; set; }
public override int ItemHeight { get; set; }
The delegates are the first big difference in CustomCheckedListBox
, as compared to the various online solutions. Simply setting colors in the control would make all the items look the same, perhaps differentiated by state. Meh. By providing methods for these delegates, each item can be colored based on any criteria you choose, and the color can be changed dynamically. The same mechanism can be used to provide a per-item font. For my wafer sorter, the list of items is fixed when the sorting process begins. The background color changes to indicate the current state of each wafer.
The ItemHeight
property has to be overridden here because the base class property is effectively read only. DrawFocusedIndicator
will be explained below. Let's look at the constructor next.
public CustomCheckedListBox(GetColorDelegate back = null,
GetColorDelegate fore = null, GetFontDelegate font = null)
{
GetForeColor = fore;
GetBackColor = back;
GetFont = font;
ItemHeight = 14;
}
Because the delegates are all null
, the entire class is effectively inert, and will therefore behave like a normal CheckedListBox
until you supply methods for these delegates. There's ItemHeight
again, which we will discuss in the next section. It has to be initialized here to prevent the Visual Studio designer from crashing. By default, ItemHeight
is set to 14
, which seems to match the value that CheckedListBox
uses. Let's move on to the OnDrawItem
method:
protected override void OnDrawItem(DrawItemEventArgs e)
{
Color foreColor = (GetForeColor != null) ? GetForeColor(this, e) : e.ForeColor;
Color backColor = (GetBackColor != null) ? GetBackColor(this, e) : e.BackColor;
if (GetFont != null) this.Font = GetFont(this, e);
DrawItemState state = (DrawFocusedIndicator)
? ((e.State & DrawItemState.Focus) == DrawItemState.Focus )
? DrawItemState.Focus
: DrawItemState.None
: e.State;
DrawItemEventArgs e2 = new DrawItemEventArgs(e.Graphics,
e.Font, e.Bounds, e.Index, state, foreColor, backColor);
base.OnDrawItem(e2);
}
protected override void OnFontChanged(EventArgs e)
{
if (GetFont == null) base.OnFontChanged(e);
}
Basically, this method just alters the DrawItemEventArgs
to reflect the colors that the delegates obtained from the app for each item. The if (GetFont != null)
line alters the font for the base CheckedListBox
. In some situations base.OnFontChanged()
causes problems, so disable it if we are supplying a per-item font.
DrawFocusedIndicator
suppresses the normal visual selection indicator (basically, white item text on a blue background). Instead, a selected item is displayed with a focused indicator, which is just a dotted border. You can turn this on or off as you please.
Some Caveats
First, you can supply different fonts for each item, but they all need to be the same size. CheckedListBox
doesn't easily deal with multiple item heights. Second, you have to set a fixed height in the constructor, and it can't be changed.
Visual Studio Integration
If you add CustomCheckedListBox
to your project, and add using Qodex
to your form, Visual Studio will add the control to your Toolbox. You can now drag the control onto a form, and access all of its properties and events just like any other control. This is not the same as making a Custom Control, which is beyond the scope of this article, but you will probably find it does exactly what you need.
Credit Where Credit's Due
The inspiration for this control came from this StackOverflow article. While it was limited to static color control, it pointed me in the right direction.
It's Your Turn
And that's it. I hope you found this article interesting, and possibly even useful. You might be happy with CustomCheckedListBox
just the way it is, or you might already be thinking of some enhancements you'd like to see. Perhaps you'd like to have the checkboxes match the item background color. Let me know what clever modifications you make.
History
- 06 December, 2015: Initial version