Dynamically altering the image source of a canvas object - c#

I have an animation that is ran on a timer along with some other animations. I would like to be able to change the images source file when an event is triggered but i am currently unable to access element.Source (element equaling the current canvas object which is a image).
public static void Clouds(Canvas canvas, int boundry)
{
var random = new Random();
foreach (FrameworkElement element in canvas.Children)
{
var elementName = Regex.Split(element.Name, "_");
if (elementName[0] == "cloud")
{
if (Canvas.GetLeft(element) < canvas.ActualWidth + element.Width)
{
Canvas.SetLeft(element, Canvas.GetLeft(element) + 1);
} else
{
// Change image source file here.
Canvas.SetTop(element, random.Next(0 - ((int)element.Height / 2), Core.GetPercentage((int)canvas.ActualHeight, boundry)));
Canvas.SetLeft(element, 0 - element.Width);
}
}
}
}
Any help would be great, thanks.

Its because FrameworkElement has no property called Source, you have to cast or just pick the image elements you want.
Somthing like this may work
public static void Clouds(Canvas canvas, int boundry)
{
var random = new Random();
foreach (var image in canvas.Children.OfType<Image>())
{
if (image.Name.Contains("cloud_"))
{
if (Canvas.GetLeft(image) < canvas.ActualWidth + image.Width)
{
Canvas.SetLeft(image, Canvas.GetLeft(image) + 1);
}
else
{
Canvas.SetTop(image, random.Next(0 - ((int)image.Height / 2), Core.GetPercentage((int)canvas.ActualHeight, boundry)));
Canvas.SetLeft(image, 0 - image.Width);
}
}
}
}
now you will be able to access the source property as the var "image" is an Image

Related

Scale winforms controls with window size

