Search code examples
.netwinformstoolstrip

How to deal with a ToolStripMenuItem in this case?


This is a CLR Project (.NET Framework 4.5).

I have a ContextMenuStrip which is displayed when the right mouse button is pressed on a ToolStripMenuItem, since ToolStripMenuItem has no ContextMenuStrip property I had to set the right click event in the MouseUp event like this:

private: System::Void itemToolStripMenuItem_MouseUp(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) {
    if (e->Button == System::Windows::Forms::MouseButtons::Right) {
        this->contextMenuStrip1->Show(this->menuStrip1, 
        this->menuStrip1->PointToClient(System::Drawing::Point(this->Cursor->Position.X, this->Cursor->Position.Y)));
    }
}

I want the DropDown to remain open after a right mouse button click, I know that there's an AutoClose property that I could set to false:

private: System::Void contextMenuStrip1_Opening(System::Object^ sender, System::ComponentModel::CancelEventArgs^ e) {
    this->itemsToolStripMenuItem->DropDown->AutoClose = false;
}

The problem is when setting AutoClose to false it won't close the DropDown anymore. I couldn't find a Leave event to close the DropDown and set AutoClose back to true.

I also need to keep a DropDown item highlighted after a right mouse button click. This is what I'm trying to achieve:

enter image description here


