48

I'm developing simple C# application using Windows.Forms on .NET. I need some button that will show a drop-down menu with subcategories - much like ToolStripMenu, but the button, you know. I searched for it and could not found any variants.

My question is: is there any way to do this, maybe some secret button property that allows attach menu to it?

Any help will be appreciated.

tonytony
  • 1,994
  • 3
  • 20
  • 27

10 Answers10

77

Button have down arrow right side of it and you can set menu of it from designer:

screenshot

With ShowMenuUnderCursor:

screenshot

MenuButton class:

public class MenuButton : Button
{
    [DefaultValue(null)]
    public ContextMenuStrip Menu { get; set; }

    [DefaultValue(false)]
    public bool ShowMenuUnderCursor { get; set; }

    protected override void OnMouseDown(MouseEventArgs mevent)
    {
        base.OnMouseDown(mevent);

        if (Menu != null && mevent.Button == MouseButtons.Left)
        {
            Point menuLocation;

            if (ShowMenuUnderCursor)
            {
                menuLocation = mevent.Location;
            }
            else
            {
                menuLocation = new Point(0, Height - 1);
            }

            Menu.Show(this, menuLocation);
        }
    }

    protected override void OnPaint(PaintEventArgs pevent)
    {
        base.OnPaint(pevent);

        if (Menu != null)
        {
            int arrowX = ClientRectangle.Width - Padding.Right - 14;
            int arrowY = (ClientRectangle.Height / 2) - 1;

            Color color = Enabled ? ForeColor : SystemColors.ControlDark;
            using (Brush brush = new SolidBrush(color))
            {
                Point[] arrows = new Point[] { new Point(arrowX, arrowY), new Point(arrowX + 7, arrowY), new Point(arrowX + 3, arrowY + 4) };
                pevent.Graphics.FillPolygon(brush, arrows);
            }
        }
    }
}
Jaex
  • 4,204
  • 2
  • 34
  • 56
  • 2
    Thank you for the reply! I really thought of button with arrow when posted the question, and it's a great idea to draw the arrow using the Graphics API. – tonytony Jun 06 '14 at 22:00
  • 2
    Why `Menu`'s value is always `null`, no matter how to set the `ContextMenuStrip` property value? – Ghasem Nov 02 '15 at 05:16
  • 2
    @Alex Add a new ContextMenuStrip object to the `Menu` property, not the `ContextMenuStrip` property. `myButton.Menu = New ContextMenuStrip` .. you are probably doing `myButton.ContextMenuStrip = New ContextMenuStrip` – cowsay May 31 '16 at 16:32
  • 1
    If you happen to just want to use a button's `OnPaint` event instead of inheriting from `Button`, remember to use the button's dimensions and not `ClientRectangle`'s. Like this: `Button btnSender = (Button)sender; int arrowX = btnSender.Width - 14; int arrowY = btnSender.Height / 2 - 1;` – Andrew Dec 24 '16 at 01:49
  • Love this! But does anyone have a solution for this that shifts a zoomed BackgroundImage to the left so the arrow does not overwrite it? – Scott Aug 11 '20 at 19:51
  • Awesome! I changed the Button class directly in the Designer code, and now my button already show the arrow in the Designer window! – Vitox Jun 08 '23 at 05:24
53

You can show the ContextMenuStrip on the click event:

private void button1_Click(object sender, EventArgs e) {
  contextMenuStrip1.Show(button1, new Point(0, button1.Height));
}

To make your own determination whether to show the menu above or below the button, you can try using this code, which measures the menu and determines whether or not it would be partially offscreen:

private void button1_Click(object sender, EventArgs e) {
  Point screenPoint = button1.PointToScreen(new Point(button1.Left, button1.Bottom));
  if (screenPoint.Y + contextMenuStrip1.Size.Height > Screen.PrimaryScreen.WorkingArea.Height) {
    contextMenuStrip1.Show(button1, new Point(0, -contextMenuStrip1.Size.Height));
  } else {
    contextMenuStrip1.Show(button1, new Point(0, button1.Height));
  }    
}
LarsTech
  • 80,625
  • 14
  • 153
  • 225
  • 1
    Looks like what I need. Do you know a good way to determine whether the menu should drop-down from the button's bottom or drop-up from the buttons top border? – tonytony May 29 '12 at 16:55
  • 1
    @tonytony It should happen sort of automatically. If the menu is below the bottom of the screen, windows will push it up so that it's all visible. – LarsTech May 29 '12 at 16:59
  • 1
    What about overlapping the button? I think I should know whether it will go up or down. – tonytony May 29 '12 at 17:02
