Wednesday, August 31, 2005

MouseEnter and MouseLeave Events on UserControls

Windows Forms introduced a type of control called the UserControl. UserControls are pretty cool because you can put a bunch of controls on it in the designer and then reuse that UserControls on multiple Forms or other even within other UserControls.

In fact, with Visual Inheritance you can really start doing some neat things in terms of creating reusable base UIs.


Example UserControl in the designer

That said, one thing which is a bit confusing is how mouse events are handled. In this article, I'll be focussing on the MouseEnter and MouseLeave case but all Window based events work the same.

The fact is that UserControls are still made up of multiple Win32 windows and that PictureBox control I put on the UserControl example above is a separate Window from the UserControl itself.

What does that mean? It means that when I mouse into the UserControl I get a MouseEnter event from the UserControl. When I mouse over the PictureBox I then get a MouseLeave event from the UserControl and a MouseEnter from the PictureBox. Likewise, if I mouse out of the PictureBox, I get a MouseLeave from the PictureBox and a MouseEnter from the UserControl.

In some cases what you really care about are the MouseEnter and MouseLeave events for the UserControl itself, the container, and not the UserControl window which is left when the mouse enters a child window of the UserControl.

In order to solve that problem, what you need to do is setup Container events such as ContainerEnter and ContainerLeave.

Instead of adding events the simple way, I'm going to use the more advanced technique that Windows Forms itself uses.

static readonly object EventContainerEnter = new object();
static readonly object EventContainerLeave = new object();

public event EventHandler ContainerEnter
{
add
{
Events.AddHandler(EventContainerEnter, value);
}
remove
{
Events.RemoveHandler(EventContainerEnter, value);
}
}
public event EventHandler ContainerLeave
{
add
{
Events.AddHandler(EventContainerLeave, value);
}
remove
{
Events.RemoveHandler(EventContainerLeave, value);
}
}

The difference is that with simple events, the compiler declares a delegate field for each event which takes up space on the object. I just counted 58 events on the System.Windows.Forms.Control class. As you can see the space required for a delegate field starts to add up. Especially since only a couple of those events actually end up having listeners.

The way Windows Forms solved this problem is to use the Component.Events property which returns an EventHandlerList and statically defined objects, one for each event, which function as keys. When an event is subscribed to, the delegate is added to that Controls EventHandlerList with the corresponding key. For example, the EventContainerEnter static object is used to add and remove an EventHandler to and from the EventHandlerList. When the event is raised, the delegate is retrieved by that same EventContainerEnter key.

This mechanism allows there to be only one set of keys for all the events on all the controls in the AppDomain instead of a delegate declaration for all the events on all the controls in the AppDomain. There are, of course, delegate instances for any events that are subscribed to.

protected virtual void OnContainerEnter(EventArgs e)
{
if (ContainsMouse == false)
{
EventHandler handler = (EventHandler)Events[EventContainerEnter];
if (handler != null)
{
handler(this, e);
}
ContainsMouse = true;
}
}
protected virtual void OnContainerLeave(EventArgs e)
{
ContainsMouse = false;
EventHandler handler = (EventHandler)Events[EventContainerLeave];
if (handler != null)
{
handler(this, e);
}
}

Now that the events and their corresponding On*() methods are defined, the key part is hooking up their functionality to the UserControls MouseEnter and MouseLeave events:

protected override void OnMouseLeave(EventArgs e)
{
Point clientMouse = PointToClient(Control.MousePosition);
if (!ClientRectangle.Contains(clientMouse))
{
OnContainerLeave(e);
}
}
protected override void OnMouseEnter(EventArgs e)
{
OnContainerEnter(e);
}

The key to the puzzle to only raise the ContainerLeave even if the mouse actually left the UserControl container. The code in the OnMouseLeave() method handles that.

Finally, there is one remaining question, what is the "ContainsMouse" boolean used for? It's used to avoid having multiple ContainerEnter events from being raised when the mouse leaves a child control and re-enters the UserControl.

Update: The last thing to deal with are situations where parts or your whole UserControl is not at the top level. For example, if you have a child control dock filled on your UserControl, you end up not getting any MouseEnter or MouseLeave events on that UserControl.

By hooking up the child control MouseEnter and MouseLeave events to your UserControl, you can handle those situations as well. You can even do it in a generic way by iterating through the Control.Controls collection.

1 Comments:

At 8:05 AM, Anonymous Anonymous said...

There seems to be a slight problem with this implementation: if the control is covered by another control or form, the MouseLeave or MouseEnter events may never be fired. This situation could occur by having a button on the user control open a form over the control.

Kerry

 

Post a Comment

<< Home