More than you want to know about Mouse Input

Someone recently asked for an end-to-end article on mouse handling, just like the keyboard handling article. Fortunately, mouse handling is not as complicated as keyboard handling, but there are a few topics we should probably chat about.

The Whidbey documentation for both Keyboard and Mouse handling has been greatly expanded. I encourage you to go check it out, send feedback, etc.

Details you don't need to know about for normal windows forms processing

Feel free to skip this section if you're more interested in Windows Forms. My understanding level here is from API usage, so feel free to correct as appropriate.

Going back to win32 land again, the operating system takes the raw input from the mouse and serves them up to an application using several mouse messages. 

Stealing parts of the table from the article on "About Mouse Input", we can see the basic categories. Obviously "L" stands for left, "M" stands for middle, "R" stands for right.

Client Mouse Button Press/Release events

Window Message Description of event Windows Forms Equivilant
WM_LBUTTONDBLCLK The left mouse button was double-clicked. DoubleClick, MouseDoubleClick (e.Button == MouseButtons.Left)
WM_LBUTTONDOWN The left mouse button was pressed. MouseDown (e.Button == MouseButtons.Left)
WM_LBUTTONUP The left mouse button was released. MouseUp (e.Button == MouseButtons.Left)
WM_MBUTTONDBLCLK The middle mouse button was double-clicked. DoubleClick, MouseDoubleClick (e.Button == MouseButtons.Middle)
WM_MBUTTONDOWN The middle mouse button was pressed. MouseDown (e.Button == MouseButtons.Middle)
WM_MBUTTONUP The middle mouse button was released. MouseUp (e.Button == MouseButtons.Middle)
WM_RBUTTONDBLCLK The right mouse button was double-clicked. DoubleClick, MouseDoubleClick (e.Button == MouseButtons.Right)
WM_RBUTTONDOWN The right mouse button was pressed. MouseDown (e.Button == MouseButtons.Right)
WM_RBUTTONUP The right mouse button was released. MouseUp (e.Button == MouseButtons.Right)

Then there's an entire set of non-client messages. Non-client refers to the frame of the window, typically one does not need to handle these messages, as the default window procedure (DefWndProc) of the form handles all the behavior associated with clicking on the non-client area. Non client messages are pre-pended with "NC". I wont bother to list them here as they're all the same concepts as above. Since folks don't typically need these events, windows forms doesn't make public events for these. If you need to handle them, you'll have to override the WndProc.

Client Mouse Move notification
WM_MOUSEMOVE - posted to a window when the cursor moves. MouseMove
WM_MOUSELEAVE - posted to a window when the cursor leaves the client area of the window MouseLeave
WM_MOUSEENTER - posted to a window when the cursor enters the client area of the window MouseEnter

HA! Tricked you! In Windows, there is no WM_MOUSEENTER! Typically folks use the first WM_MOUSEMOVE to simulate the mouse enter event, which is what windows forms does. 

Generating the mouse message

Ok, now that we've got a mapping between native messages and windows forms events, lets talk about how a mouse message becomes the event.

1. User moves and/or presses the mouse. 

2. The Operating System detects the event, starts deciding which control/window in the system should receive the event.

Has a control captured the mouse for its own personal use?
If so directly use the control, regardless if the mouse is actually within the control.
Else, use hit-testing to determine where the mouse message should go.

The operating system looks for the window that matches using something called hit-testing. Hit-testing is just like it sounds - "hey control! here's the current screen mouse coordinates, what part of you have I hit?"

This is formalized via the WM_NCHITTEST message. Again this is an advanced concept that the DefWndProc usually handles for customers pretty well, so there's no public event for this - you'll have to override the WndProc. When a control receives a WM_NCHITTEST message, they can say all kinds of interesting stuff about what part of the window it is. Depending on the return value, you can say things like the mouse is over a resizable border, or the client, or even nowhere. 