I am trying to scale all controls in my program (the ones in the tabcontrol too) when user resizes the window.
I know how to resize the controls, but i wanted my program to have everything scaled, so the ui i easier to read
i am not a good explainer, so here's a reference picture:
I have seen a lot of discussions about resizing the controls, but they are not very helpful.
can i achieve this effect in winforms?
One way to scale winforms controls with window size is to use nested TableLayoutPanel controls and set the rows and columns to use percent rather than absolute sizing.
Then, place your controls in the cells and anchor them on all four sides. For buttons that have a background image set to BackgroundImageLayout = Stretch this is all you need to do. However, controls that use text may require a custom means to scale the font. For example, you're using ComboBox controls where size is a function of the font not the other way around. To compensate for this, these actual screenshots utilize an extension to do a binary search that changes the font size until a target control height is reached.
The basic idea is to respond to TableLayoutPanel size changes by setting a watchdog timer, and when the timer expires iterate the control tree to apply the BinarySearchFontSize extension. You may want to clone the code I used to test this answer and experiment for yourself to see how the pieces fit together.
Something like this would work but you'll probably want to do more testing than I did.
static class Extensions
{
public static float BinarySearchFontSize(this Control control, float containerHeight)
{
float
vertical = BinarySearchVerticalFontSize(control, containerHeight),
horizontal = BinarySearchHorizontalFontSize(control);
return Math.Min(vertical, horizontal);
}
/// <summary>
/// Get a font size that produces control size between 90-100% of available height.
/// </summary>
private static float BinarySearchVerticalFontSize(Control control, float containerHeight)
{
var name = control.Name;
switch (name)
{
case "comboBox1":
Debug.WriteLine($"{control.Name}: CONTAINER HEIGHT {containerHeight}");
break;
}
var proto = new TextBox
{
Text = "|", // Vertical height independent of text length.
Font = control.Font
};
using (var graphics = proto.CreateGraphics())
{
float
targetMin = 0.9F * containerHeight,
targetMax = containerHeight,
min, max;
min = 0; max = proto.Font.Size * 2;
for (int i = 0; i < 10; i++)
{
if(proto.Height < targetMin)
{
// Needs to be bigger
min = proto.Font.Size;
}
else if (proto.Height > targetMax)
{
// Needs to be smaller
max = proto.Font.Size;
}
else
{
break;
}
var newSizeF = (min + max) / 2;
proto.Font = new Font(control.Font.FontFamily, newSizeF);
proto.Invalidate();
}
return proto.Font.Size;
}
}
/// <summary>
/// Get a font size that fits text into available width.
/// </summary>
private static float BinarySearchHorizontalFontSize(Control control)
{
var name = control.Name;
// Fine-tuning
string text;
if (control is ButtonBase)
{
text = "SETTINGS"; // representative max staing
}
else
{
text = string.IsNullOrWhiteSpace(control.Text) ? "LOCAL FOLDERS" : control.Text;
}
var protoFont = control.Font;
using(var g = control.CreateGraphics())
{
using (var graphics = control.CreateGraphics())
{
int width =
(control is ComboBox) ?
control.Width - SystemInformation.VerticalScrollBarWidth :
control.Width;
float
targetMin = 0.9F * width,
targetMax = width,
min, max;
min = 0; max = protoFont.Size * 2;
for (int i = 0; i < 10; i++)
{
var sizeF = g.MeasureString(text, protoFont);
if (sizeF.Width < targetMin)
{
// Needs to be bigger
min = protoFont.Size;
}
else if (sizeF.Width > targetMax)
{
// Needs to be smaller
max = protoFont.Size;
}
else
{
break;
}
var newSizeF = (min + max) / 2;
protoFont = new Font(control.Font.FontFamily, newSizeF);
}
}
}
return protoFont.Size;
}
}
Edit
The essence of my answer is use nested TableLayoutPanel controls and I didn't want to take away from that so I had given a reference link to browse the full code. Since TableLayoutPanel is not a comprehensive solution without being able to scale the height of ComboBox and other Fonts (which isn't all that trivial for the example shown in the original post) my answer also showed one way to achieve that. In response to the comment below (I do want to provide "enough" info!) here is an appendix showing the MainForm code that calls the extension.
Example:
In the method that loads the main form:
Iterate the control tree to find all the TableLayoutPanels.
Attach event handler for the SizeChanged event.
Handle the event by restarting a watchdog timer.
When the timer expires, _iterate the control tree of each TableLayoutPanel to apply the onAnyCellPaint method to obtain the cell metrics and call the extension.
By taking this approach, cells and controls can freely be added and/or removed without having to change the scaling engine.
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if(!DesignMode)
{
comboBox1.SelectedIndex = 0;
IterateControlTree(this, (control) =>
{
if (control is TableLayoutPanel tableLayoutPanel)
{
tableLayoutPanel.SizeChanged += (sender, e) => _wdtSizeChanged.StartOrRestart();
}
});
_wdtSizeChanged.PropertyChanged += (sender, e) =>
{
if (e.PropertyName!.Equals(nameof(WDT.Busy)) && !_wdtSizeChanged.Busy)
{
IterateControlTree(this, (control) =>
{
if (control is TableLayoutPanel tableLayoutPanel)
{
IterateControlTree(tableLayoutPanel, (child) => onAnyCellPaint(tableLayoutPanel, child));
}
});
}
};
}
// Induce a size change to initialize the font resizer.
BeginInvoke(()=> Size = new Size(Width + 1, Height));
BeginInvoke(()=> Size = new Size(Width - 1, Height));
}
// Browse full code sample to see WDT class
WDT _wdtSizeChanged = new WDT { Interval = TimeSpan.FromMilliseconds(100) };
SemaphoreSlim _sslimResizing= new SemaphoreSlim(1);
private void onAnyCellPaint(TableLayoutPanel tableLayoutPanel, Control control)
{
if (!DesignMode)
{
if (_sslimResizing.Wait(0))
{
try
{
var totalVerticalSpace =
control.Margin.Top + control.Margin.Bottom +
// I'm surprised that the Margin property
// makes a difference here but it does!
tableLayoutPanel.Margin.Top + tableLayoutPanel.Margin.Bottom +
tableLayoutPanel.Padding.Top + tableLayoutPanel.Padding.Bottom;
var pos = tableLayoutPanel.GetPositionFromControl(control);
int height;
float optimal;
if (control is ComboBox comboBox)
{
height = tableLayoutPanel.GetRowHeights()[pos.Row] - totalVerticalSpace;
comboBox.DrawMode = DrawMode.OwnerDrawFixed;
optimal = comboBox.BinarySearchFontSize(height);
comboBox.Font = new Font(comboBox.Font.FontFamily, optimal);
comboBox.ItemHeight = height;
}
else if((control is TextBoxBase) || (control is ButtonBase))
{
height = tableLayoutPanel.GetRowHeights()[pos.Row] - totalVerticalSpace;
optimal = control.BinarySearchFontSize(height);
control.Font = new Font(control.Font.FontFamily, optimal);
}
}
finally
{
_sslimResizing.Release();
}
}
}
}
internal void IterateControlTree(Control control, Action<Control> fx)
{
if (control == null)
{
control = this;
}
fx(control);
foreach (Control child in control.Controls)
{
IterateControlTree(child, fx);
}
}
}

