With WinForms, is there a way to be alerted to a control changing location with respect to the screen?
Say you have a Form with a button on it, and you would like to know when the button is moved from its current pixel location on the screen. If the button is moved to a different location on its parent Form you could obviously use the LocationChanged event, but if the Form is moved by the user, how do you know the button has visually moved?
In this simplified case the quick answer is to monitor the Form's LocationChanged and SizeChanged events, but there can be an arbitrary number of levels of nesting so monitoring those events for each parent up the chain to the primary form is not feasible. Using a timer to check if the location changed also seems like cheating (in a bad way).
Short version:
Given only an arbitrary Control object, is there a way to know when that Control's location changes on the screen, without knowledge of the control's parent hierarchy?
An illustration, by request:
Note that this "pinning" concept is an existing capability but it currently requires knowledge of the parent form and how the child control behaves; this is not the problem I am trying to solve. I would like to encapsulate this control tracking logic in an abstract Form that "pin-able" Forms can inherit from. Is there some message pump magic I can tap into to know when a control moves on the screen without having to deal with all the complicated parent tracking?
I'm not sure why you would say tracking the parent chain "is not feasible". Not only is it feasible, it's the right answer and the easy answer.
Just a quick hack at a solution:
private Control _anchorControl;
private List<Control> _parentChain = new List<Control>();
private void BuildChain()
{
foreach(var item in _parentChain)
{
item.LocationChanged -= ControlLocationChanged;
item.ParentChanged -= ControlParentChanged;
}
var current = _anchorControl;
while( current != null )
{
_parentChain.Add(current);
current = current.Parent;
}
foreach(var item in _parentChain)
{
item.LocationChanged += ControlLocationChanged;
item.ParentChanged += ControlParentChanged;
}
}
void ControlParentChanged(object sender, EventArgs e)
{
BuildChain();
ControlLocationChanged(sender, e);
}
void ControlLocationChanged(object sender, EventArgs e)
{
// Update Location of Form
if( _anchorControl.Parent != null )
{
var screenLoc = _anchorControl.Parent.PointToScreen(_anchorControl.Location);
UpdateFormLocation(screenLoc);
}
}
Related
TLDR: I would like to know how I can create a hook into a begin-resize and an end-resize event for a design-time control instance on the designer surface.
Detail: Specifically, I am working with a design surface produced by a BasicLoader in the System.Design and System.Component.Design .NET namespaces. Specifically, I'm working a design-time instance of the TableLayoutPanel. That control exposes a SizeChanged event and a Resize event--alas, both fire during the resize operation--that is, while the control is being resized--as well as when the resize operation is complete. I therefore have no way of know when the resize operation began and when it officially ended.
One way to tackle this would be to detect a mouse-down event along with a resize event--but it's unclear to me how I can detect a mouse-down event on any of the grab handles of a control being resized.
For the records, I revisited the BehaviorService and saw that it exposes BeginDrag, EndDrag, and Synchronize--I see nothing in that service that would help me with BeginResize/EndResize events.
So, ideally, I would like to subscribe to BeginResize/EndResize events for any designer instance of a Winform control, but I would be happy if the provided answer covered only my need to have these events attached to a designer instance of the TableLayoutPanel control...
Any thoughts?
When a resize starts, a designer transaction with a specific description starts and when the design ends, the transaction will be closed.
You can rely on TransactionOpened event of the IDesignerHost and check the TransactionDescription to see if it starts with "Resize", set a flag resizing to true. Then in TransactionClosed you can check if the flag is true, it means it's a resize end has happened.
Example
Here is a PoC to show how it works. Add the following control into a Windows Forms project and after building the project, drop an instance of MyControl on the form. Then if you try to resize the form, you will see the Resize started. and Resize ended. text on title-bar of the form:
Here is the code:
using System;
using System.ComponentModel.Design;
using System.Windows.Forms;
public class MyControl : Control
{
IDesignerHost host;
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if(DesignMode)
{
host = (IDesignerHost)Site.GetService(typeof(IDesignerHost));
host.TransactionOpened += Host_TransactionOpened;
host.TransactionClosed += Host_TransactionClosed;
}
}
bool resizing = false;
private void Host_TransactionOpened(object sender, EventArgs e)
{
if (host?.TransactionDescription?.StartsWith("Resize") == true)
{
resizing = true;
((Control)host.RootComponent).Text = "Resize Started.";
}
}
private void Host_TransactionClosed(object sender,
DesignerTransactionCloseEventArgs e)
{
if (resizing)
{
resizing = false;
((Control)host.RootComponent).Text = "Resize ended.";
}
}
}
If you want to do some R&D before going with this solution, you may want to take a look at the following classes (mostly internal) in System.Design assembly: GrabHandleGlyph, ResizeBehavior.
I am having a rather odd problem with the Gecko Webbrowser control, I have created my own class which inherits off of the Gecko Webcontrol and within the constructor of this I have set an event:
class fooGeckoClass: Gecko.GeckoWebBrowser
{
public fooGeckoClass()
{
this.DomClick += new EventHandler<Gecko.GeckoDomEventArgs>(fooEventFunction);
}
private static void fooEventFunction(Object sender, Gecko.GeckoDomEventArgs e)
{
((Gecko.GeckoWebBrowser)sender).Navigate("www.foo.com");
}
}
I am using three of these controls in a manually created UserControl, the controls are loaded in dynamically at start up from a config file and added the the UserControl controls collection. When clicking on any of the three controls, all three will navigate to "www.foo.com" away from there original site. I had a look at:
e.StopPropagation();
Which specifies that it stops further propagation of events during an event flow, however it does also specify that it will handle all events in the current flow, I believe the events must have already been given to the controls before this has a chance to stop it as the the three controls will still fire the event. I also tried e.Handled = true to no avail. Has anyone encountered this problem before and have any kind of solution to make it only fire on the control that was clicked on?
EDIT:
It may be worth showing how the controls are added to the form seeing as this must be where the problem is occurring (it does not happen if the controls are just placed in a user control in a small test app).
private void fooUserControl_Load(object sender, EventArgs e)
{
if (!this.DesignMode)
{
for (int iControls = 0; iControls < geckObs.Count(); iControls ++)
{
fooGeckoClass geckControl = new fooGeckoClass();
this.Controls.Add(geckControl );
break;
}
}
}
Odd answer but I seem to have resolved the issue, DomClick was being called at first run, changing to DomMouseClick or DomMouseUp has completely resolved the issue. I assume DomClick must be an event unto itself as it also doesn't use the GeckoDomMouseEventArgs but the regular GeckoEventArgs.
EDIT:
To add to this, the site I was going to was actually calling DomClick when it had finished loading hence the reason it was being called at start up across all three browsers.
I'm attempting to make a grid based dungeon system at the moment in Visual Studio 2010. I have a main user control which contains 64 other smaller user control objects, which I've called GridSquares, organised into an 8x8 grid. The idea behind the grid squares is to act as potential movement spaces within the 'dungeon'. The problem I have at the moment is that I need to be able to call a click event on the user controls (GridSquares) themselves, which have been placed on screen so I can retrieve their coordinate (name) for comparison. However the event does not work when I call it (through clicking).
I am aware that the events work when I place them within the usercontrol (GridSquare object) but I need the click even to work when the user control itself is clicked.
Given that all 64 objects placed are the same type I can not work within the GridSquare class as I require the name of the user control to be returned through the event.
I hope this makes sense but please ask if I need to explain further.
Many thanks, Liam
EDIT:
I'm not sure how much this will help or what code to display but the GridSpace controls have already been added to the 'dungeon' user control. Then within I add all 64 to a dictionary:
gridSpaces.Add(gs11.Name, gs11);
Where gs11 is the name of the GridSquare.
From here I tried creating event handlers for the individual user controls on the dungeon screen, which failed to call.
I think i get what your saying. Add this code to your user control:
public new event EventHandler Click {
Add {
base.Click += value;
foreach(Control i in Controls) {
i.Click+=value;
}
}
remove {
base.Click -= value;
foreach(Control i in Controls) {
i.Click -= value;
}
}
}
this will add the click event to everything in your user control, i hope i didnt make any errors, and that this helps
You can use the same handler for each GridSquare and use the sender parameter to decide which one was clicked:
protected void Page_Load(object sender, EventArgs e)
{
for (int i = 0; i < 64; i++)
{
GridSquare square = new GridSquare();
square.Click += new EventHandler(gridSquare_Click);
grid.Add(gridSquare);
}
}
void gridSquare_Click(object sender, EventArgs e)
{
GridSquare square = (GridSquare)sender;
// do something cool with the clicked square here
}
I am trying to handle a mouseclick event on a particular form that should fire if the mouse cursor falls between a set of coordinates - lets say a square.
I understand that if I had an empty form I could simply tie in to the mousemove event and off I go. But in reality there may be up to 10 different overlapping controls and in my test app the mousemove event only fires if the cursor is on the actual form itself and not if its over a child control.
Does anyone know how to handle this event when there are an unknown number of child controls at design time?
Is there an easy one-liner I can use?
try this:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
AddMouseMoveHandler(this);
}
private void AddMouseMoveHandler(Control c)
{
c.MouseMove += MouseMoveHandler;
if(c.Controls.Count>0)
{
foreach (Control ct in c.Controls)
AddMouseMoveHandler(ct);
}
}
private void MouseMoveHandler(object sender, MouseEventArgs e)
{
lblXY.Text = string.Format("X: {0}, Y:{1}", e.X, e.Y);
}
}
I know this post is quite old, but it seems to me that the simplest method would be for the form to implement IMessageFilter. In the constructor (or in OnHandleCreated) you call
Application.AddMessageFilter(this);
and then you can catch the messages of all windows in your implementation of IMessageFilter.PreFilterMessage.
You'd likely need to use P/Invoke for the WIN32 IsChild method
[DllImport("user32.dll")]
public static extern bool IsChild(IntPtr hWndParent, IntPtr hWnd);
along with the form's Handle property to ensure that you're handling the right messages.
imho there is a bit of a binary situation here : and there is no "one-liner." the only solution I can see is to get your controls that don't implement events into a .NET container that does.
When any control gets a click, the normal expected behavior is that it will become the Active Control of the Form (which can always be accessed by this.ActivceControl).
But, particulary if the control you clicked captures the mouse, something has got to raise an event since .NET does not implement event "bubbling" (as WPF does).
The usual way to deal with extending behavior of any object that is sealed or whatever is to write an extension method, and I have found writing extensions for Control quite easy, but I don't know if that will help you in this case. Unfortunately I am out of my home country right now, and do not have Visual Studio to play around with.
One strategy you can use to determine if a given Point on a Form falls within the bounds of any Control is to enumerate the areas (Bounds) of all controls on the Form via 'forall of the Forms Control.Collection (this.Controls). But, if you have overlapping Controls, you then have the issue of more than one control possibly containing a given point.
best, Bill
Why don't you just use the controls' mouseover event handlers?
I know I'm a bit late to the punch, but I was having troubles with this earlier today when using a Panel as a title bar. I had a label to display some text, a picturebox and a few buttons all nested within the Panel, but I needed to trap the MouseMove event regardless.
What I decided to do was implement a recursive method handler to do this, as I only had 1 level of nested controls, this may not scale overly well when you start approaching ridiculous levels of nesting.
Here's how I did it:
protected virtual void NestedControl_Mousemove(object sender, MouseEventArgs e)
{
Control current = sender as Control;
//you will need to edit this to identify the true parent of your top-level control. As I was writing a custom UserControl, "this" was my title-bar's parent.
if (current.Parent != this)
{
// Reconstruct the args to get a correct X/Y value.
// you can ignore this if you never need to get e.X/e.Y accurately.
MouseEventArgs newArgs = new MouseEventArgs
(
e.Button,
e.Clicks,
e.X + current.Location.X,
e.Y + current.Location.Y,
e.Delta
);
NestedControl_Mousemove(current.Parent, newArgs);
}
else
{
// My "true" MouseMove handler, called at last.
TitlebarMouseMove(current, e);
}
}
//helper method to basically just ensure all the child controls subscribe to the NestedControl_MouseMove event.
protected virtual void AddNestedMouseHandler(Control root, MouseEventHandler nestedHandler)
{
root.MouseMove += new MouseEventHandler(nestedHandler);
if (root.Controls.Count > 0)
foreach (Control c in root.Controls)
AddNestedMouseHandler(c, nestedHandler);
}
And then to set it up is relatively simple:
Define your "true" handler:
protected virtual void TitlebarMouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
this.Text = string.Format("({0}, {1})", e.X, e.Y);
}
}
And then set up the controls event subscribers:
//pnlDisplay is my title bar panel.
AddNestedMouseHandler(pnlDisplay, NestedControl_Mousemove);
Relatively simple to use, and I can vouch for the fact it works :)
I have a dialog with loads of control in it. Each and evey control will be populated during the loading sequence and it might take a while for it to get completely filled. Mean while, I don't wanna allow the user to click on any of the controls. In other words, I wanna disable the control from receiving the events and I don't wanna disable the controls (as it might look odd).Also, I don't wanna subscribe and unsubscribe for the events regular intervals. Is there any way to stop the controls from listening to the events for a brief time ??
Sudarsan Srinivasan
The whole point of disabling controls is to communicate to the user that the control cannot be used at a particular time. This is a convention that users have learned and are used to, so I would advice to follow that. Not doing that may confuse the users.
The easiest way is to disable the container in which the controls are located in, rather than disabling each and every control. A better way (or at least the way that I prefer) is to have a method that will control the Visible and Enabled properties of controls based on which state the UI is in.
The easiest way is to move the control population out of the load event (if possible). Then in Load do something like:
private bool _LoadComplete;
void OnFormLoad(Object sender, EventArgs e)
{
_LoadComplete = true;
InitializeControls();
_LoadComplete = false;
}
void InitializeControls()
{
// Populate Controls
}
void OnSomeControlEvent()
{
if (_LoadComplete)
{
// Handle the event
}
}
Edit A Couple other Ideas:
Set the Application.Cursor = WaitCursor (typically will disallow clicking, but not a 100% guarantee)
Create a "Spinner" control to let the user know that the screen is busy. When loading bring it to the front so it sits on top and covers all other controls. Once you're done loading set it to visible = false and show your other controls
Unfortunately the only way i know of is to have a class variable (called something like _loading) and in each control handler do something like:
If (! _loading )
{
...
}
And in your loading code set _loading = true; once you have finished loading.
If you just want to disable user input, then you can set the form's Enabled property to false.
This has the effect of blocking user input to any of the form's controls, without changing the appearance of the controls; it's the technique used internally by the ShowDialog method.