Introduction
This control came out of my need to create a CheckedListBox
control which does not occupy as much space on the form while the selected item(s) are visible. Although a few examples already exist using owner-drawn controls (i.e., drawing the ListBox
portion of the ComboBox
), I wasn't fully satisfied because of some shortcomings, hence I came up with a new version, the CheckedComboBox
which has the following features:
- Read-only text portion, displaying the checked item(s).
- Each checked item appears in the text portion as a
string
, each string
separated by a custom separator that I may define. - List portion stays open until the user finishes the selection.
- Check/uncheck all items.
- Display/hide the list portion via the keyboard.
- Mimic as closely as possible the .NET
ComboBox
. - Use it from the Visual Studio palette (as a custom control).
Implementation
The CheckedComboBox
derives from ComboBox
. In order to avoid the problem of the list portion disappearing before the user finishes the selection, I decided to catch the DropDown
event and display my own list portion which consists of a Form
whose client area is completely filled by a CheckListBox
. Both the Form
and the CheckListBox
are my own custom versions since I needed to override some of the default behaviour.
Therefore, my custom list portion appears either on the DropDown
event or when the user presses the Down arrow while the CheckedComboBox
has the focus. I overrode the OnDropDown()
, OnKeyDown()
, and the OnKeyPress()
methods to:
- display the dropdown list, and
- prevent any keyboard input, in order to make the text portion read-only.
protected override void OnDropDown(EventArgs e) {
base.OnDropDown(e);
DoDropDown();
}
private void DoDropDown() {
if (!dropdown.Visible) {
Rectangle rect = RectangleToScreen(this.ClientRectangle);
dropdown.Location = new Point(rect.X, rect.Y + this.Size.Height);
int count = dropdown.List.Items.Count;
if (count > this.MaxDropDownItems) {
count = this.MaxDropDownItems;
} else if (count == 0) {
count = 1;
}
dropdown.Size = new Size(this.Size.Width,
(dropdown.List.ItemHeight + 1) * count);
dropdown.Show(this);
}
}
protected override void OnKeyDown(KeyEventArgs e) {
if (e.KeyCode == Keys.Down) {
OnDropDown(null);
}
e.Handled = !e.Alt && !(e.KeyCode == Keys.Tab) &&
!((e.KeyCode == Keys.Left) || (e.KeyCode == Keys.Right) ||
(e.KeyCode == Keys.Home) || (e.KeyCode == Keys.End));
base.OnKeyDown(e);
}
protected override void OnKeyPress(KeyPressEventArgs e) {
e.Handled = true;
base.OnKeyPress(e);
}
Now, the list portion, being a Form
, will stay open without being closed (hidden actually) until the user either presses Esc (the Escape button) to cancel any changes, Enter to accept all changes, or clicks with the mouse anywhere outside the list, which is the usual behaviour of the ComboBox
.
In order to achieve this, I had to catch the Deactivate
event of the Form
(for the mouse click behaviour) and the keyboard events (for the Esc, Enter behaviour). In addition, if the user presses Del (the Delete button) or Shift + Del while the list portion has the focus, all items will be unchecked/checked, respectively, allowing a useful (to me, anyway :-)) keyboard shortcut. Note how a custom CCBoxEventArgs
class has been used in order to distinguish whether the Deactivate
message comes from the framework (i.e., the mouse click) or from the keyboard (to which I have control since I'm trapping keystrokes).
protected override void OnDeactivate(EventArgs e) {
base.OnDeactivate(e);
CCBoxEventArgs ce = e as CCBoxEventArgs;
if (ce != null) {
CloseDropdown(ce.AssignValues);
} else {
CloseDropdown(true);
}
}
Using the Code
The code comes as a demo project which you can run and see as an example of usage. In order to use the CheckedComboBox
in your code, you only need one file, CheckedComboBox.cs. Although I have not made it a fully-functional Visual Studio control with all the bows and whistles, once you compile your project, the CheckedComboBox
should appear under the Components in your Visual Studio ToolBox palette and you can drag and drop it on to your Form
in the Designer.
Alternatively, you can very easily use it by manually writing code like shown below to declare and create an instance of a CheckedComboBox
:
private CheckedComboBox ccb = new CheckedComboBox();
ccb.MaxDropDownItems = 5;
ccb.DisplayMember = "Name";
ccb.ValueSeparator = ", ";
In order to add items, use the Items
collection...
private string[] coloursArr = { "Red", "Green", "Black",
"White", "Orange", "Yellow",
"Blue", "Maroon", "Pink", "Purple" };
for (int i = 0; i < coloursArr.Length; i++) {
CCBoxItem item = new CCBoxItem(coloursArr[i], i);
ccb.Items.Add(item);
}
... where the CCBoxItem
is a simple test class I've created as follows (it's in the demo):
public class CCBoxItem {
private int val;
public int Value {
get { return val; }
set { val = value; }
}
private string name;
public string Name {
get { return name; }
set { name = value; }
}
public CCBoxItem() {
}
public CCBoxItem(string name, int val) {
this.name = name;
this.val = val;
}
public override string ToString() {
return string.Format("name: '{0}', value: {1}", name, val);
}
}
You can also programmatically check an item or set its CheckState
to Checked
, Unchecked
or Indeterminate
. Note that the user cannot set an item state to Indeterminate
from the User Interface, it can only be programmatically set (unless you want to handle the ItemCheck
notification and add your own logic there).
ccb.SetItemChecked(0, true);
ccb.SetItemCheckState(1, CheckState.Indeterminate);
The most interesting events you would probably be catching in your application are when the drop down portion closes (DropDownClosed
) and when an item checked state is about to change (ItemCheck
). You can register event handlers and handle the events as usual.
Note that the DropDownClosed
event is available from the ComboBox
directly, but the ItemCheck
I had to make available from the CheckListBox
.
this.ccb.DropDownClosed += new System.EventHandler(this.ccb_DropDownClosed);
ccb.ItemCheck += new System.Windows.Forms.ItemCheckEventHandler(this.ccb_ItemCheck);
private void ccb_DropDownClosed(object sender, EventArgs e) {
txtOut.AppendText("DropdownClosed\r\n");
txtOut.AppendText(string.Format("value changed: {0}\r\n", ccb.ValueChanged));
txtOut.AppendText(string.Format("value: {0}\r\n", ccb.Text));
StringBuilder sb = new StringBuilder("Items checked: ");
foreach (CCBoxItem item in ccb.CheckedItems) {
sb.Append(item.Name).Append(ccb.ValueSeparator);
}
sb.Remove(sb.Length-ccb.ValueSeparator.Length, ccb.ValueSeparator.Length);
txtOut.AppendText(sb.ToString());
txtOut.AppendText("\r\n");
}
private void ccb_ItemCheck(object sender, ItemCheckEventArgs e) {
CCBoxItem item = ccb.Items[e.Index] as CCBoxItem;
txtOut.AppendText(string.Format("Item '{0}' is about to be {1}\r\n",
item.Name, e.NewValue.ToString()));
}
In the code above, you can see that the checked items are available via the CheckedItems
property. Similarly, CheckedIndices
will return a collection of checked indices. Both these collections correspond to the .NET CheckedListBox
collection, so nothing unfamiliar here.
History
In version 2.0, I have included some minor fixes/enhancements as follows: