Check boxes on menu items are nice, but sometimes you want to display options that are more complex than true/false. How do you display radio buttons on menus? The following sneak preview explains how. This topic will appear in the next documentation update.

How to: Display Option Buttons in a MenuStrip

Option buttons, also known as radio buttons, are similar to check boxes except that users can select only one at a time. Although by default the ToolStripMenuItem class does not provide option-button behavior, the class does provide check-box behavior that you can customize to implement option-button behavior for menu items in a MenuStrip control.

When the CheckOnClick property of a menu item is true, users can click the item to toggle the display of a check mark. The Checked property indicates the current state of the item. To implement basic option-button behavior, you must ensure that when an item is selected, you set the Checked property for the previously selected item to false.

The following procedures describe how to implement this and additional functionality in a class that inherits the ToolStripMenuItem class. The ToolStripRadioButtonMenuItem class overrides members such as OnCheckedChanged and OnPaint to provide the selection behavior and appearance of option buttons. Additionally, this class overrides the Enabled property so that options on a submenu are disabled unless the parent item is selected.

To implement option-button selection behavior

1. Initialize the CheckOnClick property to true to enable item selection.

Visual Basic  
' Called by all constructors to initialize CheckOnClick.
Private Sub Initialize()
    CheckOnClick = True
End Sub

C#  
// Called by all constructors to initialize CheckOnClick.
private void Initialize()
{
    CheckOnClick = true;
}

2. Override the OnCheckedChanged method to clear the selection of the previously selected item when a new item is selected.

Visual Basic  
Protected Overrides Sub OnCheckedChanged(ByVal e As EventArgs)

    MyBase.OnCheckedChanged(e)

    ' If this item is no longer in the checked state, do nothing.
    If Not Checked Then Return

    ' Clear the checked state for all siblings.
    For Each item As ToolStripItem In Parent.Items

        Dim radioItem As ToolStripRadioButtonMenuItem = _
            TryCast(item, ToolStripRadioButtonMenuItem)
        If radioItem IsNot Nothing AndAlso _
            radioItem IsNot Me AndAlso _
            radioItem.Checked Then

            radioItem.Checked = False

            ' Only one item can be selected at a time,
            ' so there is no need to continue.
            Return

        End If
    Next

End Sub

C#  
protected override void OnCheckedChanged(EventArgs e)
{
    base.OnCheckedChanged(e);

    // If this item is no longer in the checked state, do nothing.
    if (!Checked) return;

    // Clear the checked state for all siblings.
    foreach (ToolStripItem item in Parent.Items)
    {
        ToolStripRadioButtonMenuItem radioItem =
            item as ToolStripRadioButtonMenuItem;
        if (radioItem != null && radioItem != this && radioItem.Checked)
        {
            radioItem.Checked = false;

            // Only one item can be selected at a time,
            // so there is no need to continue.
            return;
        }
    }
}

3. Override the OnClick method to ensure that clicking an item that has already been selected will not clear the selection.

Visual Basic  
Protected Overrides Sub OnClick(ByVal e As EventArgs)

    ' If the item is already in the checked state, do not call
    ' the base method, which would toggle the value.
    If Checked Then Return

    MyBase.OnClick(e)
End Sub

C#  
protected override void OnClick(EventArgs e)
{
    // If the item is already in the checked state, do not call
    // the base method, which would toggle the value.
    if (Checked) return;

    base.OnClick(e);
}

To modify the appearance of the option-button items

1. Override the OnPaint method to paint over the default check-mark by using the RadioButtonRenderer class.

