Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Visual Studio-style undo drop-down button - custom ToolStripSplitButton

Tags:

c#

winforms

I'm looking to implement a Visual Studio-style undo drop-down button:

Undo drop-down

I've looked all over the internet, and can't seem to find any real implementations of this.

I've started by deriving from ToolStripSplitButton, but don't really know where to go from there. Its DropDown property is a ToolStripDropDown, but that doesn't seem to have anything regarding multiple items being selected, much less scrolling, and the text at the bottom.

So instead of the default ToolStripDropDown, I'm thinking maybe the whole drop down part should be a custom control, based on a combobox. The question then, is how to cause the right-side (drop down arrow) button to do something other than show its default drop down?

Am I on the right track here? Thanks!

like image 760
Jonathon Reinhart Avatar asked Apr 23 '26 03:04

Jonathon Reinhart


2 Answers

Yes, I think you're on the right track. And in this case, ToolStripControlHost is your friend.

You don't necessarily need to derive from it (unless you are making your own control), but try just subscribing to the ToolStripSplitButton's DropDownOpening event:

Working example:

private ListBox listBox1;

public Form1()
{
  InitializeComponent();

  listBox1 = new ListBox();
  listBox1.IntegralHeight = false;
  listBox1.MinimumSize = new Size(120, 120);  \\ <- important
  listBox1.Items.Add("Item 1");
  listBox1.Items.Add("Item 2");
}

private void toolStripSplitButton1_DropDownOpening(object sender, EventArgs e) {
  ToolStripControlHost toolHost = new ToolStripControlHost(listBox1);
  toolHost.Size = new Size(120, 120);
  toolHost.Margin = new Padding(0);
  ToolStripDropDown toolDrop = new ToolStripDropDown();
  toolDrop.Padding = new Padding(0);
  toolDrop.Items.Add(toolHost);
  toolDrop.Show(this, new Point(toolStripSplitButton1.Bounds.Left,
                                toolStripSplitButton1.Bounds.Bottom));
}

Here is the result:

enter image description here

For your application, you would need to replace the ListBox with your own UserControl, so you can contain whatever your want in it. The ToolStripControlHost can only hold one control, and it's important to set the MinimumSize property, or else the dropped control isn't sized correctly.

like image 184
LarsTech Avatar answered Apr 24 '26 15:04

LarsTech


Extra thanks to LarsTech! (I didn't know about ToolStripControlHost a few hours ago)

Here is my implementation, which is really close to the VS drop down...

UndoRedoDropDown

You should be able to just drop this delegate & function into your Form:

    public delegate void UndoRedoCallback(int count);

    private void DrawDropDown(ToolStripSplitButton button, string action, IEnumerable<string> commands, UndoRedoCallback callback)
    {
        int width = 277;
        int listHeight = 181;
        int textHeight = 29;

        Panel panel = new Panel()
        {
            Size = new Size(width, textHeight + listHeight),
            Padding = new Padding(0),
            Margin = new Padding(0),
            BorderStyle = BorderStyle.FixedSingle,
        };
        Label label = new Label()
        {
            Size = new Size(width, textHeight),
            Location = new Point(1, listHeight - 2),
            TextAlign = ContentAlignment.MiddleCenter,
            Text = String.Format("{0} 1 Action", action),
            Padding = new Padding(0),
            Margin = new Padding(0),
        };
        ListBox list = new ListBox()
        {
            Size = new Size(width, listHeight),
            Location = new Point(1,1),
            SelectionMode = SelectionMode.MultiSimple,
            ScrollAlwaysVisible = true,
            Padding = new Padding(0),
            Margin = new Padding(0),
            BorderStyle = BorderStyle.None,
            Font = new Font(panel.Font.FontFamily, 9),
        };
        foreach (var item in commands) { list.Items.Add(item); }
        if (list.Items.Count == 0) return;
        list.SelectedIndex = 0;

        ToolStripControlHost toolHost = new ToolStripControlHost(panel)
        {
            Size = panel.Size,
            Margin = new Padding(0),
        };
        ToolStripDropDown toolDrop = new ToolStripDropDown()
        {
            Padding = new Padding(0),
        };
        toolDrop.Items.Add(toolHost);

        panel.Controls.Add(list);
        panel.Controls.Add(label);
        toolDrop.Show(this, new Point(button.Bounds.Left + button.Owner.Left, button.Bounds.Bottom + button.Owner.Top));

        // *Note: These will be "up values" that will exist beyond the scope of this function
        int index = 1;
        int lastIndex = 1;

        list.Click += (sender, e) => { toolDrop.Close(); callback(index); };
        list.MouseMove += (sender, e) =>
        {
            index = Math.Max(1, list.IndexFromPoint(e.Location) + 1);
            if (lastIndex != index)
            {
                int topIndex = Math.Max(0, Math.Min(list.TopIndex + e.Delta, list.Items.Count - 1));
                list.BeginUpdate();
                list.ClearSelected();
                for (int i = 0; i < index; ++i) { list.SelectedIndex = i; }
                label.Text = String.Format("{0} {1} Action{2}", action, index, index == 1 ? "" : "s");
                lastIndex = index;
                list.EndUpdate();
                list.TopIndex = topIndex;
            }
        };
        list.Focus();
    }

You can set it up and test like this, assuming you have a blank form (Form1) with a toolStrip that has 1 ToolStripSplitButton (toolStripSplitButton1) added:

    public Form1()
    {
        InitializeComponent();

        // Call DrawDropDown with:
        //   The clicked ToolStripSplitButton
        //   "Undo" as the action
        //   TestDropDown for the enumerable string source for the list box
        //   UndoCommands for the click callback
        toolStripSplitButton1.DropDownOpening += (sender, e) => { DrawDropDown(
            toolStripSplitButton1,
            "Undo",
            TestDropDown,
            UndoCommands
        ); };
    }


    private IEnumerable<string> TestDropDown
    {
        // Provides a list of strings for testing the drop down
        get { for (int i = 1; i < 1000; ++i) { yield return "test " + i; } }
    }

    private void UndoCommands(int count)
    {
        // Do something with the count when an action is clicked
        Console.WriteLine("Undo: {0}", count);
    }

Here is a better example using the Undo/Redo system from: http://www.codeproject.com/KB/cs/AutomatingUndoRedo.aspx

    public Form1()
    {
        InitializeComponent();

        // Call DrawDropDown with:
        //   The Undo ToolStripSplitButton button on the Standard tool strip
        //   "Undo" as the action name
        //   The list of UndoCommands from the UndoRedoManager
        //   The Undo method of the UndoRedoManager
        m_TSSB_Standard_Undo.DropDownOpening += (sender, e) => { DrawDropDown(
            m_TSSB_Standard_Undo,
            "Undo",
            UndoRedoManager.UndoCommands,
            UndoRedoManager.Undo
        ); };
    }

*Note: I did modify the Undo & Redo methods in the UndoRedoManager to accept a count:

    // Based on code by Siarhei Arkhipenka (Sergey Arhipenko) (http://www.codeproject.com/KB/cs/AutomatingUndoRedo.aspx)
    public static void Undo(int count)
    {
        AssertNoCommand();
        if (CanUndo == false) return;
        for (int i = 0; (i < count) && CanUndo; ++i)
        {
            Command command = history[currentPosition--];
            foreach (IUndoRedo member in command.Keys)
            {
                member.OnUndo(command[member]);
            }
        }
        OnCommandDone(CommandDoneType.Undo);
    }
like image 39
Josh Stribling Avatar answered Apr 24 '26 16:04

Josh Stribling