Measuring the width of a control that is not actually part of the UI layout in UWP

I am trying to create a UWP control that is a combobox but whose width stays constant no matter which item is selected. (The default ComboBox will resize itself every time a new item is picked, which may cause the layout of the rest of the page to change.)
I am basing my control off of one that I wrote for WPF, which works fine. This is the code for the UWP version:
using Windows.UI.Xaml.Controls;
using Windows.Foundation;
using System.Reflection;
namespace Foo
{
public sealed class ConstantWidthComboBox : ComboBox
{
private readonly double _emptyWidth;
public ConstantWidthComboBox()
{
this.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
_emptyWidth = this.DesiredSize.Width;
}
protected override void OnItemsChanged(object e)
{
base.OnItemsChanged(e);
AdjustWidth();
}
private void AdjustWidth()
{
double maxItemWidth = 0;
foreach (var item in Items)
{
if (item is ComboBoxItem cmbItem)
{
cmbItem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (cmbItem.DesiredSize.Width > maxItemWidth)
maxItemWidth = cmbItem.DesiredSize.Width;
}
else
{
object content = item;
if (!string.IsNullOrEmpty(this.DisplayMemberPath))
{
content = EvaluateDisplayMemberPath(item, this.DisplayMemberPath);
}
ContentPresenter presenter = new ContentPresenter() { Content = content };
presenter.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (presenter.DesiredSize.Width > maxItemWidth)
maxItemWidth = presenter.DesiredSize.Width;
}
}
this.Width = maxItemWidth + _emptyWidth + this.Padding.Left + this.Padding.Right;
}
private object EvaluateDisplayMemberPath(object item, string path)
{
if (item == null)
return null;
if (string.IsNullOrEmpty(path))
return item;
int k = path.IndexOf(".");
string property = (k == -1) ? path : path.Substring(0, k);
string trailer = (k == -1) ? null : path.Substring(k + 1);
object value = item.GetType().GetProperty(property, BindingFlags.Public | BindingFlags.Instance).GetValue(item, null);
return EvaluateDisplayMemberPath(value, trailer);
}
}
}
(Note that this works if the combobox consists of ComboBoxItems or if the DisplayMemberPath is used to get the text to display. I haven't tested it on wider circumstances such as if the items are data templates or whatnot.)
While this works in WPF, in UWP the presenter's DesiredSize.Width value is always zero.
Does anyone know how to measure the (hypothetical) width of a control that is not actually part of the UI layout in UWP, such as for the ContentPresenter in the above code?
Please find the ContentPresenter control through Visual Tree, then access the ActualWidth property.
You can follow the first answer of this question edited by Martin Zikmund to do this like following.
private void Page_Loaded(object sender, RoutedEventArgs e)
{
var contentPresenter = FindChild<ContentPresenter>(MyConstantWidthComboBox);
double k = contentPresenter.ActualWidth;
}
public T FindChild<T>(DependencyObject parent)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T typedChild)
{
return typedChild;
}
var inner = FindChild<T>(child);
if (inner != null)
{
return inner;
}
}
return default;
}