30

Expanding @Jaex answer a little bit to allow for a separator line, conditional drawing of the arrow if nothing is configured and a separate click event for the main button body and the menu arrow.

It should be noted that for better alignment you can set the button.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;

enter image description here

Here is my slight improvement

public class SplitButton : Button
{
    [DefaultValue(null), Browsable(true),
    DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public ContextMenuStrip Menu { get; set; }

    [DefaultValue(20), Browsable(true),
    DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public int SplitWidth { get; set; }

    public SplitButton() 
    {
        SplitWidth = 20;
    }

    protected override void OnMouseDown(MouseEventArgs mevent)
    {
        var splitRect = new Rectangle(this.Width - this.SplitWidth, 0, this.SplitWidth, this.Height);

        // Figure out if the button click was on the button itself or the menu split
        if (Menu != null && 
            mevent.Button == MouseButtons.Left &&
            splitRect.Contains(mevent.Location) )
        {
            Menu.Show(this, 0, this.Height);    // Shows menu under button
            //Menu.Show(this, mevent.Location); // Shows menu at click location
        }
        else
        {
            base.OnMouseDown(mevent);
        }
    }

    protected override void OnPaint(PaintEventArgs pevent)
    {
        base.OnPaint(pevent);

        if (this.Menu != null && this.SplitWidth > 0)
        { 
            // Draw the arrow glyph on the right side of the button
            int arrowX = ClientRectangle.Width - 14;
            int arrowY = ClientRectangle.Height / 2 - 1;

            var arrowBrush = Enabled ? SystemBrushes.ControlText : SystemBrushes.ButtonShadow;
            var arrows = new[] { new Point(arrowX, arrowY), new Point(arrowX + 7, arrowY), new Point(arrowX + 3, arrowY + 4) };
            pevent.Graphics.FillPolygon(arrowBrush, arrows);

            // Draw a dashed separator on the left of the arrow
            int lineX = ClientRectangle.Width - this.SplitWidth;
            int lineYFrom = arrowY - 4;
            int lineYTo = arrowY + 8;
            using( var separatorPen = new Pen(Brushes.DarkGray){DashStyle = DashStyle.Dot})
            {
                pevent.Graphics.DrawLine(separatorPen, lineX, lineYFrom, lineX, lineYTo);
            }
        }
    }
}
Sverrir Sigmundarson
  • 2,453
  • 31
  • 27
  • 4
    `var splitRect = new Rectangle(this.Width - this.SplitWidth, this.Location.Y, this.SplitWidth, this.Height);` the second parameter should be changed to **0** – Jacky1205 Jan 11 '15 at 02:29
  • 2
    Jacky is right. The dropdown "click area" does not cover all the button's height if that parameter is not 0. – Jan Kadeřábek Feb 26 '17 at 21:18
  • 3
    I've edited the code to fix the bug that @Jacky identified. – Wolfgang Dec 01 '17 at 16:11
  • How to call the drop-down menu without using the mouse, using only the keyboard? – Andrei Krasutski Feb 14 '19 at 08:34
  • 2
    Great code. I suggest updating 7th line from the end to `using (var separatorPen = new Pen(Brushes.DarkGray) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dot })` That way it doesn't require having the library included. – James Carlyle-Clarke May 19 '19 at 13:11
8

The simplest option would be to use the ToolStripDropDownButton in an undocked ToolStrip that only shows the single button. Then you can add sub-items to it, etc. To do this: - drag a Toolstrip onto your control/form - use the layout helper to add a DropDownButton - set GripStyle to Hidden - set Dock to None

The result is a standalone toolbar-style button that supports the drop-down behavior that you described.

JoshL
  • 10,737
  • 11
  • 55
  • 61
  • 1
    Thanks for reply, that's clean and straightforward, but not quite what I need. Menu button is kind of unbodied, while I want something more fleshy :) – tonytony May 29 '12 at 16:54
  • Actually to make it really work as expected, you add one ToolStripButton which acts as the main button and then add a ToolStripDropDownButton where the drop items are located. This way you avoid having to deal with the weird logic of button click and drop down handling. and you can style your dropdown arrow using an image of your choosing. – Hefaistos68 Feb 01 '21 at 17:45
7

easy was we can do it. this may help :)

ContextMenuStrip contextMenuStrip1 = new ContextMenuStrip();