Solution

  • The menu.DropDown.Closing += ...; event is used to prevent the menu from closing if an item was right clicked. The IsSelected property is overridden in a derived ToolStripMenuItem3 class in order to appear to remain selected.

    public class MyForm : Form {
    
        MenuStrip menubar = new MenuStrip { Dock = DockStyle.Top };
        ToolStripMenuItem menu = new ToolStripMenuItem("Menu");
        ToolStripDropDown menuRemove = new ToolStripDropDown();
        ToolStripMenuItem miRemove = new ToolStripMenuItem("Remove");
        ToolStripItem miClickedItem = null;
        Object tag = new Object();
    
        public MyForm() : base() {
            var item1 = new ToolStripMenuItem3("Item1");
            var item2 = new ToolStripMenuItem3("Item2");
            var item3 = new ToolStripMenuItem3("Item3");
            menu.DropDownItems.AddRange(new [] { item1, item2, item3 });
            menubar.Items.Add(menu);
            this.MainMenuStrip = menubar;
    
            Controls.Add(menubar);
    
            menuRemove.Items.Add(miRemove);
    
            menu.DropDown.MouseClick += DropDown_MouseClick;
            menu.DropDown.Closing += DropDown_Closing;
    
            menuRemove.Closed += menuRemove_Closed;
            menuRemove.ItemClicked += menuRemove_ItemClicked;
        }
    
        private class ToolStripMenuItem3 : ToolStripMenuItem {
    
            public ToolStripMenuItem3(String text) : base(text) { }
    
            public override bool Selected {
                get {
                    bool b = base.Selected;
                    return b || this.Tag != null;
                }
            }
        }
    
        void menuRemove_Closed(object sender, ToolStripDropDownClosedEventArgs e) {
            if (miClickedItem != null) {
                miClickedItem.Tag = null;
                miClickedItem.Invalidate();
                miClickedItem = null;
                menu.DropDown.Refresh();
            }
    
            // There is a bug that prevents the DropDown from auto-closing.
            // The menu.DropDown.Closing event is no longer received.
            // Calling Visible = true fixes this problem.
            menu.DropDown.Visible = true;
            bool inside = menu.DropDown.Bounds.Contains(Cursor.Position);
            if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked) { } // Remove clicked, keep menu open
            else if (e.CloseReason == ToolStripDropDownCloseReason.AppClicked) {
                if (!inside)
                    menu.DropDown.Close(); // mouse was clicked outside of the menu bounds
            }
            else
                menu.DropDown.Close();
        }
    
        void menuRemove_ItemClicked(object sender, ToolStripItemClickedEventArgs e) {
            if (e.ClickedItem == miRemove) {
                menu.DropDown.Items.Remove(miClickedItem);
                miClickedItem = null;
                if (menu.DropDown.Items.Count == 0)
                    menu.DropDown.Close(ToolStripDropDownCloseReason.CloseCalled);
            }
        }
    
        void DropDown_Closing(object sender, ToolStripDropDownClosingEventArgs e) {
            e.Cancel = (miClickedItem != null);
        }
    
        void DropDown_MouseClick(object sender, MouseEventArgs e) {
            var dropDown = (ToolStripDropDown) sender;
            if (e.Button == MouseButtons.Right) {
                Point p = e.Location;
                if (miClickedItem != null) {
                    miClickedItem.Tag = null;
                    miClickedItem.Invalidate();
                }
    
                miClickedItem = dropDown.GetItemAt(p);
                if (miClickedItem != null) {
                    // miClickedItem is null if the mouse click is on the border of the menu
                    miClickedItem.Tag = tag; // any non-null object
                    menuRemove.Show(dropDown, p);
                    miRemove.Select();
                    dropDown.Invalidate();
                }
            }
        }
    
        protected override void Dispose(bool disposing) {
            base.Dispose(disposing);
            if (disposing) {
                if (menuRemove != null) {
                    menuRemove.Dispose();
                    menuRemove = null;
                }
            }
        }
    }
    

    ToolStripMenuItem with built-in close button

    The ToolStripMenuItemX class:

    public class MyForm2 : Form {
    
        MenuStrip menubar = new MenuStrip { Dock = DockStyle.Top };
        ToolStripMenuItem menu = new ToolStripMenuItem("Menu");
    
        public MyForm2() : base() {
            var item1 = new ToolStripMenuItemX("Item1");
            var item2 = new ToolStripMenuItemX("Item2");
            var item3 = new ToolStripMenuItemX("Item3");
            menu.DropDownItems.AddRange(new [] { item1, item2, item3 });
            menubar.Items.Add(menu);
            this.MainMenuStrip = menubar;
    
            Controls.Add(menubar);
        }
    }
    
    public class ToolStripMenuItemX : ToolStripMenuItem {
    
        DrawItemState closeState = DrawItemState.None;
        bool isHit = false;
        bool cancelClose = false;
        Point ptMouse = Point.Empty;
        ToolStripDropDown menuCurrent;
        bool dispose = false;
    
        public ToolStripMenuItemX(String text) : base(text) {
        }
    
        protected override void OnParentChanged(ToolStrip oldParent, ToolStrip newParent) {
            base.OnParentChanged(oldParent, newParent);
            if (newParent is ToolStripDropDown) {
                var menu = (ToolStripDropDown) newParent;
                menu.Closing += menu_Closing;
                menuCurrent = menu;
            }
        }
    
        void menu_Closing(object sender, ToolStripDropDownClosingEventArgs e) {
            if (cancelClose || isHit)
                e.Cancel = true;
    
            if (dispose) {
                // after the MouseUp event, this menu_Closing is called immediately after, one last time
                // unhook the event and dispose this item
                menuCurrent.Closing -= menu_Closing;
                menuCurrent = null;
                Dispose();
            }
        }
    
        protected override void OnMouseMove(MouseEventArgs mea) {
            ptMouse = mea.Location;
            var r = GetCloseRectangle();
            closeState = (ptMouse.X >= r.X ? DrawItemState.HotLight : DrawItemState.Selected);
            if (!isHit && mea.Button == MouseButtons.Left) {
                // originally the user clicked the item, but then moved the mouse over the X button
                // in this case, don't close the 
                cancelClose = (ptMouse.X >= r.X);
            }
            base.OnMouseMove(mea);
            Invalidate();
        }
    
        protected override void OnMouseLeave(EventArgs e) {
            base.OnMouseLeave(e);
            closeState = DrawItemState.None;
            cancelClose = false;
            isHit = false;
            Invalidate();
        }
    
        protected override void OnMouseDown(MouseEventArgs e) { 
            var r = GetCloseRectangle();
            isHit = r.Contains(e.Location);
            cancelClose = isHit;
            base.OnMouseDown(e);
            Invalidate();
        }
    
        protected override void OnMouseUp(MouseEventArgs e) {
            base.OnMouseUp(e);
            cancelClose = false;
    
            // OnMouseUp is called after the 'Closing' event is processed, so setting canceClose has no effect here
            var r = GetCloseRectangle();
            if (isHit) {
                if (r.Contains(e.Location)) {
                    // remove the menu item, keep the drop down menu open
                    menuCurrent.Items.Remove(this);
                    menuCurrent.Invalidate();
                    dispose = true; // flags this item for disposal
                }
            }
        }
    
        public override bool Selected {
            get {
                Rectangle r = GetCloseRectangle();
                return (ptMouse.X >= r.X ? false : base.Selected);
            }
        }
    
        private Rectangle GetCloseRectangle() {
            var r = this.ContentRectangle;
            int h = r.Height;
            return new Rectangle(r.Right - h, r.Y, h, h);
        }
    
        protected override void OnPaint(PaintEventArgs e) {
            base.OnPaint(e);
            var g = e.Graphics;
            if (closeState == DrawItemState.HotLight || closeState == DrawItemState.Selected) { 
                var r = GetCloseRectangle();
    
                if (closeState == DrawItemState.HotLight) {
                    var renderer = (ToolStripProfessionalRenderer) this.DropDown.Renderer;
                    var ct = renderer.ColorTable;
                    Color c1 = (isHit ? ct.ButtonPressedHighlight : ct.ButtonSelectedHighlight);
                    Color c2 = (isHit ? ct.ButtonPressedBorder : ct.ButtonSelectedBorder);
    
                    using (var b = new SolidBrush(c1))
                        g.FillRectangle(b, r);
    
                    using (var p = new Pen(c2))
                        g.DrawRectangle(p, r);
                }
    
                using (var sf = StringFormat.GenericDefault) {
                    sf.Alignment = StringAlignment.Center;
                    sf.LineAlignment = StringAlignment.Center;
                    g.DrawString("x", this.Font, SystemBrushes.ActiveCaptionText, r, sf);
                }
            }
        }
    }