Prevent ToolStripMenuItems from jumping to second screen - c#

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.

Related

How to draw a semi-transparent rectangle on Panel containing some user controls in it?

I want to draw a semi-transparent rectangle on panel containing some user controls in it.
My main goal is to create selection rectangle similar to what the windows file explorer does.
And tried with Control.Paint Event but the rectangle shows beneath the user control. I want that semi-transparent rectangle to show on top of all controls present in the panel.
Here is what I tried:
class test
{
public FlowLayoutPanel flp;
public test()
{
flp = new FlowLayoutPanel();
flp.Controls.Add(c);//Here c is User Control
flp.Paint += (s, pe) =>
{
Rectangle f = new Rectangle();
f.Size = new Size(100, 100);
Brush b = new System.Drawing.SolidBrush(Color.FromArgb(25, Color.Red));
pe.Graphics.FillRectangle(b, f);
b.Dispose();
};
}
}
Here Rectangle is visible under the user control.
Here is an example of using a 2nd Form to overlay the selection.
I am using a TabPage tabPage5 as my container, you should be able to adapt to any other Control or even the whole Form..
I use two class level variables and prepare the overlay Form somwhere at the start..:
Form overlay = new Form();
Point m_Down = Point.Empty;
void prepareOverlay()
{
overlay.BackColor = Color.Fuchsia; // your selection color
overlay.Opacity = 0.2f; // tranparency
overlay.MinimizeBox = false; // prepare..
overlay.MaximizeBox = false;
overlay.Text = "";
overlay.ShowIcon = false;
overlay.ControlBox = false;
overlay.FormBorderStyle = FormBorderStyle.None;
overlay.Size = Size.Empty;
overlay.TopMost = true;
}
These events are needed for the parent container the user will drag over:
private void tabPage5_MouseDown(object sender, MouseEventArgs e)
{
m_Down = e.Location;
overlay.Size = Size.Empty;
overlay.Show();
}
private void tabPage5_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
overlay.Location = tabPage5.PointToScreen(m_Down);
overlay.Size = new Size(e.X - m_Down.X, e.Y - m_Down.Y);
}
}
private void tabPage5_MouseUp(object sender, MouseEventArgs e)
{
// do your stuff and then hide overlay..
overlay.Hide();
}
I have only coded for the standard case of a user dragging from the top left to the bottom right.
For the other cases you will need to do a little juggling with Math.Abs& Math.Min, calculating the Size in absolutes and using the coordinate minima for the Location..
To collect all covered Controls you will have to revert the overlay.Bounds to the client coordinates using selectionRect = tabPage5.RectangleToClient (overlay.Bounds); To collect nested Controls the selection could use a method like this one:
List<Control> controlSelection = new List<Control>();
List<Control> getControls(Control container, Rectangle rect)
{
controlSelection = new List<Control>();
foreach (Control ctl in container.Controls)
if (rect.Contains(ctl.Bounds))
{
controlSelection.Add(ctl);
foreach (Control ct in ctl.Controls) controlSelection.Add(ct); ;
}
return controlSelection;
}
To call it use:
Rectangle grabRect = tabPage5.RectangleToClient (overlay.Bounds);
controlSelection = getControls(tabPage5, grabRect);
Console.WriteLine(controlSelection.Count + " Controls grabbed.");
For multiple levels of nesting you will need a recursive function! For this case it may be better to leave the selectionRectangle is screen coordinates and to transform the tested controls' bound to screen as well..
And, of course it is up to you to decide if a selection must just hit (Rectangle.IntersectsWith()) or cover the targets completely; I have coded the latter (Rectangle.Contains()).
Child controls are always drawn after the parent control is drawn, so they will always show on top of anything you paint on the parent.
You will need to create a semi transparent child control and make sure the z-index of that child control is higher than the other controls.
Edit: This solution is not possible in WinForms. Or at least not with the standard controls.
In frameworks where this is possible, the control on top will likely block mouse and touch events from coming through.
I took the awesome code, from the answer above and been tweaking it. Made it so you can move the mouse in any direction and it'll draw & size the form accordingly. I confine the overlay, to the inside of the parent form. This is still a work, in progress.
public partial class Form1 : Form
{
Form overlay;
bool a, b;
int c, d, f, g;
Point ptNope, ptHold, ptTest, ptDown = Point.Empty;
List<Control> controlSelection = new List<Control>();
List<Control> getControls(Control container, Rectangle rect)
{
rect = RectangleToClient(rect);
controlSelection = new List<Control>();
foreach (Control ctl in container.Controls)
if (rect.Contains(ctl.Bounds))
{
controlSelection.Add(ctl);
foreach (Control ct in ctl.Controls) controlSelection.Add(ct);
}
return controlSelection;
}
public Form1()
{
InitializeComponent();
overlay = new Form()
{
FormBorderStyle = FormBorderStyle.None,
BackColor = Color.Fuchsia,
Opacity = 0.2f,
TopMost = true
};
}
private void Form1_MouseDown(object sender, MouseEventArgs e)
{
ptNope.X = this.Left; ptNope.Y = this.Top;
ptDown = e.Location;
overlay.Show();
}
public static int vise(int value, int min, int max) { return (value < min) ? min : (value > max) ? max : value; }
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
ptHold = PointToScreen(ptDown);
ptTest = PointToScreen(e.Location);
a = (ptTest.X < ptHold.X); b = (ptTest.Y < ptHold.Y);
c = a ? ptTest.X : ptHold.X; c = vise(c, ptNope.X, ptNope.X + this.Width);
d = b ? ptTest.Y : ptHold.Y; d = vise(d, ptNope.Y, ptNope.Y + this.Height);
f = a ? (ptHold.X - ptTest.X) : (ptTest.X - ptHold.X); f = vise(f, 0, a ? (ptHold.X - ptNope.X) : ((ptNope.X + this.Width) - ptHold.X));
g = b ? (ptHold.Y - ptTest.Y) : (ptTest.Y - ptHold.Y); g = vise(g, 0, b ? (ptHold.Y - ptNope.Y) : ((ptNope.Y + this.Height) - ptHold.Y));
overlay.Left = c; overlay.Top = d; overlay.Width = f; overlay.Height = g;
controlSelection = getControls(this, Bounds);
foreach (Control c in controlSelection) { c.BackColor = SystemColors.Control; }
controlSelection = getControls(this, overlay.Bounds);
foreach (Control c in controlSelection) { c.BackColor = Color.Fuchsia; }
label1.Text = controlSelection.Count + " Control(s) grabbed";
}
}
private void Form1_MouseUp(object sender, MouseEventArgs e)
{
overlay.Hide(); label1.Text = "";
}
}
Thoughts and ideas, for improvement are welcome.

