atlanteh
atlanteh

Reputation: 5835

Prevent ToolStripMenuItems from jumping to second screen

I have an application that is mostly operated through NotifyIcon's ContextMenuStrip
There are multiple levels of ToolStripMenuItems and the user can go through them.
The problem is, that when the user has two screen, the MenuItems jump to second screen when no space is available. like so:

enter image description here

How can I force them to stay on the same screen? I've tried to search through the web but couldn't find an appropriate answer.

Here is a sample piece of code i'm using to test this senario:

public partial class Form1 : Form
{

    public Form1()
    {
        InitializeComponent();

        var resources = new ComponentResourceManager(typeof(Form1));
        var notifyIcon1 = new NotifyIcon(components);
        var contextMenuStrip1 = new ContextMenuStrip(components);
        var level1ToolStripMenuItem = new ToolStripMenuItem("level 1 drop down");
        var level2ToolStripMenuItem = new ToolStripMenuItem("level 2 drop down");
        var level3ToolStripMenuItem = new ToolStripMenuItem("level 3 drop down");

        notifyIcon1.ContextMenuStrip = contextMenuStrip1;
        notifyIcon1.Icon = ((Icon)(resources.GetObject("notifyIcon1.Icon")));
        notifyIcon1.Visible = true;

        level2ToolStripMenuItem.DropDownItems.Add(level3ToolStripMenuItem);
        level1ToolStripMenuItem.DropDownItems.Add(level2ToolStripMenuItem);
        contextMenuStrip1.Items.Add(level1ToolStripMenuItem);
    }
}

Upvotes: 6

Views: 1997

Answers (5)

osexpert
osexpert

Reputation: 563

Not answering the question, but here is how I solved if for ContextMenuStrip:

    private void Form1_MouseClick(object sender, MouseEventArgs e)
    {
        // Get width of widest child item (skip separators!)
        var maxWidth = contextMenuStrip1.Items.OfType<ToolStripMenuItem>().Select(m => m.Width).Max();
        var currentScreen = Screen.FromPoint(e.Location);

        var dir = contextMenuStrip1.DefaultDropDownDirection;
        if (dir == ToolStripDropDownDirection.Right && p.X + maxWidth > currentScreen.Bounds.Right)
        {
            dir = ToolStripDropDownDirection.Left;
        }
        else if (dir == ToolStripDropDownDirection.Left && p.X - maxWidth < currentScreen.Bounds.Left)
        {
            dir = ToolStripDropDownDirection.Right;
        }

        contextMenuStrip1.Show(this, e.Location, dir);
    }

Upvotes: 0

Tom Bogle
Tom Bogle

Reputation: 509

I did not try the solution by tombam. But since the others didn't seem to work, I came up with this simple solution:

        private void MenuDropDownOpening(object sender, EventArgs e)
    {
        var menuItem = sender as ToolStripDropDownButton;
        if (menuItem == null || menuItem.HasDropDownItems == false)
            return; // not a drop down item

        // Current bounds of the current monitor
        var upperRightCornerOfMenuInScreenCoordinates = menuItem.GetCurrentParent().PointToScreen(new Point(menuItem.Bounds.Right, menuItem.Bounds.Top));
        var currentScreen = Screen.FromPoint(upperRightCornerOfMenuInScreenCoordinates);

        // Get width of widest child item (skip separators!)
        var maxWidth = menuItem.DropDownItems.OfType<ToolStripMenuItem>().Select(m => m.Width).Max();

        var farRight = upperRightCornerOfMenuInScreenCoordinates.X + maxWidth;
        var currentMonitorRight = currentScreen.Bounds.Right;

        menuItem.DropDownDirection = farRight > currentMonitorRight ? ToolStripDropDownDirection.Left :
            ToolStripDropDownDirection.Right;
    }

Note that in my world, I was not concerned about multiple levels of cascading menus (as in the OP), so I did not test my solution in that scenario. But this works correctly for a single ToolStripDropDownButton on a ToolStrip.

Upvotes: -2

David
David

Reputation: 1871

It is not easy, but you can write code in the DropDownOpening event to look at where the menu is at (its bounds), the current screen, and then set the DropDownDirection of the ToolStripMenuItem:

private void submenu_DropDownOpening(object sender, EventArgs e)
{
    ToolStripMenuItem menuItem = sender as ToolStripMenuItem;
    if (menuItem.HasDropDownItems == false)
    {
        return; // not a drop down item
    }
    // Current bounds of the current monitor
    Rectangle Bounds = menuItem.GetCurrentParent().Bounds;
    Screen CurrentScreen = Screen.FromPoint(Bounds.Location);
    // Look how big our children are:
    int MaxWidth = 0;
    foreach (ToolStripMenuItem subitem in menuItem.DropDownItems)
    {
        MaxWidth = Math.Max(subitem.Width, MaxWidth);
    }
    MaxWidth += 10; // Add a little wiggle room

    int FarRight = Bounds.Right + MaxWidth;
    int CurrentMonitorRight = CurrentScreen.Bounds.Right;

    if (FarRight > CurrentMonitorRight)
    {
        menuItem.DropDownDirection = ToolStripDropDownDirection.Left;
    }
    else
    {
        menuItem.DropDownDirection = ToolStripDropDownDirection.Right;
    }
}

