I have a CheckedListBox. I would like to be able to select items when I click on the text but check/uncheck them when I click on the checkbox area on the left. If I set CheckOnClick then the items are checked and unchecked whenever I click, even on the text, so that's no good. But if I clear CheckOnClick then I have to click twice to check and uncheck.
My first thought is to handle MouseClick or MouseDown events and call IndexFromPoint to find out which row is clicked. Then I would just guess that the checkbox is on the left, from x=position from 0 to, say, ItemRectangle.Height. Depending on the distance from the left, I could select or check/uncheck.
The question is whether there is a better way to determine if the mouse is over the checkbox or over the text. Different styles could have different sized checkboxes and might put them on the left, right, etc...
I wrote this and it seems to work, thanks to SLaks. To use this, CheckOnClick must be true and CheckInCheckbox as well. Inherit from CheckedListbox.
The idea is to figure out where the checkbox is and, if the click is outside of the checkbox, to set the checkstate to its opposite. Later, when the mouseclick is received by the base class, CheckedListbox, it will again change the checkbox state back to what it was.
A little hacky changing the state back and forth but I couldn't find any other way to get around the way that CheckedListbox uses SelectedIndex to check/uncheck, which is sort of a hack, too.
Private MyCheckInCheckbox As Boolean = False
''' <summary>
''' Only change the checkbox value when clicking on the box
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property CheckInCheckbox() As Boolean
Get
Return MyCheckInCheckbox
End Get
Set(ByVal value As Boolean)
MyCheckInCheckbox = value
End Set
End Property
Private Sub MyCheckedListBox_MouseClick(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseClick
If CheckInCheckbox Then
Dim border As Integer = 1
Dim index As Integer = IndexFromPoint(e.Location)
If index <> ListBox.NoMatches Then
Dim bounds As Rectangle = Me.GetItemRectangle(index)
Dim idealCheckSize As Integer
If Application.RenderWithVisualStyles Then
Dim cbState As VisualStyles.CheckBoxState
Select Case Me.GetItemCheckState(index)
Case CheckState.Checked
cbState = VisualStyles.CheckBoxState.CheckedNormal
Case CheckState.Indeterminate
cbState = VisualStyles.CheckBoxState.MixedNormal
Case CheckState.Unchecked
cbState = VisualStyles.CheckBoxState.UncheckedNormal
End Select
Dim g As Graphics = Me.CreateGraphics
idealCheckSize = CheckBoxRenderer.GetGlyphSize(g, cbState).Width
g.Dispose()
End If
Dim centeringFactor As Integer = Math.Max((bounds.Height - idealCheckSize) \ 2, 0)
If centeringFactor + idealCheckSize > bounds.Height Then
centeringFactor = bounds.Height - idealCheckSize
End If
Dim box As Rectangle = New Rectangle(bounds.X + border, bounds.Y + centeringFactor, idealCheckSize, idealCheckSize)
If RightToLeft = Windows.Forms.RightToLeft.Yes Then
box.X = bounds.X + bounds.Width - idealCheckSize - border
End If
If Not box.Contains(e.Location) Then
Me.SelectedIndex = index
SetItemChecked(index, Not GetItemChecked(index))
End If
End If
End If
End Sub
Here is the actual code (from .Net Reference Source) that paints the checkboxes, in the DrawItem
event:
Rectangle bounds = e.Bounds;
int border = 1;
int height = Font.Height + 2 * border;
// set up the appearance of the checkbox
// [Snip]
// If we are drawing themed CheckBox .. get the size from renderer..
// the Renderer might return a different size in different DPI modes..
if (Application.RenderWithVisualStyles) {
VisualStyles.CheckBoxState cbState = CheckBoxRenderer.ConvertFromButtonState(state, false, ((e.State & DrawItemState.HotLight) == DrawItemState.HotLight));
idealCheckSize = (int)(CheckBoxRenderer.GetGlyphSize(e.Graphics, cbState)).Width;
}
// Determine bounds for the checkbox
//
int centeringFactor = Math.Max((height - idealCheckSize) / 2, 0);
// Keep the checkbox within the item's upper and lower bounds
if (centeringFactor + idealCheckSize > bounds.Height) {
centeringFactor = bounds.Height - idealCheckSize;
}
Rectangle box = new Rectangle(bounds.X + border,
bounds.Y + centeringFactor,
idealCheckSize,
idealCheckSize);
if (RightToLeft == RightToLeft.Yes) {
// For a RightToLeft checked list box, we want the checkbox
// to be drawn at the right.
// So we override the X position.
box.X = bounds.X + bounds.Width - idealCheckSize - border;
}
// Draw the checkbox.
//
if (Application.RenderWithVisualStyles) {
VisualStyles.CheckBoxState cbState = CheckBoxRenderer.ConvertFromButtonState(state, false, ((e.State & DrawItemState.HotLight) == DrawItemState.HotLight));
CheckBoxRenderer.DrawCheckBox(e.Graphics, new Point(box.X, box.Y), cbState);
}
else {
ControlPaint.DrawCheckBox(e.Graphics, box, state);
}
EDIT: Here is CheckBoxRenderer.ConvertFromButtonState
:
internal static CheckBoxState ConvertFromButtonState(ButtonState state, bool isMixed, bool isHot) {
if (isMixed) {
if ((state & ButtonState.Pushed) == ButtonState.Pushed) {
return CheckBoxState.MixedPressed;
}
else if ((state & ButtonState.Inactive) == ButtonState.Inactive) {
return CheckBoxState.MixedDisabled;
}
else if (isHot) {
return CheckBoxState.MixedHot;
}
return CheckBoxState.MixedNormal;
}
else if ((state & ButtonState.Checked) == ButtonState.Checked) {
if ((state & ButtonState.Pushed) == ButtonState.Pushed) {
return CheckBoxState.CheckedPressed;
}
else if ((state & ButtonState.Inactive) == ButtonState.Inactive) {
return CheckBoxState.CheckedDisabled;
}
else if (isHot) {
return CheckBoxState.CheckedHot;
}
return CheckBoxState.CheckedNormal;
}
else { //unchecked
if ((state & ButtonState.Pushed) == ButtonState.Pushed) {
return CheckBoxState.UncheckedPressed;
}
else if ((state & ButtonState.Inactive) == ButtonState.Inactive) {
return CheckBoxState.UncheckedDisabled;
}
else if (isHot) {
return CheckBoxState.UncheckedHot;
}
return CheckBoxState.UncheckedNormal;
}
}