|
I have a panel on a form that is completely covered in other controls (and it
isn't determined until runtime which and where those controls are on
the panel). There is absolutely 0 space of the panel which is exposed.
I need to find out when the mouse enters and leaves the bounds of this
control. The MouseEnter and MouseLeave events don't work because for
those to fire, the mouse has to be directly over the panel (and instead
it is directly over the panel's controls).
I've printed out the WndProc messages, and I noticed that sometimes
WM_NOTIFY or WM_SETCURSOR messages are fired, but it is sporadic. It
seems like if I move the mouse fast, those events don't ever get fired.
Does anyone have a tip for how to handle something like this? | | BlueMikey Tuesday, October 31, 2006 5:30 PM | I've been playing with IMessageFilter as of late. It seemed perfect to solve this problem. First, we need to subclass the Panel so we can generate the MouseEnter and MouseLeave events. I also put the message processing code in it:
using System; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms;
class MyPanel : Panel { private bool mHasMouse; public bool HasMouse { // Helper property to generate the MouseEnter/Leave events get { return mHasMouse; } set { if (mHasMouse == value) return; mHasMouse = value; if (value) OnMouseEnter(new EventArgs()); else OnMouseLeave(new EventArgs()); } } [DllImport("user32.dll")] private static extern bool ClientToScreen(IntPtr hWnd, ref Point p);
public static void TrapMouse(Form f, ref Message m) { if (m.Msg != 0x200 && m.Msg != 0x2A1) return; // Only want WM_MOUSEMOVE and WM_MOUSEHOVER // Map mouse position to screen coordinates Point pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16); ClientToScreen(m.HWnd, ref pos); // Search panel at mouse position SearchPanel(f.Controls, pos); } private static void SearchPanel(ControlCollection ctls, Point pos) { // Search <ctls> for a MyPanel instance foreach (Control ctl in ctls) { if (ctl is MyPanel) { MyPanel pnl = (MyPanel)ctl; Point ppos = pnl.PointToClient(pos); pnl.HasMouse = ppos.X >= 0 && ppos.Y < pnl.Width && ppos.Y >= 0 && ppos.Y < pnl.Height; } // Recurse to find panels on a container if (ctl.Controls.Count > 0) SearchPanel(ctl.Controls, pos); } } }
The TrapMouse method implements the logic of handling the mouse messages and finding the panel that the mouse might be on. We need to call it from the form that hosts the panel(s). Build your project and drop a MyPanel control on the form. Cover it with other controls as necessary, leave a bit of space so you can see the results. Here's the form code:
using System; using System.Drawing; using System.Windows.Forms;
namespace WindowsApplication1 { public partial class Form1 : Form, IMessageFilter { public Form1() { InitializeComponent(); // Install message filter Application.AddMessageFilter(this); // Install event handlers this.FormClosed += Form1_FormClosed; myPanel1.MouseLeave += myPanel1_MouseLeave; myPanel1.MouseEnter += myPanel1_MouseEnter; myPanel1.BackColor = Color.Black; } private void Form1_FormClosed(object sender, FormClosedEventArgs e) { // Unregister message filter Application.RemoveMessageFilter(this); } public bool PreFilterMessage(ref Message m) { // Trap mouse messages MyPanel.TrapMouse(this, ref m); return false; } private void myPanel1_MouseLeave(object sender, EventArgs e) { // Demo, black background when mouse leaves panel myPanel1.BackColor = Color.Black; } private void myPanel1_MouseEnter(object sender, EventArgs e) { // Demo, white background when mouse enters panel myPanel1.BackColor = Color.White; } } }
| | nobugz Friday, November 03, 2006 8:27 PM | You are seeing controls notifying their container. It depends on the type of control what kind of notifications are sent. A button generates WM_GETTEXT and WM_PRINTCLIENT, a ListBox generates WM_NOTIFY and WM_SETCURSOR when you're on its border. A CheckBox generates nothing at all.
You'll need the panel's controls to help out telling the panel what's going on. This is probably most easily done with a UserControl, not a panel...
| | nobugz Wednesday, November 01, 2006 12:34 PM | I was afraid that might be the only solution. The problem with that is that our dynamic controls have tons of controls on them as well. I'd have this tree traversal thing going on adding and removing handlers all over the place.
We ended up solving it right now through mouse hooks instead. I don't know if that is the best practice, but it works at least for the time being. I guess I just wished that MouseLeave worked a bit differently.
| | BlueMikey Friday, November 03, 2006 5:39 PM | I've been playing with IMessageFilter as of late. It seemed perfect to solve this problem. First, we need to subclass the Panel so we can generate the MouseEnter and MouseLeave events. I also put the message processing code in it:
using System; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms;
class MyPanel : Panel { private bool mHasMouse; public bool HasMouse { // Helper property to generate the MouseEnter/Leave events get { return mHasMouse; } set { if (mHasMouse == value) return; mHasMouse = value; if (value) OnMouseEnter(new EventArgs()); else OnMouseLeave(new EventArgs()); } } [DllImport("user32.dll")] private static extern bool ClientToScreen(IntPtr hWnd, ref Point p);
public static void TrapMouse(Form f, ref Message m) { if (m.Msg != 0x200 && m.Msg != 0x2A1) return; // Only want WM_MOUSEMOVE and WM_MOUSEHOVER // Map mouse position to screen coordinates Point pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16); ClientToScreen(m.HWnd, ref pos); // Search panel at mouse position SearchPanel(f.Controls, pos); } private static void SearchPanel(ControlCollection ctls, Point pos) { // Search <ctls> for a MyPanel instance foreach (Control ctl in ctls) { if (ctl is MyPanel) { MyPanel pnl = (MyPanel)ctl; Point ppos = pnl.PointToClient(pos); pnl.HasMouse = ppos.X >= 0 && ppos.Y < pnl.Width && ppos.Y >= 0 && ppos.Y < pnl.Height; } // Recurse to find panels on a container if (ctl.Controls.Count > 0) SearchPanel(ctl.Controls, pos); } } }
The TrapMouse method implements the logic of handling the mouse messages and finding the panel that the mouse might be on. We need to call it from the form that hosts the panel(s). Build your project and drop a MyPanel control on the form. Cover it with other controls as necessary, leave a bit of space so you can see the results. Here's the form code:
using System; using System.Drawing; using System.Windows.Forms;
namespace WindowsApplication1 { public partial class Form1 : Form, IMessageFilter { public Form1() { InitializeComponent(); // Install message filter Application.AddMessageFilter(this); // Install event handlers this.FormClosed += Form1_FormClosed; myPanel1.MouseLeave += myPanel1_MouseLeave; myPanel1.MouseEnter += myPanel1_MouseEnter; myPanel1.BackColor = Color.Black; } private void Form1_FormClosed(object sender, FormClosedEventArgs e) { // Unregister message filter Application.RemoveMessageFilter(this); } public bool PreFilterMessage(ref Message m) { // Trap mouse messages MyPanel.TrapMouse(this, ref m); return false; } private void myPanel1_MouseLeave(object sender, EventArgs e) { // Demo, black background when mouse leaves panel myPanel1.BackColor = Color.Black; } private void myPanel1_MouseEnter(object sender, EventArgs e) { // Demo, white background when mouse enters panel myPanel1.BackColor = Color.White; } } }
| | nobugz Friday, November 03, 2006 8:27 PM | Ooh, that is a pretty solution. I definitely like that better than throwing these mouse hook handlers all over the place.
And, more than that, what I'm doing is a little more simple. I just need to know if it is within the bounds of the panel, so instead of that recursive method, I could just check the bounds of the panel versus the point from the message.
Very nice, thanks very much. I'll be throwing that in now. :)
| | BlueMikey Friday, November 03, 2006 9:50 PM | Glad you liked it, I was quite happy with it too. This is really a testament to the original .NET class library designers. I've been getting quite impressed as of late how smart these guys and gals were when they came up with System.Windows.Forms and the BCL. They managed to make it both easy to use and infinitely extensible. Anders rules. I'm not that thrilled about the .NET 2.0 additions and got my toes curled about .NET 3.0; hope it works out...
| | nobugz Friday, November 03, 2006 11:15 PM | Another solution not requiring unmanaged code is stated in post:
http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=805145&SiteID=1
A helperproperty to determine if mouse leave/enter events for the inner controls is ignore is below:
private bool IsMouseInsideControl
{
get
{
Point mousePosistion = new Point(MousePosition.X, MousePosition.Y);
mousePosistion = PointToClient(mousePosistion);
// If we're to hide the control on a MouseLeave event, that event will be fired
// when the mouse is one pixel shy of leaving the control. So unless we reduce
// the client rectangle by at least one pixel, the mouse will always appear to be
// inside the control on this event.
Rectangle reducedClientRect = Rectangle.Inflate(ClientRectangle, -1, -1);
// return if the mouse position is inside the control
return reducedClientRect.Contains(mousePosistion);
}
} | | DotNetAcing Wednesday, November 08, 2006 12:50 AM | Nice hack... is this a short coming in the UI framework?
Maybe they need four events?
MouseEnter, MouseEnterBounds MouseLeave, MouseLeaveBounds
Cheers, James. | | James Miles Tuesday, November 14, 2006 7:12 AM |
|