Visual Basic  
' Let the item paint itself, and then paint the RadioButton
' where the check mark is displayed, covering the check mark
' if it is present.
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)

    MyBase.OnPaint(e)

    ' If the client sets the Image property, the selection behavior
    ' remains unchanged, but the RadioButton is not displayed and the
    ' selection is indicated only by the selection rectangle.
    If Image IsNot Nothing Then Return

    ' Determine the correct state of the RadioButton.
    Dim buttonState As RadioButtonState = _
        RadioButtonState.UncheckedNormal
    If Enabled Then
        If mouseDownState Then
            If Checked Then
                buttonState = RadioButtonState.CheckedPressed
            Else
                buttonState = RadioButtonState.UncheckedPressed
            End If
        ElseIf mouseHoverState Then
            If Checked Then
                buttonState = RadioButtonState.CheckedHot
            Else
                buttonState = RadioButtonState.UncheckedHot
            End If
        Else
            If Checked Then buttonState = RadioButtonState.CheckedNormal
        End If
    Else
        If Checked Then
            buttonState = RadioButtonState.CheckedDisabled
        Else
            buttonState = RadioButtonState.UncheckedDisabled
        End If
    End If

    ' Calculate the position at which to display the RadioButton.
    Dim offset As Int32 = CInt((ContentRectangle.Height - _
        RadioButtonRenderer.GetGlyphSize( _
        e.Graphics, buttonState).Height) / 2)
    Dim imageLocation As Point = New Point( _
        ContentRectangle.Location.X + 4, _
        ContentRectangle.Location.Y + offset)

    ' If the item is selected and the RadioButton paints with partial
    ' transparency, such as when theming is enabled, the check mark
    ' shows through the RadioButton image. In this case, paint a
    ' non-transparent background first to cover the check mark.
    If Checked AndAlso RadioButtonRenderer _
        .IsBackgroundPartiallyTransparent(buttonState) Then

        Dim glyphSize As Size = RadioButtonRenderer _
            .GetGlyphSize(e.Graphics, buttonState)
        glyphSize.Height -= 1
        glyphSize.Width -= 1
        Dim backgroundRectangle As _
            New Rectangle(imageLocation, glyphSize)
        e.Graphics.FillEllipse( _
            SystemBrushes.Control, backgroundRectangle)
    End If

    RadioButtonRenderer.DrawRadioButton( _
        e.Graphics, imageLocation, buttonState)

End Sub

C#  
// Let the item paint itself, and then paint the RadioButton
// where the check mark is displayed, covering the check mark
// if it is present.
protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);

    // If the client sets the Image property, the selection behavior
    // remains unchanged, but the RadioButton is not displayed and the
    // selection is indicated only by the selection rectangle.
    if (Image != null) return;

    // Determine the correct state of the RadioButton.
    RadioButtonState buttonState = RadioButtonState.UncheckedNormal;
    if (Enabled)
    {
        if (mouseDownState)
        {
            if (Checked) buttonState = RadioButtonState.CheckedPressed;
            else buttonState = RadioButtonState.UncheckedPressed;
        }
        else if (mouseHoverState)
        {
            if (Checked) buttonState = RadioButtonState.CheckedHot;
            else buttonState = RadioButtonState.UncheckedHot;
        }
        else
        {
            if (Checked) buttonState = RadioButtonState.CheckedNormal;
        }
    }
    else
    {
        if (Checked) buttonState = RadioButtonState.CheckedDisabled;
        else buttonState = RadioButtonState.UncheckedDisabled;
    }

    // Calculate the position at which to display the RadioButton.
    Int32 offset = (ContentRectangle.Height -
        RadioButtonRenderer.GetGlyphSize(
        e.Graphics, buttonState).Height) / 2;
    Point imageLocation = new Point(
        ContentRectangle.Location.X + 4,
        ContentRectangle.Location.Y + offset);

    // If the item is selected and the RadioButton paints with partial
    // transparency, such as when theming is enabled, the check mark
    // shows through the RadioButton image. In this case, paint a
    // non-transparent background first to cover the check mark.
    if (Checked && RadioButtonRenderer
        .IsBackgroundPartiallyTransparent(buttonState))
    {
        Size glyphSize = RadioButtonRenderer
            .GetGlyphSize(e.Graphics, buttonState);
        glyphSize.Height--;
        glyphSize.Width--;
        Rectangle backgroundRectangle =
            new Rectangle(imageLocation, glyphSize);
        e.Graphics.FillEllipse(
            SystemBrushes.Control, backgroundRectangle);
    }

    RadioButtonRenderer.DrawRadioButton(
        e.Graphics, imageLocation, buttonState);
}

2. Override the OnMouseEnter, OnMouseLeave, OnMouseDown, and OnMouseUp methods to track the state of the mouse and ensure that the OnPaint method paints the correct option-button state.

Visual Basic  
Private mouseHoverState As Boolean = False

Protected Overrides Sub OnMouseEnter(ByVal e As EventArgs)
    mouseHoverState = True

    ' Force the item to repaint with the new RadioButton state.
    Invalidate()

    MyBase.OnMouseEnter(e)
End Sub

Protected Overrides Sub OnMouseLeave(ByVal e As EventArgs)
    mouseHoverState = False
    MyBase.OnMouseLeave(e)
End Sub

Private mouseDownState As Boolean = False

Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
    mouseDownState = True

    ' Force the item to repaint with the new RadioButton state.
    Invalidate()

    MyBase.OnMouseDown(e)
End Sub

Protected Overrides Sub OnMouseUp(ByVal e As MouseEventArgs)
    mouseDownState = False
    MyBase.OnMouseUp(e)