What is the best way to create custom item scrollable list view in c# .NET?

I want to create GUI application using .NET and I need to implement such kind of scrollable list with custom items. All items should grouped to 3 different groups and to include images, text and favourite star button as shown here:
I would also like to implement filtering using search box, but once I would be able to create such list with customizable items then I hope it would be much easier.
I tried using .NET list view, but I want each cell to look like 1 cell - with out any borders between the image, text, image.
I would extremely appreciate any thoughts about this manner!
The best way would be making control template for either WPF ListView or ListBox control.
For example, ListBox.ItemTemplate allows for custom item look.
If you would like to use 3rd party components, Better ListView allows this using owner drawing (requires subclassing BetterListView):
Here is a setup code for the control:
this.customListView = new CustomListView
{
Dock = DockStyle.Fill,
Parent = this
};
this.customListView.BeginUpdate();
for (int i = 0; i < 6; i++)
{
var item = new BetterListViewItem
{
Image = imageGraph,
Text = String.Format("Item no. {0}", i)
};
this.customListView.Items.Add(item);
}
var group1 = new BetterListViewGroup("First group");
var group2 = new BetterListViewGroup("Second group");
var group3 = new BetterListViewGroup("Third group");
this.customListView.Groups.AddRange(new[] { group1, group2, group3 });
this.customListView.Items[0].Group = group1;
this.customListView.Items[1].Group = group1;
this.customListView.Items[2].Group = group2;
this.customListView.Items[3].Group = group2;
this.customListView.Items[4].Group = group2;
this.customListView.Items[5].Group = group3;
this.customListView.AutoSizeItemsInDetailsView = true;
this.customListView.GroupHeaderBehavior = BetterListViewGroupHeaderBehavior.None;
this.customListView.ShowGroups = true;
this.customListView.LayoutItemsCurrent.ElementOuterPadding = new Size(0, 8);
this.customListView.EndUpdate();
and CustomListView class (implements custom drawing and interaction with star icons):
using BetterListView;
internal sealed class CustomListView : BetterListView
{
private const int IndexUndefined = -1;
private Image imageStarNormal;
private Image imageStarHighlight;
private int lastStarIndex = IndexUndefined;
private int currentStarIndex = IndexUndefined;
public CustomListView()
{
this.imageStarNormal = Image.FromFile("icon-star-normal.png");
this.imageStarHighlight = Image.FromFile("icon-star-highlight.png");
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
var item = HitTest(e.Location).ItemDisplay;
if (item == null)
{
return;
}
var bounds = GetItemBounds(item);
if (bounds == null)
{
return;
}
Rectangle boundsStar = GetStarBounds(bounds);
UpdateStarIndex(boundsStar.Contains(e.Location)
? item.Index
: IndexUndefined);
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
UpdateStarIndex(IndexUndefined);
}
protected override void OnDrawGroup(BetterListViewDrawGroupEventArgs eventArgs)
{
eventArgs.DrawSeparator = false;
base.OnDrawGroup(eventArgs);
}
protected override void OnDrawItem(BetterListViewDrawItemEventArgs eventArgs)
{
base.OnDrawItem(eventArgs);
Graphics g = eventArgs.Graphics;
BetterListViewItemBounds bounds = eventArgs.ItemBounds;
int imageWidth = this.imageStarNormal.Width;
int imageHeight = this.imageStarNormal.Height;
g.DrawImage(
(this.currentStarIndex == eventArgs.Item.Index) ? this.imageStarHighlight : this.imageStarNormal,
GetStarBounds(bounds),
0, 0, imageWidth, imageHeight,
GraphicsUnit.Pixel);
Rectangle boundsSelection = bounds.BoundsSelection;
g.DrawRectangle(
Pens.Gray,
new Rectangle(boundsSelection.Left, boundsSelection.Top, boundsSelection.Width - 1, boundsSelection.Height - 1));
}
private void UpdateStarIndex(int starIndex)
{
if (starIndex == this.lastStarIndex)
{
return;
}
bool isUpdated = false;
if (this.lastStarIndex != IndexUndefined)
{
Items[this.lastStarIndex].Invalidate();
isUpdated = true;
}
if (starIndex != IndexUndefined)
{
Items[starIndex].Invalidate();
isUpdated = true;
}
this.lastStarIndex = this.currentStarIndex;
this.currentStarIndex = starIndex;
if (isUpdated)
{
RedrawItems();
}
}
private Rectangle GetStarBounds(BetterListViewItemBounds bounds)
{
Rectangle rectInner = bounds.BoundsInner;
int widthImage = this.imageStarNormal.Width;
int heightImage = this.imageStarNormal.Height;
return (new Rectangle(
rectInner.Width - widthImage,
rectInner.Top + ((rectInner.Height - heightImage) >> 1),
widthImage,
heightImage));
}
}

