.NET Multiple ToolStripButtons in a single Context

2019-02-20 14:14发布

问题:

I'm trying to create a ContextMenu where some items in the menu contain more than a single item. You could see it as trying to combine a ToolStrip and ContextMenuItem. I've tried using a ToolStripControlHost, but this creates problems with the focus. It basically requires you to click everything in the ToolStrip twice..

ContextMenuStrip m = new ContextMenuStrip();
ToolStrip tStrip = new ToolStrip(new ToolStripDropDownButton(), new ToolStripButton());
ToolStripControlHost tsHost = new ToolStripControlHost(tStrip);
m.Items.Add(tsHost);

Any thoughts on how to achieve this?

回答1:

A ContextMenuStrip is too attractive a target for custom popup windows that don't act like a context menu. It is desirable because it automatically pops down when the user clicks outside of the menu. It has limitations though, it just isn't a very good control host. The click problem is classical one, CMS captures the mouse to detect when the user clicks outside of the window.

This really ought to be a form. To give it the same behavior as a CMS requires a bit of work though. You have to detect mouse clicks that are off the window so you can make the window disappear. Capturing the mouse like CMS does doesn't work. One trick is using IMessageFilter, it lets you take a peek at input messages before they are delivered to the window with the focus. Here's a sample form that implements this:

public partial class MyContextMenu : Form, IMessageFilter {
    public MyContextMenu() {
        InitializeComponent();
        Application.AddMessageFilter(this);
    }
    protected override void OnFormClosed(FormClosedEventArgs e) {
        Application.RemoveMessageFilter(this);
        base.OnFormClosed(e);
    }
    public void Show(Control ctl, Point pos) {
        this.StartPosition = FormStartPosition.Manual;
        this.Location = ctl.PointToScreen(pos);
        while (!(ctl is Form)) ctl = ctl.Parent;
        this.Show((Form)ctl);
    }
    public bool PreFilterMessage(ref Message m) {
        // Detect mouse clicks outside of the form
        if (m.Msg == 0x201 || m.Msg == 0x204 || m.Msg == 0x207 || 
            m.Msg == 0xA1  || m.Msg == 0xA4  || m.Msg == 0xA7) {
            Point pos = new Point(m.LParam.ToInt32());
            Control ctl = Control.FromHandle(m.HWnd);
            if (ctl != null) pos = ctl.PointToScreen(pos);
            pos = this.PointToClient(pos);
            if (pos.X < 0 || pos.Y < 0 || pos.X >= this.Width || pos.Y >= this.Height) {
                this.Close();
            }
        }
        return false;
    }
}

Use the designer as normal to design the form. You want to at least give it a different FormBorderStyle. Use the provided Show() method overload the same way you use it for CMS. Do note that the form pops down only when you click on a window that's owned by the application, unlike a CMS. Feature, not a bug.