Also, make sure you have the DropDownOpening event hooked up (you would really need to add this to every menu item):

level1ToolStripMenuItem += submenu_DropDownOpening;

Upvotes: 7

tombam
tombam

Reputation: 167

I have solved it this way:

  1. For the ContextMenuStrip itself to open on a desired screen, I created a ContextMenuStripEx with the following methods:

    protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
    {
        Rectangle dropDownBounds = new Rectangle(x, y, width, height);
    
        dropDownBounds = ConstrainToBounds(Screen.FromPoint(dropDownBounds.Location).Bounds, dropDownBounds);
    
        base.SetBoundsCore(dropDownBounds.X, dropDownBounds.Y, dropDownBounds.Width, dropDownBounds.Height, specified);
    }
    
    internal static Rectangle ConstrainToBounds(Rectangle constrainingBounds, Rectangle bounds)
    {
        if (!constrainingBounds.Contains(bounds))
        {
            bounds.Size = new Size(Math.Min(constrainingBounds.Width - 2, bounds.Width), Math.Min(constrainingBounds.Height - 2, bounds.Height));
            if (bounds.Right > constrainingBounds.Right)
            {
                bounds.X = constrainingBounds.Right - bounds.Width;
            }
            else if (bounds.Left < constrainingBounds.Left)
            {
                bounds.X = constrainingBounds.Left;
            }
            if (bounds.Bottom > constrainingBounds.Bottom)
            {
                bounds.Y = constrainingBounds.Bottom - 1 - bounds.Height;
            }
            else if (bounds.Top < constrainingBounds.Top)
            {
                bounds.Y = constrainingBounds.Top;
            }
        }
        return bounds;
    }
    

(ConstrainToBounds method is taken from the base class ToolStripDropDown via Reflector)

  1. for the nested MenuItems to open on the same screen as ContextMenuStrip, I created a ToolStripMenuItemEx (which derives from ToolStripMenuItem). In my case it looks like this:

    private ToolStripDropDownDirection? originalToolStripDropDownDirection;
    
    protected override void OnDropDownShow(EventArgs e)
    {
        base.OnDropDownShow(e);
    
        if (!Screen.FromControl(this.Owner).Equals(Screen.FromPoint(this.DropDownLocation)))
        {
            if (!originalToolStripDropDownDirection.HasValue)
                originalToolStripDropDownDirection = this.DropDownDirection;
    
            this.DropDownDirection = originalToolStripDropDownDirection.Value == ToolStripDropDownDirection.Left ? ToolStripDropDownDirection.Right : ToolStripDropDownDirection.Left;
        }
    }
    

Upvotes: 0

Phap Duong Dieu
Phap Duong Dieu

Reputation: 165

The code of @David does not fix if the menu is opened in the left side of second screen. I have improved that code to work on all screen corner.

private void subMenu_DropDownOpening(object sender, EventArgs e)
    {
        ToolStripMenuItem mnuItem = sender as ToolStripMenuItem;
        if (mnuItem.HasDropDownItems == false)
        {
            return; // not a drop down item
        }

        //get position of current menu item
        var pos = new Point(mnuItem.GetCurrentParent().Left, mnuItem.GetCurrentParent().Top);

        // Current bounds of the current monitor
        Rectangle bounds = Screen.GetWorkingArea(pos);
        Screen currentScreen = Screen.FromPoint(pos);

        // Find the width of sub-menu
        int maxWidth = 0;
        foreach (var subItem in mnuItem.DropDownItems)
        {
            if (subItem.GetType() == typeof(ToolStripMenuItem))
            {
                var mnu = (ToolStripMenuItem) subItem;
                maxWidth = Math.Max(mnu.Width, maxWidth);
            }
        }
        maxWidth += 10; // Add a little wiggle room


        int farRight = pos.X + mnuMain.Width + maxWidth;
        int farLeft = pos.X - maxWidth;

        //get left and right distance to compare
        int leftGap = farLeft - currentScreen.Bounds.Left;
        int rightGap = currentScreen.Bounds.Right - farRight;


        if (leftGap >= rightGap)
        {
            mnuItem.DropDownDirection = ToolStripDropDownDirection.Left;
        }
        else
        {
            mnuItem.DropDownDirection = ToolStripDropDownDirection.Right;
        }
    }

Upvotes: -1

Related Questions