Some really neat values of WM_NCHITTEST are

  • HTCLIENT (says that its in the client area and you should get the mouse message as normal).
  • HTTRANSPARENT (says that this control is over another control, and just wants the mouse message to pass through to the control underneath it.)
  • HTNOWHERE (says the control doesn't want the mouse message at all)
  • HTBOTTOMRIGHT (says that this is the bottom right resizable area of the form - this is how a status bar's grip works)

3. Deciding which event should be generated 

Now that we know which window it goes to, and what part of the window has been clicked, the operating system decides what kind of window message to generate.

Should we generate a double click?
For particular controls, if it's a mouse down, the system checks if the delta of time between mouse downs is within SystemInformation.DoubleClickTime and the amount moved is within DoubleClickSize. If so, it will also generate a matching DBLCLK message. In win32, this is controlled by the CS_DBLCLKS class style. By default in windows forms, controls get this class style, and you can toggle on/off whether or not you want to get the DoubleClick event by using the ControlStyles.StandardDoubleClick control style.

4. Processing the mouse message:
Back at the message pump (you may have started one by calling Application.Run), under the covers someone has called GetMessage. In user32 this sets a few wheels in motion.

1. Process the message through any message hooks. This includes the mouse hook, as created by the SetWindowsHook(WH_MOUSE, ...) API. If the hook returns a non-zero value, this means that the message has been processed, and the operating system considers this message "handled". No more processing will occur.

2. If the message is a mouse down message, are we clicking on a new window? If the titlebar of this form wasn't active (dark blue) before the mouse button was pressed, we've got to see whether or not the act of clicking on the new form should make the new form active or not. Typically, the answer is yes, so once again this is not exposed by a public event in windows forms. 

In order to find out, the OS finds the control that's been clicked on and sends WM_MOUSEACTIVATE message. If the control doesn't know what to do, it calls DefWndProc, which goes up the parent chain until it gets to the topmost window (usually your form) and asks. It's really a two part question. 

  1. Do you want this mouse message?
  2. Do you think this window should be activated as a result of this click?

If the result of this message is that the window should be activated, the operating system will activate the window and set the cursor.

3. Finally the message is returned from GetMessage.

Windows Forms processing (start reading here if you've skipped the first section)

Once we've gotten the message, we run through the usual windows forms pre-processing tricks. The only relevant pre-processing goop for mouse messages is the IMessageFilter interface. At this point returning true from the IMessageFilter.PreFilterMessage will prevent the mouse message from being Translated/Dispatched to the control in question. In other words, returning true will prevent the control from recieving the mouse message/therefore it will not raise any events.

Everything from here on out is pretty straight forward. There are a few details I'd like to highlight.

Capture

Capture is the concept of "trapping" all mouse messages and forwarding them to a particular control.

When a control gets a MouseDown, windows forms will set Capture = true, so that all future mouse messages go to the control until the mouse button has been released. Windows Forms manually resets Capture = false when it gets a MouseUp.

The order of events is typically (don't hold me to it...)

  1. MouseEnter - user entered the control
  2. MouseMove - user moved the mouse within the control
  3. MouseHover - user has not moved the mouse for SystemInformation.MouseHoverTime & MouseHoverSize
  4. MouseDown - user has pressed mouse button
  5. Click - user has released mouse button
  6. MouseClick - (same as click - has more info)
  7. DoubleClick - user has mousedown'ed twice within SystemInformation.DoubleClickTime & DoubleClickSize
  8. MouseDoubleClick (same as click - has more info)
  9. MouseUp - (fired after Click stuff)
  10. MouseLeave - user has exited the control

The coordinates in all of these events will be client relative - that is they are the distance from the upper left hand corner of the control, as opposed to the upper left hand corner of the screen.

Hooks

Because hooks are so pervasive, avoid 'em if you can. Prefer using Capture or IMessageFilter. I know you'll ignore me, so here's a sample. =)

ControlStyles

There are several control styles that apply to mouse handling.

ControlStyles.StandardClick - this means a control wants to receive the Click event on MouseUp
ControlStyles.StandardDoubleClick - this means a control wants to receive the DoubleClick event on a second, well timed MouseUp
ControlStyles.UserMouse - this is typically defaulted to true, means you want all the events to fire. Otherwise all mouse events will be forwarded to DefWndProc with no extra processing by windows forms.

Want to get a second hover message?

If you want to "reset" your MouseHover event so you'll get it again, you can call ResetMouseEventArgs(). Note this is only necessary if you want to get a second mouse hover before you leave and enter the control again.

Getting the position of the mouse when you dont have a MouseEventArgs...

You can obtain the screen position of the mouse using either Control.MousePosition or Cursor.Position at any time. However these are the current position when you asked, which if you've paused in the debugger and moved your mouse around, can be problematic. The solution: GetMessagePos(). Unfortunately, this is not wrapped by windows forms. Each time someone calls GetMessage function, the coordinates of the mouse are snapped.

Translating coordinate systems

If you have client coordinates you want to convert to screen - use PointToScreen() or RectangleToScreen()
If you have screen coordinates you want to convert to client - use PointToClient() or RectangleToClient()

Note you should always use these methods instead of hand calculating, especially if you're planning on using a mirrored control (in whidbey RightToLeft = Yes, RightToLeftLayout = true). In this case, 0,0 of the coordinate system is actually from the upper right hand corner of the client area. 

If you're trying to map between two controls you can always a.PointToClient(b.PointToScreen(clientLocationInB)); Alternately you can use the native MapWindowPoints API.

More reading

About window procedures
https://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/windowprocedures/aboutwindowprocedures.asp

About Messages and Queues
https://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/messagesandmessagequeues/aboutmessagesandmessagequeues.asp

About Mouse Input
https://msdn.microsoft.com/library/en-us/winui/winui/windowsuserinterface/userinput/mouseinput/aboutmouseinput.asp?frame=true

SetCapture
https://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/userinput/mouseinput/mouseinputreference/mouseinputfunctions/setcapture.asp

Mirroring
https://www.microsoft.com/middleeast/msdn/mirror.aspx

WM_NCHITTEST
https://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/userinput/mouseinput/mouseinputreference/mouseinputmessages/wm_nchittest.asp

GetMessagePos
https://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/messagesandmessagequeues/messagesandmessagequeuesreference/messagesandmessagequeuesfunctions/getmessagepos.asp

int lastXY = GetMessagePos();
Point screenCoordOfLastMessage = new Point(SignedLOWORD(lastXY), SignedHIWORD(lastXY)) ;

   [DllImport("User32", ExactSpelling=true, CharSet=System.Runtime.InteropServices.CharSet.Auto)]
public static extern int GetMessagePos();

   public static int SignedHIWORD(IntPtr n) {
return SignedHIWORD( unchecked((int)(long)n) );
}
public static int SignedLOWORD(IntPtr n) {
return SignedLOWORD( unchecked((int)(long)n) );
}