Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

A Multi-Color, Multi-Font CheckedListBox

4.38/5 (10 votes)
7 Dec 2015CPOL5 min read 35.7K   1.7K  
A CheckedListBox with dynamic foreground, background, and font

CustomCheckedListBox test driver

CustomCheckedListBox test driver 2

CustomCheckedListBox test driver 3

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.

Wafer Sorter

The Wish List

A nice CheckedListBox control would allow you to:

  1. Set the item background color dynamically
  2. Set the item foreground color dynamically
  3. Set the item font dynamically
  4. Set the item height to a non-standard value
  5. Control the focus/selection visual attributes
  6. 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;

	//******************************************************************
	// If you want to set the item height to a specific value that can
	// be independent of the font size, this is the place to do it.
	//******************************************************************
	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;

	//
	// The CheckListBox is going to ignore the font in the event
	// args and use its own.  So, make its own the one we want it
	// to be.  For this to always work right, we will have to shut
	// down the OnFontChanged method below.
	//
	if (GetFont != null) this.Font = GetFont(this, e);

	//
	// If desired, draw an item focused, but not selected.
	//
	DrawItemState state = (DrawFocusedIndicator)
		? ((e.State & DrawItemState.Focus) == DrawItemState.Focus )
			? DrawItemState.Focus 
			: DrawItemState.None
		: e.State;

	//
	// e.Font is going to be ignored.
	//
	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

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)