views:

500

answers:

2

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...

+1  A: 

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;
   }

}

SLaks
I can't find CheckBoxRender.ConvertFromButtonState anywhere! Is it some internal API that I can't access?
Eyal
A: 

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
Eyal