09 September 2018

Did the mouse leave the window?

There are two mouse-messages that are never received unless you explicitly instruct Windows to track the mouse movement. The first message is WM_MOUSELEAVE that is supposed to report that the mouse has left the client-area. The second is WM_MOUSEHOVER which is posted after hovering a certain amount of time over some area. To obtain one (or both) of these messages you need to call TrackMouseEvent() API which notifies the application when the mouse leaves the window or when the mouse hovers over an area for a while.

The next program illustrates how to implement a mouseover feature by drawing a box that turns black when you move the mouse over it. The basic idea is to use WM_ MOUSEMOVE to know when the mouse has moved in or out of the box. The only problem is that if the user moves the mouse quickly outside the window, you won't get a WM_ MOUSEMOVE. To implement a correct behavior of mouseover, you need to know when the mouse has left the window entirely.

The program doesn’t use the _MouseMove eventsub, but combines the mouseover-logic into the _Message eventsub, which receives all (posted) mouse messages. There are no event subs for WM_MOUSELEAVE and WM_MOUSEHOVER, so they have to be handled in a general event- sub. An alternative would be to handle the messages in _MessageProc, but its use is a bit more complicated. In addition, _Message doesn’t require any return values, so it serves our purpose best.

OpenW Center 1
Do
  Sleep
Until Win_1 Is Nothing

Sub Win_1_Paint
  Box 10, 10, 100, 100
EndSub

Sub Win_1_Message(hWnd%, Mess%, wParam%, lParam%)
  Static Bool fTrackingMouse, fBoxHighLighted
  Dim tme As TRACKMOUSEEVENT
  Local Int mx, my
  Switch Mess%
  Case WM_MOUSEMOVE

    ' Track a mouseleave event. Results in a WM_MOUSELEAVE
    ' message when the mouse leaves the window.
    If !fTrackingMouse           ' set it only once
      tme.cbSize     = SizeOf(TRACKMOUSEEVENT)
      tme.dwFlags    = TME_LEAVE
      tme.hwndTrack  = Me.hWnd
      fTrackingMouse = TrackMouseEvent(tme) != 0
    EndIf

    ' If mouse is over the box start timer
    mx = LoWord(lParam%), my = HiWord(lParam%)
    If mx > 10 && mx < 100 && my > 10 && my < 100
      tme.cbSize      = SizeOf(TRACKMOUSEEVENT)
      tme.dwFlags     = TME_HOVER       ' start timer
      tme.hwndTrack   = Me.hWnd
      tme.dwHoverTime = HOVER_DEFAULT   ' use default time
      TrackMouseEvent(tme)              ' now wait for WM_MOUSEHOVER
    Else If fBoxHighLighted             ' hilighted and not over box
      Win_1.Invalidate 10, 10, 90, 90
      fBoxHighLighted = False
    EndIf

  Case WM_MOUSELEAVE            ' triggered by TrackMouseEvent
    fTrackingMouse = False      ' TrackMouseEvent not active anymore
    If fBoxHighLighted          ' redraw original box
      Win_1.Invalidate 10, 10, 90, 90
      fBoxHighLighted = False   ' box is not highlighted
    EndIf

  Case WM_MOUSEHOVER            ' triggered by TrackMouseEvent's timer
    mx = LoWord(lParam%), my = HiWord(lParam%)    ' mouse coordinates
    If !fBoxHighLighted && mx > 10 && mx < 100 && my > 10 && my < 100
      PBox 10, 10, 100, 100                     ' highlight the box
      fBoxHighLighted = True
    EndIf
  EndSwitch
EndSub

Public Const HOVER_DEFAULT   = 0xFFFFFFFF
Type TRACKMOUSEEVENT
  - DWord  cbSize
  - DWord  dwFlags
  - Handle hwndTrack
  - DWord  dwHoverTime
EndType
Declare Function TrackMouseEvent Lib "user32" Alias _
  "TrackMouseEvent" (ByRef EventTrack As TRACKMOUSEEVENT) As Long

The _Message sub declares two static booleans, fTrackingMouse and fBoxHighLighted, that keep track of the current state of the mouseover-logic. (If I would have used the _MouseMove eventsub to initiate the mouse tracking the variables should have been declared global, I always try to avoid global variables as much as possible.)
When the first (of many) WM_MOUSEMOVE message is received, the TrackMouseEvent() API is used to set up a WM_MOUSELEAVE  "one-shot" event. Exactly one and only one  WM_MOUSELEAVE message will be posted to the window specified in the hwndTrack member of the TRACKMOUSEEVENT structure, when the mouse has left the client area.
Note - The message will be generated only once. The application must call the TrackMouseEvent API again in order for the system to generate another WM_MOUSELEAVE message. In addition, when the mouse pointer is not over the application, a call to TrackMouseEvent() will result in the immediate posting of a WM_MOUSELEAVE message.

When the mouse is over the box, the TrackMouseEvent() is used to start a timer that eventually posts the WM_MOUSEHOVER message. After receiving WM_MOUSEHOVER the box is highlighted if the mouse is still over the box. The fBoxHighLigted variable is set to indicate the state of the box. If the variable is set but the mouse is no longer over the box the area occupied by the box is invalidated so that it is redrawn eventually.