Handle scrolling of a WinForms control manually

I have a control (System.Windows.Forms.ScrollableControl) which can potentially be very large. It has custom OnPaint logic. For that reason, I am using the workaround described here.
public class CustomControl : ScrollableControl
{
public CustomControl()
{
this.AutoScrollMinSize = new Size(100000, 500);
this.DoubleBuffered = true;
}
protected override void OnScroll(ScrollEventArgs se)
{
base.OnScroll(se);
this.Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var graphics = e.Graphics;
graphics.Clear(this.BackColor);
...
}
}
The painting code mainly draws "normal" things that move when you scroll. The origin of each shape that is drawn is offsetted by this.AutoScrollPosition.
graphics.DrawRectangle(pen, 100 + this.AutoScrollPosition.X, ...);
However, the control also contains "static" elements, which are always drawn at the same position relative to the parent control. For that, I just don't use AutoScrollPosition and draw the shapes directly:
graphics.DrawRectangle(pen, 100, ...);
When the user scrolls, Windows translates the entire visible area in the direction opposite to the scrolling. Usually this makes sense, because then the scrolling seems smooth and responsive (and only the new part has to be redrawn), however the static parts are also affected by this translation (hence the this.Invalidate() in OnScroll). Until the next OnPaint call has successfully redrawn the surface, the static parts are slightly off. This causes a very noticable "shaking" effect when scrolling.
Is there a way I can create a scrollable custom control that does not have this problem with static parts?
You could do this by taking full control of scrolling. At the moment, you're just hooking in to the event to do your logic. I've faced issues with scrolling before, and the only way I've ever managed to get everything to work smoothly is by actually handling the Windows messages by overriding WndProc. For instance, I have this code to synchronize scrolling between several ListBoxes:
protected override void WndProc(ref Message m) {
base.WndProc(ref m);
// 0x115 and 0x20a both tell the control to scroll. If either one comes
// through, you can handle the scrolling before any repaints take place
if (m.Msg == 0x115 || m.Msg == 0x20a)
{
//Do you scroll processing
}
}
Using WndProc will get you the scroll messages before anything gets repainted at all, so you can appropriately handle the static objects. I'd use this to suspend scrolling until an OnPaint occurs. It won't look as smooth, but you won't have issues with the static objects moving.
Since I really needed this, I ended up writing a Control specifically for the case when you have static graphics on a scrollable surface (whose size can be greater than 65535).
It is a regular Control with two ScrollBar controls on it, and a user-assignable Control as its Content. When the user scrolls, the container sets its Content's AutoScrollOffset accordingly. Therefore, it is possible to use controls which use the AutoScrollOffset method for drawing without changing anything. The Content's actual size is exactly the visible part of it at all times. It allows horizontal scrolling by holding down the shift key.
Usage:
var container = new ManuallyScrollableContainer();
var content = new ExampleContent();
container.Content = content;
container.TotalContentWidth = 150000;
container.TotalContentHeight = 5000;
container.Dock = DockStyle.Fill;
this.Controls.Add(container); // e.g. add to Form
Code:
It became a bit lengthy, but I could avoid ugly hacks. Should work with mono. I think it turned out pretty sane.
public class ManuallyScrollableContainer : Control
{
public ManuallyScrollableContainer()
{
InitializeControls();
}
private class UpdatingHScrollBar : HScrollBar
{
protected override void OnValueChanged(EventArgs e)
{
base.OnValueChanged(e);
// setting the scroll position programmatically shall raise Scroll
this.OnScroll(new ScrollEventArgs(ScrollEventType.EndScroll, this.Value));
}
}
private class UpdatingVScrollBar : VScrollBar
{
protected override void OnValueChanged(EventArgs e)
{
base.OnValueChanged(e);
// setting the scroll position programmatically shall raise Scroll
this.OnScroll(new ScrollEventArgs(ScrollEventType.EndScroll, this.Value));
}
}
private ScrollBar shScrollBar;
private ScrollBar svScrollBar;
public ScrollBar HScrollBar
{
get { return this.shScrollBar; }
}
public ScrollBar VScrollBar
{
get { return this.svScrollBar; }
}
private void InitializeControls()
{
this.Width = 300;
this.Height = 300;
this.shScrollBar = new UpdatingHScrollBar();
this.shScrollBar.Top = this.Height - this.shScrollBar.Height;
this.shScrollBar.Left = 0;
this.shScrollBar.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
this.svScrollBar = new UpdatingVScrollBar();
this.svScrollBar.Top = 0;
this.svScrollBar.Left = this.Width - this.svScrollBar.Width;
this.svScrollBar.Anchor = AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom;
this.shScrollBar.Width = this.Width - this.svScrollBar.Width;
this.svScrollBar.Height = this.Height - this.shScrollBar.Height;
this.Controls.Add(this.shScrollBar);
this.Controls.Add(this.svScrollBar);
this.shScrollBar.Scroll += this.HandleScrollBarScroll;
this.svScrollBar.Scroll += this.HandleScrollBarScroll;
}
private Control _content;
/// <summary>
/// Specifies the control that should be displayed in this container.
/// </summary>
public Control Content
{
get { return this._content; }
set
{
if (_content != value)
{
RemoveContent();
this._content = value;
AddContent();
}
}
}
private void AddContent()
{
if (this.Content != null)
{
this.Content.Left = 0;
this.Content.Top = 0;
this.Content.Width = this.Width - this.svScrollBar.Width;
this.Content.Height = this.Height - this.shScrollBar.Height;
this.Content.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right;
this.Controls.Add(this.Content);
CalculateMinMax();
}
}
private void RemoveContent()
{
if (this.Content != null)
{
this.Controls.Remove(this.Content);
}
}
protected override void OnParentChanged(EventArgs e)
{
// mouse wheel events only arrive at the parent control
if (this.Parent != null)
{
this.Parent.MouseWheel -= this.HandleMouseWheel;
}
base.OnParentChanged(e);
if (this.Parent != null)
{
this.Parent.MouseWheel += this.HandleMouseWheel;
}
}
private void HandleMouseWheel(object sender, MouseEventArgs e)
{
this.HandleMouseWheel(e);
}
/// <summary>
/// Specifies how the control reacts to mouse wheel events.
/// Can be overridden to adjust the scroll speed with the mouse wheel.
/// </summary>
protected virtual void HandleMouseWheel(MouseEventArgs e)
{
// The scroll difference is calculated so that with the default system setting
// of 3 lines per scroll incremenet,
// one scroll will offset the scroll bar value by LargeChange / 4
// i.e. a quarter of the thumb size
ScrollBar scrollBar;
if ((Control.ModifierKeys & Keys.Shift) != 0)
{
scrollBar = this.HScrollBar;
}
else
{
scrollBar = this.VScrollBar;
}
var minimum = 0;
var maximum = scrollBar.Maximum - scrollBar.LargeChange;
if (maximum <= 0)
{
// happens when the entire area is visible
return;
}
var value = scrollBar.Value - (int)(e.Delta * scrollBar.LargeChange / (120.0 * 12.0 / SystemInformation.MouseWheelScrollLines));
scrollBar.Value = Math.Min(Math.Max(value, minimum), maximum);
}
public event ScrollEventHandler Scroll;
protected virtual void OnScroll(ScrollEventArgs e)
{
var handler = this.Scroll;
if (handler != null)
{
handler(this, e);
}
}
/// <summary>
/// Event handler for the Scroll event of either scroll bar.
/// </summary>
private void HandleScrollBarScroll(object sender, ScrollEventArgs e)
{
OnScroll(e);
if (this.Content != null)
{
this.Content.AutoScrollOffset = new System.Drawing.Point(-this.HScrollBar.Value, -this.VScrollBar.Value);
this.Content.Invalidate();
}
}
private int _totalContentWidth;
public int TotalContentWidth
{
get { return _totalContentWidth; }
set
{
if (_totalContentWidth != value)
{
_totalContentWidth = value;
CalculateMinMax();
}
}
}
private int _totalContentHeight;
public int TotalContentHeight
{
get { return _totalContentHeight; }
set
{
if (_totalContentHeight != value)
{
_totalContentHeight = value;
CalculateMinMax();
}
}
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
CalculateMinMax();
}
private void CalculateMinMax()
{
if (this.Content != null)
{
// Reduced formula according to
// http://msdn.microsoft.com/en-us/library/system.windows.forms.scrollbar.maximum.aspx
// Note: The original formula is bogus.
// According to the article, LargeChange has to be known in order to calculate Maximum,
// however, that is not always possible because LargeChange cannot exceed Maximum.
// If (LargeChange) == (1 * visible part of control), the formula can be reduced to:
if (this.TotalContentWidth > this.Content.Width)
{
this.shScrollBar.Enabled = true;
this.shScrollBar.Maximum = this.TotalContentWidth;
}
else
{
this.shScrollBar.Enabled = false;
}
if (this.TotalContentHeight > this.Content.Height)
{
this.svScrollBar.Enabled = true;
this.svScrollBar.Maximum = this.TotalContentHeight;
}
else
{
this.svScrollBar.Enabled = false;
}
// this must be set after the maximum is determined
this.shScrollBar.LargeChange = this.shScrollBar.Width;
this.shScrollBar.SmallChange = this.shScrollBar.LargeChange / 10;
this.svScrollBar.LargeChange = this.svScrollBar.Height;
this.svScrollBar.SmallChange = this.svScrollBar.LargeChange / 10;
}
}
}
Example content:
public class ExampleContent : Control
{
public ExampleContent()
{
this.DoubleBuffered = true;
}
static Random random = new Random();
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var graphics = e.Graphics;
// random color to make the clip rectangle visible in an unobtrusive way
var color = Color.FromArgb(random.Next(160, 180), random.Next(160, 180), random.Next(160, 180));
graphics.Clear(color);
Debug.WriteLine(this.AutoScrollOffset.X.ToString() + ", " + this.AutoScrollOffset.Y.ToString());
CheckerboardRenderer.DrawCheckerboard(
graphics,
this.AutoScrollOffset,
e.ClipRectangle,
new Size(50, 50)
);
StaticBoxRenderer.DrawBoxes(graphics, new Point(0, this.AutoScrollOffset.Y), 100, 30);
}
}
public static class CheckerboardRenderer
{
public static void DrawCheckerboard(Graphics g, Point origin, Rectangle bounds, Size squareSize)
{
var numSquaresH = (bounds.Width + squareSize.Width - 1) / squareSize.Width + 1;
var numSquaresV = (bounds.Height + squareSize.Height - 1) / squareSize.Height + 1;
var startBoxH = (bounds.X - origin.X) / squareSize.Width;
var startBoxV = (bounds.Y - origin.Y) / squareSize.Height;
for (int i = startBoxH; i < startBoxH + numSquaresH; i++)
{
for (int j = startBoxV; j < startBoxV + numSquaresV; j++)
{
if ((i + j) % 2 == 0)
{
Random random = new Random(i * j);
var color = Color.FromArgb(random.Next(70, 95), random.Next(70, 95), random.Next(70, 95));
var brush = new SolidBrush(color);
g.FillRectangle(brush, i * squareSize.Width + origin.X, j * squareSize.Height + origin.Y, squareSize.Width, squareSize.Height);
brush.Dispose();
}
}
}
}
}
public static class StaticBoxRenderer
{
public static void DrawBoxes(Graphics g, Point origin, int boxWidth, int boxHeight)
{
int height = origin.Y;
int left = origin.X;
for (int i = 0; i < 25; i++)
{
Rectangle r = new Rectangle(left, height, boxWidth, boxHeight);
g.FillRectangle(Brushes.White, r);
g.DrawRectangle(Pens.Black, r);
height += boxHeight;
}
}
}