Why I don't see any more the TextField inside OnGUI in EditorWindow script?

using UnityEngine;
using UnityEditor;
public class SearchableWindow : EditorWindow
{
string searchString = "";
[MenuItem("Tools/Searching")]
private static void Searching()
{
const int width = 340;
const int height = 420;
var x = (Screen.currentResolution.width - width) / 2;
var y = (Screen.currentResolution.height - height) / 2;
GetWindow<SearchableWindow>().position = new Rect(x, y, width, height);
}
void OnGUI()
{
GUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.FlexibleSpace();
searchString = GUILayout.TextField(searchString, EditorStyles.toolbarTextField);
GUILayout.EndHorizontal();
var items = Selection.gameObjects;
// Do comparison here. For example
for (int i = 0; i < items.Length; i++)
{
if (items[i].name.Contains(searchString))
{
GUILayout.Label(items[i].name);
}
}
}
}
Before it was working fine but now everything is very slow and also I don't see the TextField and when selecting a GameObject in the Hierarchy it's taking almost 5 seconds to show it in the Editor Window.
And before it was all fast and showing the whole Hierarchy in the Editor Window.
Now it's empty:
I took the answer from this question:
Searchable
OnGUI is only called while the mouse is (moved/clicked) over the according Window -> it is not taking almost 5 seconds but until you move your mouse over the window again.
In order to solve that you could implement EditorWindow.OnSelectionChange and force a EditorWindow.Repaint so your window is refreshed everytime the Selection changes.
private void OnSelectionChange()
{
Repaint();
}
The second issue is produced as follows:
GUILayout.TextField together with GUILayout.FlexibleSpace if not defined different uses automatically the width of the inserted text -> Since you have an empty text at start the width is almost 0 ... you actually can see your TextField very thin there and it gets bigger as you fill it:
Using the drawers from the EditorGUILayout instead solves this:
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.FlexibleSpace();
searchString = EditorGUILayout.TextField(searchString, EditorStyles.toolbarTextField);
EditorGUILayout.EndHorizontal();
var items = Selection.gameObjects;
// Do comparison here. For example
foreach (var selectedObject in selected)
{
if (selectedObject .name.Contains(searchString))
{
EditorGUILayout.LabelField(selectedObject.name);
}
}
Tip If you replace
EditorGUILayout.LabelField(items[i].name);
with
if (GUILayout.Button(selectedObject.name, EditorStyles.label))
{
EditorGUIUtility.PingObject(selectedObject);
}
you can click on the name and "ping" the according GameObject in the hierachy!
Update since asked in the comments
If you want to include all children of the selection recursive you could do something like (there might be better ways but that's what I came up with within 10 minutes)
private static IEnumerable<GameObject> GetChildrenRecursive(GameObject root)
{
var output = new List<GameObject>();
//add the root object itself
output.Add(root);
// iterate over direct children
foreach (Transform child in root.transform)
{
// add the child itslef
output.Add(child.gameObject);
// Recursion here: Get all subchilds of this child
var childsOfchild = GetChildrenRecursive(child.gameObject);
output.AddRange(childsOfchild);
}
return output;
}
private static IEnumerable<GameObject> GetChildrenRecursive(IEnumerable<GameObject> rootObjects)
{
var output = new List<GameObject>();
foreach (var root in rootObjects)
{
output.AddRange(GetChildrenRecursive(root));
}
// remove duplicates
return output.Distinct().ToList();
}
and using
var selected = GetChildrenRecursive(Selection.gameObjects);
Another Update
Since this might not be very efficient you probably should move it to Searching and than only refresh the selected value also in OnSelectionChange like
public class SearchableWindow : EditorWindow
{
private string searchString = "";
private List<GameObject> selected;
[MenuItem("Tools/Searching")]
private static void Searching()
{
const int width = 340;
const int height = 420;
var x = (Screen.currentResolution.width - width) / 2;
var y = (Screen.currentResolution.height - height) / 2;
var window = GetWindow<SearchableWindow>();
window.position = new Rect(x, y, width, height);
window.selected = GetChildrenRecursive(Selection.gameObjects).ToList();
}
private void OnSelectionChange()
{
selected = GetChildrenRecursive(Selection.gameObjects).ToList();
Repaint();
}
private void OnGUI()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.FlexibleSpace();
searchString = EditorGUILayout.TextField(searchString, EditorStyles.toolbarTextField);
EditorGUILayout.EndHorizontal();
// only as fallback
if (selected == null)
{
selected = GetChildrenRecursive(Selection.gameObjects).ToList();
}
// Do comparison here. For example
foreach (var selectedObject in selected)
{
if (selectedObject.name.Contains(searchString))
{
if (GUILayout.Button(selectedObject.name, EditorStyles.label))
{
EditorGUIUtility.PingObject(selectedObject);
}
}
}
}
private static IEnumerable<GameObject> GetChildrenRecursive(GameObject root)
{
var output = new List<GameObject>();
//add the root object itself
output.Add(root);
// iterate over direct children
foreach (Transform child in root.transform)
{
// add the children themselves
output.Add(child.gameObject);
var childsOfchild = GetChildrenRecursive(child.gameObject);
output.AddRange(childsOfchild);
}
return output;
}
private static IEnumerable<GameObject> GetChildrenRecursive(IEnumerable<GameObject> rootObjects)
{
var output = new List<GameObject>();
foreach (var root in rootObjects)
{
output.AddRange(GetChildrenRecursive(root));
}
// remove any duplicates that would e.g. appear if you select a parent and its child
return output.Distinct();
}
}