End Sub

C#  
private bool mouseHoverState = false;

protected override void OnMouseEnter(EventArgs e)
{
    mouseHoverState = true;

    // Force the item to repaint with the new RadioButton state.
    Invalidate();

    base.OnMouseEnter(e);
}

protected override void OnMouseLeave(EventArgs e)
{
    mouseHoverState = false;
    base.OnMouseLeave(e);
}

private bool mouseDownState = false;

protected override void OnMouseDown(MouseEventArgs e)
{
    mouseDownState = true;

    // Force the item to repaint with the new RadioButton state.
    Invalidate();

    base.OnMouseDown(e);
}

protected override void OnMouseUp(MouseEventArgs e)
{
    mouseDownState = false;
    base.OnMouseUp(e);
}

To disable options on a submenu when the parent item is not selected

1. Override the Enabled property so that the item is disabled when it has a parent item with both a CheckOnClick value of true and a Checked value of false.

Visual Basic  
' Enable the item only if its parent item is in the checked state
' and its Enabled property has not been explicitly set to false.
Public Overrides Property Enabled() As Boolean
    Get
        Dim ownerMenuItem As ToolStripMenuItem = _
            TryCast(OwnerItem, ToolStripMenuItem)

        ' Use the base value in design mode to prevent the designer
        ' from setting the base value to the calculated value.
        If Not DesignMode AndAlso _
           
ownerMenuItem IsNot Nothing AndAlso _
            ownerMenuItem.CheckOnClick Then
            Return MyBase.Enabled AndAlso ownerMenuItem.Checked
        Else
            Return MyBase.Enabled
        End If
    End Get

    Set(ByVal value As Boolean)
        MyBase.Enabled = value
    End Set
End Property

C#  
// Enable the item only if its parent item is in the checked state
// and its Enabled property has not been explicitly set to false.
public override bool Enabled
{
    get
    {
        ToolStripMenuItem ownerMenuItem =
            OwnerItem as ToolStripMenuItem;

        // Use the base value in design mode to prevent the designer
        // from setting the base value to the calculated value.
        if (!DesignMode &&
            ownerMenuItem != null && ownerMenuItem.CheckOnClick)
        {
            return base.Enabled && ownerMenuItem.Checked;
        }
        else return base.Enabled;
    }
    set
    {
        base.Enabled = value;
    }
}

2. Override the OnOwnerChanged method to subscribe to the CheckedChanged event of the parent item.

Visual Basic  
' When OwnerItem becomes available, if it is a ToolStripMenuItem
' with a CheckOnClick property value of true, subscribe to its
' CheckedChanged event.
Protected Overrides Sub OnOwnerChanged(ByVal e As EventArgs)

    Dim ownerMenuItem As ToolStripMenuItem = _
        TryCast(OwnerItem, ToolStripMenuItem)

    If ownerMenuItem IsNot Nothing AndAlso _
        ownerMenuItem.CheckOnClick Then
        AddHandler ownerMenuItem.CheckedChanged, New _
            EventHandler(AddressOf OwnerMenuItem_CheckedChanged)
    End If

    MyBase.OnOwnerChanged(e)

End Sub

C#  
// When OwnerItem becomes available, if it is a ToolStripMenuItem
// with a CheckOnClick property value of true, subscribe to its
// CheckedChanged event.
protected override void OnOwnerChanged(EventArgs e)
{
    ToolStripMenuItem ownerMenuItem =
        OwnerItem as ToolStripMenuItem;
    if (ownerMenuItem != null && ownerMenuItem.CheckOnClick)
    {
        ownerMenuItem.CheckedChanged +=
            new EventHandler(OwnerMenuItem_CheckedChanged);
    }
    base.OnOwnerChanged(e);
}

3. In the handler for the parent-item CheckedChanged event, invalidate the item to update the display with the new enabled state.

Visual Basic  
' When the checked state of the parent item changes,
' repaint the item so that the new Enabled state is displayed.
Private Sub OwnerMenuItem_CheckedChanged( _
    ByVal sender As Object, ByVal e As EventArgs)
    Invalidate()
End Sub

C#  
// When the checked state of the parent item changes,
// repaint the item so that the new Enabled state is displayed.
private void OwnerMenuItem_CheckedChanged(
    object sender, EventArgs e)
{
    Invalidate();
}

Example

You can download the complete code example from the Attachment(s) section below. The code example provides the complete ToolStripRadioButtonMenuItem class, plus a Form class and Program class to demonstrate the option-button behavior.

Compiling the Code

This example requires:

  • References to the System, System.Drawing, and System.Windows.Forms assemblies.