5

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);
    }
}
atlanteh
  • 5,615
  • 2
  • 33
  • 54
  • Try to add it with form designer and see what code is generated. Maybe you simply miss one assignment. Behavior looks strange, like if "level 3 drop down" can't determine parent (to stick to the same screen as its parent). – Sinatr Oct 27 '14 at 13:04
  • This IS form designer. I just refactored it a little bit for readability. (Converted the fields to locals, and removed unnecessary lines) – atlanteh Oct 27 '14 at 13:32
  • It happens all the time. I played with it for a while and it seems that if the context menu is close enough, like in the picture it, will expand to the second monitor, but if I change the names to "level # tool tip" which is a character less than the original, then somehow it remains on the same screen. – atlanteh Oct 27 '14 at 14:36

4 Answers4

6

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;
Joshua Wade
  • 4,755
  • 2
  • 24
  • 44
David
  • 1,743
  • 1
  • 18
  • 25
  • Thx man! I've long forgotten about this app, but I'll try it now :) – atlanteh Nov 15 '15 at 16:42
  • This code does not seem to take into account that menuItem.GetCurrentParent() -- which gets the tool strip -- has bounds that are relative to its parent window. If that window is not slammed up to the left of the screen, the calculation can be way off, and the menu still opens on the wrong screen. – Tom Bogle Aug 17 '21 at 15:47
0

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;
        }
    }
    
tombam
  • 168
  • 2
  • 5
-1

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;
        }
    }
Phap Duong Dieu
  • 164
  • 2
  • 7
  • This solution does not seem to work in any interesting configuration. – Tom Bogle Aug 17 '21 at 15:54
  • Well, I have been using this for years without any issues – Phap Duong Dieu Aug 18 '21 at 04:46
  • Good. We apparently have scenarios and/or needs which differ in some way. But when I tried to replace the accepted solution with this code in my program (which does not have cascading menus), the behavior went from partial success to complete failure. Perhaps there's yet room for an even better solution that works for everyone. My solution worked for me, was shorter, and avoided the need for "wiggle room" (which I prefer to avoid in code if at all possible). – Tom Bogle Aug 19 '21 at 12:19
-2

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.

Tom Bogle
  • 464
  • 4
  • 18
  • Although it is not required, it's really helpful to others (and maybe even to me) if when you downvote an answer you add a comment as to what the problem is or where/why it failed to work. – Tom Bogle Aug 19 '21 at 12:21