Creating ImageList object in a loop

I have very simple logic for list view items creating. I'm storing all the col headers name from dataGridView into ColNamesForm1 array and then only comparing if last 5 characters match with (num) or (cat) string. For these two options I'm appending different picture into listview lvMoveFrom using of Populate function stored in static classs OtherFunctions.
But something is wrong in my code because after last iteration it is appending images from the last column - if the first column is (num) and the last column is (cat) the images in listview are all the same - from cat image.
How can I fix this issue? I was wondering about creating new ImageList object for every columns but I have no idea, how can I do that dynamically, e.g. using of loop index i.
Please, can you help to solve my problem.
private void Form1_Load(object sender, EventArgs e)
{
for (int i = 0; i < ColNamesForm1.Length; i++)
{
if (ColNamesForm1[i].ToString().Substring(0, 4).ToUpper() != "COL_" && Regex.IsMatch(ColNamesForm1[i].ToString().Substring(4, 1), #"^\d+$") == false)
{
if (ColNamesForm1[i].ToString().Substring(ColNamesForm1[i].ToString().Length - 5) == "(num)")
{
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), #"C:\pictures\num.png");
}
else
{
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), #"C:\pictures\cat.png");
}
}
}
}
public static void Populate(ListView lv, string itemName, string pathToPicture)
{
ImageList img = new ImageList();
img.ImageSize = new Size(30, 30);
img.Images.Add(Image.FromFile(pathToPicture));
lv.SmallImageList = img;
lv.Items.Add(itemName, 0);
}
The Problem
So basically this:
ImageList img = new ImageList();
img.ImageSize = new Size(30, 30);
img.Images.Add(Image.FromFile(pathToPicture));
lv.SmallImageList = img;
lv.Items.Add(itemName, 0);
Was adding a new image list to the ListView. The same ListView you passed it everytime (so you were effectively overwriting it). Secondly, the line:
lv.Items.Add(itemName, 0);
The second argument is the index in the image list (that you assigned to the ListView). So giving it 0 will ask the ListView to pick the image from lv.SmallImageList[0] (psuedo code) basically.
The Solution
To remove the overwriting I pulled the image setup logic out of the Populate and put it back in the main method. I'll break down just the setup logic:
ImageList img = new ImageList();
img.ImageSize = new Size(30, 30);
var paths = new List<string> { #"C:\pictures\num.png", #"C:\pictures\cat.png" };
paths.ForEach(path => img.Images.Add(MediaTypeNames.Image.FromFile(path)));
lvMoveFrom.SmallImageList = img;
I put all the image paths into a List<string> and just used the LINQ ForEach to iterate over each one adding it to the ImageList img. There is no difference from your original code, apart from I add all the images to the ListView and I only do it once.
So then to make your code easier to understand I did some simple refactorings.
First was invert if statement:
if (ColNamesForm1[i].ToString().Substring(0, 4).ToUpper() != "COL_" && Regex.IsMatch(ColNamesForm1[i].ToString().Substring(4, 1), #"^\d+$") == false)
To:
if (ColNamesForm1[i].ToString().Substring(0, 4).ToUpper() == "COL_"
|| Regex.IsMatch(ColNamesForm1[i].ToString().Substring(4, 1), #"^\d+$"))
{
continue;
}
This is almost like a guard clause, it says if we don't meet these minimum conditions then move to the next item.
Then I simplified your method execution by reducing duplication:
if (ColNamesForm1[i].ToString().Substring(ColNamesForm1[i].ToString().Length - 5) == "(num)")
{
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), #"C:\pictures\num.png");
}
else
{
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), #"C:\pictures\cat.png");
}
To:
var image = ColNamesForm1[i].ToString().EndsWith("(num)")
? 0 // corresponds with the position of the image in the ImageList
: 1;
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), image);
Finally you will see that I change your Populate method. Firstly we are prepopulating your ListView with the images, then using that ternary operator to choose which index of the image to show.
The code all together is:
private void Form1_Load(object sender, EventArgs e)
{
ImageList img = new ImageList();
img.ImageSize = new Size(30, 30);
var paths = new List<string> { #"C:\pictures\num.png", #"C:\pictures\cat.png" };
paths.ForEach(path => img.Images.Add(Image.FromFile(path)));
lvMoveFrom.SmallImageList = img;
for (int i = 0; i < ColNamesForm1.Length; i++)
{
if (ColNamesForm1[i].ToString().Substring(0, 4).ToUpper() == "COL_"
|| Regex.IsMatch(ColNamesForm1[i].ToString().Substring(4, 1), #"^\d+$"))
{
continue;
}
var image = ColNamesForm1[i].ToString().EndsWith("(num)")
? 0 // corresponds with the position of the image in the ImageList
: 1;
OtherFunctions.Populate(lvMoveFrom, ColNamesForm1[i].ToString(), image);
}
}
public static void Populate(ListView lv, string itemName, int imageIndex)
{
lv.Items.Add(itemName, imageIndex);
}
Now you can simplify further:
private void Form1_Load(object sender, EventArgs e)
{
ImageList img = new ImageList();
img.ImageSize = new Size(30, 30);
var paths = new List<string> { #"C:\pictures\num.png", #"C:\pictures\cat.png" };
paths.ForEach(path => img.Images.Add(Image.FromFile(path)));
lvMoveFrom.SmallImageList = img;
for (int i = 0; i < ColNamesForm1.Length; i++)
{
if (ColNamesForm1[i].ToString().Substring(0, 4).ToUpper() == "COL_"
|| Regex.IsMatch(ColNamesForm1[i].ToString().Substring(4, 1), #"^\d+$"))
{
continue;
}
var image = ColNamesForm1[i].ToString().EndsWith("(num)")
? 0 // corresponds with the position of the image in the ImageList
: 1;
lvMoveFrom.Items.Add(ColNamesForm1[i].ToString(), image);
}
}

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:
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);
}
}
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;
I have solved it this way:
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)
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;
}
}
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;
}
}
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.

Categories