Making text move with the rectangle it is on top of

I am new to silverlight and so I may just be missing something due to my lack of knowledge. I am writing an app that uses Rectangles that you can click and drag to around the screen after they are generated. I have function that generates the rectangles:
public void formatBox(block b)
{
Rectangle Rec = new Rectangle();
Rec.Height = 100;
Rec.Width = 200;
SolidColorBrush newBrush = new SolidColorBrush();
newBrush.Color = b.blockColor;
SolidColorBrush blackBrush = new SolidColorBrush();
blackBrush.Color = Colors.Black;
Rec.StrokeThickness = 4;
Rec.Stroke = blackBrush;
Rec.Fill = newBrush;
Canvas.SetTop(Rec, generateY(b.locationY));
Canvas.SetLeft(Rec, generateX(b.locationX));
TextBlock blockname = new TextBlock();
blockname.Text = b.blockText;
blockname.FontSize = 25;
canvas1.Children.Add(Rec);
canvas1.Children.Add(blockname);
Binding topbinding = new Binding("Top");
Binding leftbinding = new Binding("Left");
topbinding.Mode = BindingMode.OneWay;
leftbinding.Mode = BindingMode.OneWay;
topbinding.Source = Rec.GetValue(Canvas.TopProperty);
leftbinding.Source = Rec.GetValue(Canvas.LeftProperty);
blockname.SetBinding(Canvas.TopProperty, topbinding);
blockname.SetBinding(Canvas.LeftProperty, leftbinding);
Rec.MouseLeftButtonDown += new MouseButtonEventHandler(Handle_MouseDown);
Rec.MouseMove += new MouseEventHandler(Handle_MouseMove);
Rec.MouseLeftButtonUp += new MouseButtonEventHandler(Handle_MouseUp);
}
These rectangles are built from a block class
public class block
{
public double locationX { get; set; }
public double locationY { get; set; }
public Color blockColor { get; set; }
public string blockText { get; set; }
public block(double x, double y, Color c, string s)
{
this.locationX = x;
this.locationY = y;
this.blockColor = c;
this.blockText = s;
}
}
And my mouse event handlers:
bool isMouseCaptured;
double mouseVerticalPosition;
double mouseHorizontalPosition;
public void Handle_MouseDown(object sender, MouseEventArgs args)
{
Rectangle item = sender as Rectangle;
mouseVerticalPosition = args.GetPosition(null).Y;
mouseHorizontalPosition = args.GetPosition(null).X;
isMouseCaptured = true;
item.CaptureMouse();
}
public void Handle_MouseMove(object sender, MouseEventArgs args)
{
Rectangle item = sender as Rectangle;
if (isMouseCaptured)
{
// Calculate the current position of the object.
double deltaV = args.GetPosition(null).Y - mouseVerticalPosition;
double deltaH = args.GetPosition(null).X - mouseHorizontalPosition;
double newTop = deltaV + (double)item.GetValue(Canvas.TopProperty);
double newLeft = deltaH + (double)item.GetValue(Canvas.LeftProperty);
// Set new position of object.
item.SetValue(Canvas.TopProperty, newTop);
item.SetValue(Canvas.LeftProperty, newLeft);
// Update position global variables.
mouseVerticalPosition = args.GetPosition(null).Y;
mouseHorizontalPosition = args.GetPosition(null).X;
}
}
public void Handle_MouseUp(object sender, MouseEventArgs args)
{
Rectangle item = sender as Rectangle;
isMouseCaptured = false;
item.ReleaseMouseCapture();
mouseVerticalPosition = -1;
mouseHorizontalPosition = -1;
}
The text I am trying to move is called blockname in formatBox(). I have tried Binding as you can see here but I think there is a gap in my knowledge of an easier way to do this. Either way I would like the text to move when the block moves under it when the mouse event handlers are triggered. How do I get the text to move with it?
Since you have mousehandlers already you could just skip the binding and use code. (The binding wouldn't work as expected anyway: the text would have the same coordinates as the reactangle so it gets drawn over the top/left lines of the rectangle. This will look ugly and make the text hard to read, you need to offset the text so that it's inside the reactangle or outside). Basically what you would do is in MouseDown put a flag high to idicate the mouse was pressed, and record the point where the mouse is. Then in MouseMove you check the flag: if it is on, calculate the new position for your rectangle as it's currentposition + the distance moved from the point recorded in MouseDown. The position of the text would then be the new position + some offset.
btw I suggest to split methods like formatBox into multiple smaller methods and choose better names for your variables: it will make the code not only more readable, but also more maintainable.
edit
to figure out which rectangle to move, do a hit test on all your elements in the MouseDwon handler. Something like this:
Rectangle rectangleUnderMouse = null;
foreach( var rec in rectangles )
{
if( VisualTreeHelper.HitTest( rec, pointWhereMouseIs ) )
{
rectangleUnderMouse = rec;
break;
}
}
edit
sorry I didn't see you asked which textblock to move.. That is easier: you could keep a Dictionary<Rectangle,TextBlock> inside your main class:
public void formatBox(block b)
{
//...
myDictionary[ Rec ] = textblock;
}
public void Handle_MouseMove( object sender, MouseEventArgs args )
{
//...
textBlockForThisRect = myDictionary[ item ];
//move textBlockForThisRect
}

