Can I have a Checked state on a ToolStripSplitButton in WinForms?

1.6k Views Asked by At

Is there a way to have a Checked state on a ToolStripSplitButton? I want to have a ToolStripButton with a Checked property to determine if an overlay is shown in my application's window. My users want to have direct interaction to control the opacity of the overlay, and to do that I imagine having a split button where the secondary button opens a slider bar to control the opacity. (That of course isn't standard functionality, but I'm confident of being able to put that together).

However, the ToolStripSplitButton doesn't have a Checked property, so I can't do that directly. I tried to have a standard ToolStripButton with a separate ToolStripSplitButton next to it, with the DropDownButtonWidth = 11, and Margin = -5, 1, 0, 2, so that it looks like it belongs to the ToolStripButton. It looks fine, but to complete the illusion of them being a single button, I need to get them to both to draw with the same VisualStyles.PushButtonState - so they both go Hot when the mouse is over either of them.

First I tried using the MouseEnter and MouseLeave events on both buttons to try to change the background color of the other button, but that had no effect. Then I tried to use the Paint event of each button to try to render them with the ButtonRenderer.DrawButton with a PushButtonState = Hot, but that didn't seem to be working either and was becoming very messy. It seems like there must be a better way!

Is there a simple, or at least a practical solution to this?

Edit: The effect I am after is something like this:

Combined Check button with split drop-down

2

There are 2 best solutions below

1
On BEST ANSWER

The solution that I came up with was to create my own button by inheriting from ToolStripSplitButton, add my own Checked property, and then manually draw over it when it is checked:

/// <summary>
/// ToolStripSplitCheckButton adds a Check property to a ToolStripSplitButton.
/// </summary>
public partial class ToolStripSplitCheckButton : ToolStripSplitButton
{
    //==============================================================================
    // Inner class: ToolBarButonSplitCheckButtonEventArgs
    //==============================================================================

    /// <summary>
    /// The event args for the check button click event. To be able to use the OnCheckedChanged
    /// event, we must also record a dummy button as well as this one (hack).
    /// </summary>
    public class ToolBarButonSplitCheckButtonEventArgs : ToolBarButtonClickEventArgs
    {
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="split_button">The sender split check button</param>
        public ToolBarButonSplitCheckButtonEventArgs(ToolStripSplitCheckButton split_button)
            : base(new ToolBarButton("Dummy Button"))       // Hack - Dummy Button is not used
        {
            SplitCheckButton = split_button;
        }

        /// <summary>
        /// The ToolStripSplitCheckButton to be sent as an argument.
        /// </summary>
        public ToolStripSplitCheckButton SplitCheckButton { get; set; }
    }


    //==========================================================================
    // Construction

    public ToolStripSplitCheckButton()
    {
        m_checked = false;
        m_mouse_over = false;
    }


    //==========================================================================
    // Properties

    /// <summary>
    /// Indicates whether the button should toggle its Checked state on click.
    /// </summary>
    [Category("Behavior"),
    Description("Indicates whether the item should toggle its selected state when clicked."),
    DefaultValue(true)]
    public bool CheckOnClick { get; set; }

    /// <summary>
    /// Indictates the Checked state of the button.
    /// </summary>
    [Category("Behavior"),
    Description("Indicates whether the ToolStripSplitCheckButton is pressed in or not pressed in."),
    DefaultValue(false)]
    public bool Checked { get { return m_checked; } set { m_checked = value; } }


    //==========================================================================
    // Methods

    /// <summary>
    /// Toggle the click state on button click.
    /// </summary>
    protected override void OnButtonClick(EventArgs e)
    {
        if (CheckOnClick) {
            m_checked = !m_checked;
            // Raise the OnCheckStateChanged event when the button is clicked
            if (OnCheckChanged != null) {
                ToolBarButonSplitCheckButtonEventArgs args = new ToolBarButonSplitCheckButtonEventArgs(this);
                OnCheckChanged(this, args);
            }
        }
        base.OnButtonClick(e);
    }

    /// <summary>
    /// On mouse enter, record that we are over the button.
    /// </summary>
    protected override void OnMouseEnter(EventArgs e)
    {
        m_mouse_over = true;
        base.OnMouseEnter(e);
        this.Invalidate();
    }

    /// <summary>
    /// On mouse leave, record that we are no longer over the button.
    /// </summary>
    protected override void OnMouseLeave(EventArgs e)
    {
        m_mouse_over = false;
        base.OnMouseLeave(e);
        this.Invalidate();
    }

    /// <summary>
    /// Paint the check highlight when required.
    /// </summary>
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        if (m_checked) {
            // I can't get the check + mouse over to render properly, so just give the button a colour fill - Hack
            if (m_mouse_over) {
                using (Brush brush = new SolidBrush(Color.FromArgb(64, SystemColors.MenuHighlight))) {
                    e.Graphics.FillRectangle(brush, ButtonBounds);
                }
            }
            ControlPaint.DrawBorder(
                e.Graphics,
                e.ClipRectangle,            // To draw around the button + drop-down
                //this.ButtonBounds,        // To draw only around the button 
                SystemColors.MenuHighlight,
                ButtonBorderStyle.Solid);
        }
    }


    //==========================================================================
    // Member Variables

    // The delegate that acts as a signature for the function that is ultimately called 
    // when the OnCheckChanged event is triggered.
    public delegate void SplitCheckButtonEventHandler(object source, EventArgs e);
    public event SplitCheckButtonEventHandler OnCheckChanged;

    private bool m_checked;
    private bool m_mouse_over;
}

To use this, handle the OnCheckChanged event for this button.

This has a couple of clunky hacks, however. To be able to raise an OnCheckedChanged event for the check button, the event args have to include a reference to an unused dummy button in addition to the actual button. And even after experimenting with various renderers, I couldn't get the checked + mouse over render to be exactly the same as that for a normal check button, so I just draw a colour overlay, which is nearly right but not quite.

1
On

I suggest you to create your own UserControl and to use it as a ToolStripItem.

toolStripMenuItem.DropDownItems.Add(new ToolStripControlHost(new OverlayControl()));

Here is a simple example of the OverlayControl, which is deduced from your description (I suggest you to create a real UserControl with the Designer).

public class OverlayControl : UserControl
{
    private readonly CheckBox _checkBox = new CheckBox();
    private readonly TrackBar _trackBar = new TrackBar();

    public ToolStripExtended()
    {
        _checkBox.Text = "Overlay";
        BackColor = SystemColors.Window;
        _checkBox.AutoSize = true;
        _trackBar.AutoSize = true;
        var flowLayoutPanel = new FlowLayoutPanel {AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink};
        flowLayoutPanel.Controls.AddRange( new Control[] { _checkBox, _trackBar });
        Controls.Add(flowLayoutPanel);
        AutoSize = true;
        AutoSizeMode = AutoSizeMode.GrowAndShrink;
        PerformLayout();
    }
}