        private void button1_Click(object sender, EventArgs e)
        {
            contextMenuStrip1.Items.Clear();
            contextMenuStrip1.Items.Add("item1");
            contextMenuStrip1.Items.Add("item2");

            contextMenuStrip1.Show(button1, new Point(0, button1.Height));
        }

        private void contextMenuStrip1_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
        {
            if (e.ClickedItem.Text == "item1")
            {
                MessageBox.Show(e.ClickedItem.Text);
            }
        }
Mou
  • 15,673
  • 43
  • 156
  • 275
3

Jaex's MenuButton class above was perfect for me. I did add the logic below into the OnMouseDown so that the context menu would only show up if I clicked on the arrow. The normal click event would get triggered if I clicked in the larger portion. Allowed for a "Default" click action.

if (Menu != null && mevent.Button == MouseButtons.Left)
{
    if (mevent.Location.X >= this.Width - 14)
    {
        System.Drawing.Point menuLocation;

        if (ShowMenuUnderCursor)
        {
            menuLocation = mevent.Location; 
        }
        else
        {
            menuLocation = new System.Drawing.Point(0, Height);
        }

        Menu.Show(this, menuLocation);
    }
}

Thought this might be useful to someone. Thanks Jaex

Jeff Scott
  • 61
  • 1
  • 4
2

Show context menu below button when it's clicked.

clearpath
  • 916
  • 8
  • 24
0

Infragistics has the WinDropDownButton: http://help.infragistics.com/Help/NetAdvantage/WinForms/2012.1/CLR2.0/html/WinDropDownButton_About_WinDropDownButton.html

So it certainly exists, however you may not be looking for a paid third-party control.

roken
  • 3,946
  • 1
  • 19
  • 32
0

I was fiddling with this issue as well and found an extremely simple solution (albeit a little dirty-hacky): place a ComboBox under the Button, such that it shows the dropdown arrow right next to the button.

Then use SelectedIndexChanged of the ComboBox to change the Button behaviour, or do what you want it to do immediately.

Bart Friederichs
  • 33,050
  • 15
  • 95
  • 195
0

So I have come up with a custom control that is based on a toolstrip and thus fully customizable with text and images, having its own click event for each button/action. And it can be designed in the winform editor. There are a few minor layout issues like the alignment of the dropdown items, but nothing really serious. The button will make a once clicked drop item the main item, this can be changed in the OnActions_DropDownItemClicked() method

Button closed

Button dropped

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

//System.Windows.Forms.Design.ControlDesigner
namespace YourNamespace
{
    /// <summary>
    /// Implements a drop button using only standard winform controls
    /// </summary>
    [DesignerSerializer("System.Windows.Forms.Design.ToolStripCodeDomSerializer, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.ComponentModel.Design.Serialization.CodeDomSerializer, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
    [Designer("System.Windows.Forms.Design.ControlDesigner, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
    public class DropButton : ToolStrip
    {
        #region Private Fields

        private List<ActionButtonInfo> _actionButtons = new List<ActionButtonInfo>();

        private ToolStripLayoutStyle _layoutStyle = ToolStripLayoutStyle.Flow;
        private int _splitButtonWidth = 30;
        private System.Windows.Forms.ToolStripDropDownButton btnActions;

        private System.Windows.Forms.ToolStripButton btnMainAction;

        #endregion Private Fields

        #region Public Properties

        /// <summary>
        /// Gets or sets the action buttons.
        /// </summary>
        public List<ActionButtonInfo> ActionButtons
        {
            get
            {
                return this._actionButtons;
            }

            set
            {
                this._actionButtons = value;
                SetupActionButtons();
            }
        }

        /// <summary>
        /// Gets or sets the drop down direction.
        /// </summary>
        public ToolStripDropDownDirection DropDownDirection
        {
            get; set;
        }

        /// <inheritdoc/>
        [Browsable(false)]
        public new ToolStripGripStyle GripStyle => ToolStripGripStyle.Hidden;

        /// <inheritdoc/>
        [Browsable(false)]
        public new ToolStripItemCollection Items
        {
            get
            {
                return base.Items;
            }
        }

        /// <inheritdoc/>
        [Browsable(false)]
        public new ToolStripLayoutStyle LayoutStyle => _layoutStyle;

        public new ToolStripLayoutStyle LayoutStyle1 => ToolStripLayoutStyle.Flow;

        /// <summary>
        /// Gets or sets the split button width.
        /// </summary>
        public int SplitButtonWidth
        {
            get
            {
                return _splitButtonWidth;
            }

            set
            {
                if(value < 10 || value > this.Width)
                {
                    throw new ArgumentOutOfRangeException();
                }

                _splitButtonWidth = value;
                ResizeButtons();
            }
        }

        #endregion Public Properties

        #region Private Methods

        /// <summary>
        /// The actual implementation that adds a button to the button list
        /// </summary>
        /// <param name="abi">The abi.</param>
        private void AddActionButtonImpl(ActionButtonInfo abi)
        {
            ToolStripItem tsi = new ToolStripButton
            {
                AutoSize = false,
                Text = abi.Text,
                Image = abi.Image,
                Tag = abi,
                Height = btnMainAction.Height,
                Width = btnMainAction.Width + btnActions.Width,
                TextImageRelation = TextImageRelation.ImageBeforeText,
                TextAlign = ContentAlignment.MiddleLeft,
                Padding = new System.Windows.Forms.Padding(2, 2, 2, 2)
            };

            btnActions.DropDownItems.Add(tsi);
        }

        private void OnActions_DropDownItemClicked(object sender, ToolStripItemClickedEventArgs e)
        {
            if(e.ClickedItem != null && !String.IsNullOrEmpty(e.ClickedItem.Text))
            {
                ActionButtonInfo abi = e.ClickedItem.Tag as ActionButtonInfo;
                if(abi != null)
                {
                    SetMainButton(abi);
                    abi.Clicked?.Invoke(this, null);
                }
            }
        }

        private void OnbtnActions_DropDownOpening(object sender, EventArgs e)
        {
            ToolStripDropDownMenu tdd = btnActions.DropDown as ToolStripDropDownMenu;

            tdd.DefaultDropDownDirection = ToolStripDropDownDirection.BelowLeft;
            tdd.ShowCheckMargin = false;
            tdd.ShowImageMargin = false;

            tdd.MinimumSize = btnMainAction.Size;
        }

        /// <summary>
        /// Resizes the buttons.
        /// </summary>
        /// <param name="suspend">If true, suspend.</param>
        private void ResizeButtons(bool suspend = true)
        {
            if(btnActions is null || btnMainAction is null)
                return;

            if(suspend)
                this.SuspendLayout();

            int marginX = (this.Margin.Left + this.Margin.Right);
            int marginY = (this.Margin.Top + this.Margin.Bottom);
            btnMainAction.Width = this.Width - _splitButtonWidth - marginX;
            btnActions.Width = _splitButtonWidth - marginX - 1;

            btnMainAction.Height = this.Height - marginY;
            btnActions.Height = this.Height - marginY;

            if(suspend)
                this.ResumeLayout(true);
        }

        /// <summary>
        /// Sets the main button.
        /// </summary>
        /// <param name="abi">The abi.</param>
        private void SetMainButton(ActionButtonInfo abi)
        {
            btnMainAction.Image = abi.Image;
            btnMainAction.Text = abi.Text;

            // btnMainAction.Click += abi.Clicked;
            btnMainAction.Tag = abi;
        }

        /// <summary>
        /// Setups the action buttons.
        /// </summary>
        private void SetupActionButtons()
        {
            if(_actionButtons.Count == 0)
            {
                btnActions.Enabled = false;
                return;
            }

            btnActions.Enabled = true;

            SetMainButton(_actionButtons[0]);

            foreach(ActionButtonInfo abi in _actionButtons)
            {
                AddActionButtonImpl(abi);
            }

            btnActions.DropDownOpening += OnbtnActions_DropDownOpening;
        }

        #endregion Private Methods

        #region Protected Methods

        /// <inheritdoc/>
        protected override void OnCreateControl()
        {
            System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DropButton));

            base.OnCreateControl();

            this.btnMainAction = new System.Windows.Forms.ToolStripButton();
            this.btnActions = new System.Windows.Forms.ToolStripDropDownButton();

            this.SuspendLayout();

            this.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
            this.btnMainAction,
            this.btnActions});

            this.MinimumSize = new Size(100, 40);
            base.GripStyle = System.Windows.Forms.ToolStripGripStyle.Hidden;
            base.LayoutStyle = System.Windows.Forms.ToolStripLayoutStyle.Flow;
            this.AutoSize = false;
            this.Dock = DockStyle.None;

            // this.ItemClicked += new System.Windows.Forms.ToolStripItemClickedEventHandler(this.toolStripAction_ItemClicked);

            //
            // btnMainAction
            //
            this.btnMainAction.AutoSize = false;
            this.btnMainAction.BackColor = System.Drawing.Color.Gainsboro;
            this.btnMainAction.ForeColor = System.Drawing.Color.Black;
            this.btnMainAction.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
            this.btnMainAction.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
            this.btnMainAction.ImageTransparentColor = System.Drawing.Color.Magenta;
            this.btnMainAction.Name = "btnMainAction";
            this.btnMainAction.Size = new System.Drawing.Size(this.Width, this.Height);
            this.btnMainAction.Text = "Test";

            //
            // btnActions
            //
            this.btnActions.AutoSize = false;
            this.btnActions.AutoToolTip = false;
            this.btnActions.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Image;
            this.btnActions.BackColor = System.Drawing.Color.Gainsboro;
            this.btnActions.ForeColor = System.Drawing.Color.Black;
            this.btnActions.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point);
            this.btnActions.Image = Properties.Resources.DropButtonArrow;
            this.btnActions.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
            this.btnActions.Name = "btnActions";
            this.btnActions.ShowDropDownArrow = false;
            this.btnActions.Size = new System.Drawing.Size(_splitButtonWidth, this.Height);
            this.btnActions.TextImageRelation = System.Windows.Forms.TextImageRelation.Overlay;

            btnActions.DropDownDirection = ToolStripDropDownDirection.BelowLeft;
            btnActions.DropDownItemClicked += OnActions_DropDownItemClicked;

            ResizeButtons(false);

            this.ResumeLayout(false);
            this.PerformLayout();
        }

        /// <summary>
        /// Propagate font changes to the child controls
        /// </summary>
        /// <param name="e"></param>
        protected override void OnFontChanged(EventArgs e)
        {
            base.OnFontChanged(e);

            if(btnActions is null || btnMainAction is null)
                return;

            btnMainAction.Font = this.Font;
            btnActions.Font = this.Font;
        }

        /// <inheritdoc/>
        protected override void OnLayout(LayoutEventArgs e)
        {
            ResizeButtons(false);
            base.OnLayout(e);
        }
    
        #endregion Protected Methods

        #region Public Methods

        /// <summary>
        /// Adds an action button.
        /// </summary>
        /// <param name="actionButtonInfo">The action button info.</param>
        public void AddActionButton(ActionButtonInfo actionButtonInfo)
        {
            _actionButtons.Add(actionButtonInfo);

            if(_actionButtons.Count == 1)
                SetupActionButtons();
            else
                AddActionButtonImpl(actionButtonInfo);
        }

        #endregion Public Methods
    }
}
Hefaistos68
  • 381
  • 3
  • 9