setting position of a control doesn't seem to work when scroll is 'moved' (c#, winforms)

Description of the problem:
Create a 'custom control'. Set it's property AutoScroll to 'true'. Change it's bg-color to green.
Create second 'custom control'. Change it's bg-color to red.
On main form place first custom control
In code create 20 instances of second control
Add a button and in the button:
In code set their position in loop like c.Location = new Point(0, y);
y += c.Height;
Run App.
Press the button
Scroll the container
Press the button again and can someone please explain me WHY the 0 is not the beggining of the container form?! The controls are shifted...
Before you answer:
1) Yes the things need to be this way
2) Code sample below:
public partial class Form1 : Form
{
List<UserControl2> list;
public Form1()
{
InitializeComponent();
list = new List<UserControl2>();
for (int i = 0; i < 20; i++)
{
UserControl2 c = new UserControl2();
list.Add(c);
}
}
private void Form1_Load(object sender, EventArgs e)
{
foreach (UserControl2 c in list)
userControl11.Controls.Add(c);
}
private void button1_Click(object sender, EventArgs e)
{
int y = 0;
foreach (UserControl2 c in list)
{
c.Location = new Point(0, y);
y += c.Height;
}
}
}
Its because Location gives the coordinates of the upper left corner of the control relative to the upper left corner of its container. So when you scroll down, the Location will change.
Here is how to fix it:
private void button1_Click(object sender, EventArgs e)
{
int y = list[0].Location.Y;
foreach (UserControl2 c in list)
{
c.Location = new Point(0, y);
y += c.Height;
}
}
The first item will not be at position 0 because at the time locations are calculated, the new item has not been added to the panel controls. Besides you should use AutoScrollPosition to ajust the positons.
Here is my suggestion:
int pos = (Container.AutoScrollPosition.Y != 0 ? Container.AutoScrollPosition.Y - newitem.Height : 0);
if (Container.Controls.Count > 0)
{
foreach (Control c in Container.Controls)
{
c.Location = new Point(0, pos);
pos += c.Height;
}
}
}
newitem.Location = new Point(0, pos);
Container.Controls.Add(newitem);

Categories