Page 1 of 3

A little MouseHover Library (last update: August 10, 2016)

Posted: Sat Aug 19, 2006 11:13 pm
by netmaestro
This is a little includefile which will keep track of Mouse Enter/Leave events for gadgets on a window. Certain gadgets lend themselves well to such events and others do not. Those that don't are typically made up of multiple components, such as the IPAddress gadget, SpinGadget, PanelGadget, ComboBox, and a few others. I don't try to overcome this by treating these gadgets by window rectangle because I don't see a lot of use in catching mouse hover events for them anyway.

Library compiles fine with Tailbite if that's your preference.

Commands are:
InitHoverEvents(WindowID) ; Fire up the library
AddHoverGadget(GadgetID) ; Add a gadget to the list of those generating hover events
RemoveHoverGadget(GadgetID) ; Remove a single gadget from hover events (not necessary if you're executing EndHoverEvents() )
EndHoverEvents(WindowID) ; Remove all gadgets from hover events and free all resources used by the library

Here's the code:

Code: Select all

;===================================================== 
; Program:          Hover.pbi 
; Author:           netmaestro, srod 
; Date:             August 19, 2006 (Version 1.0)
;                   January 30, 2011 (Version 2.0)
;                   August 10, 2016 (Version 3.0)
; Target OS:        Windows all 
; Target Compiler:  PureBasic 4.0 and above 
;===================================================== 
; 
Import ""
  PB_Gadget_SendGadgetCommand(hwnd, EventType)
EndImport

#HoverEvent_EnterGadget = #WM_USER + 100 
#HoverEvent_LeaveGadget = #WM_USER + 101

Procedure CallWndProc(nCode, wParam, lParam)
  Static NewMap HotStack.RECT()
  Static current_hotgadget.i
  
  *mhs.MOUSEHOOKSTRUCT = lparam
  If GetProp_(*mhs\hwnd, "track_hover_events")
    If current_hotgadget <> *mhs\hwnd
      If MapSize(hotstack())
        ForEach HotStack()
          GetCursorPos_(@cp.POINT)
          If PtInRect_(HotStack(), PeekQ(@cp)) = 0 Or IsWindowVisible_(Val(MapKey(HotStack()))) = 0
            PB_Gadget_SendGadgetCommand(Val(MapKey(HotStack())), #HoverEvent_LeaveGadget)
            DeleteMapElement(HotStack())
          EndIf
        Next
      EndIf
      If Not FindMapElement(HotStack(), Str(*mhs\hwnd))
        PB_Gadget_SendGadgetCommand(*mhs\hwnd, #HoverEvent_EnterGadget)
        AddMapElement(HotStack(), Str(*mhs\hwnd))
        GetWindowRect_(*mhs\hwnd, FindMapElement(HotStack(), Str(*mhs\hwnd)))
      EndIf
      current_hotgadget = *mhs\hwnd
    EndIf
  Else
    If MapSize(hotstack())
      ForEach HotStack()
        GetCursorPos_(@cp.POINT)
        If PtInRect_(HotStack(), PeekQ(@cp)) = 0 Or IsWindowVisible_(Val(MapKey(HotStack()))) = 0
          PB_Gadget_SendGadgetCommand(Val(MapKey(HotStack())), #HoverEvent_LeaveGadget)
          DeleteMapElement(HotStack())
        EndIf
      Next
    EndIf
    If current_hotgadget
      current_hotgadget = 0
    EndIf
  EndIf
  
  ProcedureReturn CallNextHookEx_(0, nCode, wParam, lParam)
  
EndProcedure

ProcedureDLL InitHoverEvents(hwnd)
  Shared hook
  hook = SetWindowsHookEx_(#WH_MOUSE, @CallWndProc(), #Null, GetCurrentThreadId_())
  If hook
    ProcedureReturn 1
  Else
    ProcedureReturn 0
  EndIf
EndProcedure

ProcedureDLL AddHoverGadget(hwnd)
  If SetProp_(hwnd, "track_hover_events", #True)
    ProcedureReturn 1
  Else
    ProcedureReturn 0
  EndIf
EndProcedure

ProcedureDLL RemoveHoverGadget(hwnd)
  If RemoveProp_(hwnd, "track_hover_events")
    ProcedureReturn 1
  Else
    ProcedureReturn 0
  EndIf
EndProcedure

Procedure EnumControls(hwnd, lParam)
  If GetProp_(hwnd, "track_hover_events")
    RemoveProp_(hwnd, "track_hover_events")
  EndIf
  ProcedureReturn 1
EndProcedure

ProcedureDLL EndHoverEvents(hwnd)
  Shared hook
  EnumChildWindows_(hwnd, @EnumControls(), 0)
  If UnhookWindowsHookEx_(hook)
    ProcedureReturn 1
  Else
    ProcedureReturn 0
  EndIf
EndProcedure

;========================================================
;                  End of Library Code
;========================================================

CompilerIf #PB_Compiler_IsMainFile
  OpenWindow(0, 0, 0, 640, 480, "", #PB_Window_SystemMenu | #PB_Window_ScreenCentered) 
  
  PanelGadget(0, 8, 8, 300, 300)
  AddGadgetItem(0,0,"Tab 1")
  ContainerGadget(30, 5,5,260,220,#PB_Container_Flat)
  ContainerGadget(20, 15,15,220,180,#PB_Container_Flat)
  ButtonGadget(10, 25, 25, 100, 30, "")
  CloseGadgetList()
  CloseGadgetList()
  AddGadgetItem(0,1,"Tab 2")
  ButtonGadget(11, 10, 40, 200, 30, "")
  
  CloseGadgetList()
  
  InitHoverEvents(WindowID(0))
  AddHoverGadget(GadgetID(10))
  AddHoverGadget(GadgetID(11))
  AddHoverGadget(GadgetID(20))
  AddHoverGadget(GadgetID(30))
  
  Repeat 
    EventID = WaitWindowEvent()
    Select EventID
        
      Case #PB_Event_Gadget
        If EventType() = #HoverEvent_EnterGadget
          Debug "Entered Gadget " + EventGadget()
          GetWindowRect_(GadgetID(EventGadget()), @wr.RECT)
          MapWindowPoints_(0, WindowID(0), @wr, 2)
          StartDrawing(WindowOutput(0))
            DrawingMode(#PB_2DDrawing_Outlined)
            Box(wr\left-1, wr\top-1, wr\right-wr\left+2, wr\bottom-wr\top+2, #Red)
          StopDrawing()
          
        ElseIf EventType() = #HoverEvent_LeaveGadget
          Debug "Left Gadget " + EventGadget()
          GetWindowRect_(GadgetID(EventGadget()), @wr.RECT)
          MapWindowPoints_(0, WindowID(0), @wr, 2)
          StartDrawing(WindowOutput(0))
            DrawingMode(#PB_2DDrawing_Outlined)
            Box(wr\left-1, wr\top-1, wr\right-wr\left+2, wr\bottom-wr\top+2, GetSysColor_(#COLOR_3DFACE))
          StopDrawing()
        EndIf
    EndSelect
  Until EventID = #PB_Event_CloseWindow
  
  EndHoverEvents(WindowID(0))
CompilerEndIf


Posted: Sun Aug 20, 2006 12:36 am
by srod
Nice idea.

Have managed to shorten the code a little:

Code: Select all

REMOVED
Hope you don't mind?

:)

Posted: Sun Aug 20, 2006 12:42 am
by netmaestro
I don't mind at all, I appreciate the streamlining. I thought it looked a bit bloated. The only thing is, you've posted the pbi and the test program in one block which is titled Hover.pbi. If you could separate the test program into its own codeblock, I think that would be better. And, please add srod to the Author list! Thanks for the help

Posted: Sun Aug 20, 2006 12:52 am
by srod
Done. :oops:

Have not added my name 'cause it's your code, your idea.

Posted: Sun Aug 20, 2006 8:01 am
by netmaestro
MSDN wrote:The ChildWindowFromPoint function determines which, if any, of the child windows belonging to a parent window contains the specified point. The search is restricted to immediate child windows, grandchildren, and deeper descendant windows are not searched.
Uh-oh. I knew there was a reason I didn't trust ChildWindowFromPoint for this kind of task, I instinctively shied away from it when I designed this. Here's the problem: It seems to work fine except in situations where a gadget is on top of another gadget. Then ChildWindowFromPoint fails. I remember running into this on a previous project from several months back and dumping it for another approach. To see the problem, load the example program from the PB help file for the PanelGadget. Hook the streamlined version of the include into it and try to hover over the buttons on Panel 2. It doesn't notice them! My code above, based on the all-seeing EnumChildWindows, has no problem as every single child window is counted.

So question: Can the longer above code be streamlined and still keep all its functionality? Because it does seem a bit bloated to me.

[edit] Although it really works a treat. You can whip the mousepointer vertically over all the gadgets in the test program at a really fast speed and it records every entry/leaving without fail. I wouldn't want to lose that either.

Posted: Sun Aug 20, 2006 12:24 pm
by srod
They both fail, however, if you leave a gadget very fast and also leave the bounds of the main window!

I didn't know that about ChildWindowFromPoint_(), nothing about it in the Window's help file! Oh well... :?

I have a version running using a mousehook (which automatically works out the underlying child control etc), but whilst it's still shorter than the original code, it's complicated by various factors (e.g. the need to set #SS_NOTIFY styles for text gadgets, differentiating between a ListIcon and its header control etc.) I won't post this as it's getting fiddly.

In EasyVENT I use a mousehook, but because of the nature of EasyVENT, the mousehook works well.

With this in mind, the EnumChildWindows_() would seem the best way to go in this case. :)

Posted: Mon Aug 21, 2006 7:07 am
by netmaestro
I've reexamined this library very carefully and I've reached the conclusion that there is no alternative to maintaining a structured list of the states of all gadgets. If this is not done, then gadgets within gadgets cannot be counted properly. So the code logic, imho, has to stay pretty close to what it is to maintain functionality.

But I have managed to make a few improvements:

- Messages can now be retrieved in the main loop, no callback is required

- Switched to a threaded approach, solving the problem of quick exits getting missed.
There is guaranteed to be a LeaveGadget message for every EnterGadget message.

- The value in wParam now holds the hwnd of the gadget, and the ID is still in lParam.

- Code is a tiny bit more readable.

I've tested this pretty thoroughly and in my humble opinion its performance and reliability are "production-ready." ( \ducks)

Posted: Mon Aug 21, 2006 12:12 pm
by srod
Excellent work! :)

A thread... great idea!

A bit of testing and I really see how good this code of your is!
With PB's panel gadget example, the way it detects entering a child but not leaving the parent! No amount of fiddling with FindWindow_() without EnumChild... will do this!

I might update EasyVENT to use this method as it does suffer from the 'quick slide of the mouse' problem.

A suggestion ( :wink: )
Instead of the linked list, what about using a couple of Window' properties for each child window instead? This would save iterating through the list and perhaps speed things up a little. In fact a lot of the work could be done in the GetChildWindows() procedure - all in one go.


Thanks netmaestro. Great coding.

Posted: Mon Aug 21, 2006 7:50 pm
by netmaestro
Thanks for the kind words. And thanks for the idea! Yes, too many iterations, I knew this bugger was bloated. The EnumChildWindows command forces at least one full iteration through the gadgets, why not utilize that to get the messaging done? So, a rewrite based on your idea yields 24 fewer lines of code, 2 fewer iterations through the list and removes the necessity for the list to be global. Also the list only contains currently selected gadgets, not all gadgets in the window. So it'll only have 2-3 elements at most, usually one. All in all a much better program imho. Thanks again for the ** very ** valuable help.

(code is updated in first post)

Posted: Mon Aug 21, 2006 10:46 pm
by srod
The following removes the linked list and uses window's properties instead:

Code: Select all

REMOVED
This is perhaps a little faster than the linked list method, but it does suffer from one drawback; the application must remove the window's properties (which I've neglected above) - as they are not removed by Windows when the program closes! :)

Inspired by your code, I've nearly finished updating EasyVENT with this code which is working very well at the moment. EasyVENT is set up perfecty though to use the Window properties method.

It's all good stuff!

Posted: Mon Aug 21, 2006 11:21 pm
by netmaestro
I like the WindowProperties idea. To be honest I'm not that familiar with using them, so this is a good learning project for me. Question - What would the best way to remove the properties be? What I had in mind was a pair of commands, BeginHoverEvents() which would start the thread, and EndHoverEvents() which would call EnumChildWindows to remove all the properties. The only drawback to this approach would be you'd have to go to a loop logic like:

Code: Select all

BeginHoverEvents(WindowID(0))
Quit = 0
Repeat
  EventID = WaitWindowEvent()
  Select EventID
     Case #PB_Event_CloseWindow
       EndHoverEvents(WindowID(0))
       Quit = 1
  EndSelect
Until Quit
Is this the best way? Or can it be done without a second command?

Posted: Mon Aug 21, 2006 11:48 pm
by srod
Yea, it is quite a drawback.

In EasyVENT, all effected controls are superclassed (all share the same window proc) and so I remove the properties from the relevant control whenever it fires its #WM_NCDESTROY message.

In a similar way, alternatives to what you've already suggested for the code above would be a main window callback, or superclass each control (within the GetChildWindows() proc - being careful not to superclass each control twice!) and await #WM_NCDESTROY messages (one for each control) etc. This latter scheme would require a second property holding the address of the original window proc etc.

If I've time later I'll add the code.

Posted: Tue Aug 22, 2006 9:43 am
by netmaestro
Holy sh__, I just noticed this from the first post!
Last edited by netmaestro on Mon Aug 21, 2006 4:49 pm; edited 20 times in total
:oops: :roll:

Oh well - a creative writing prof I had once used to say the key to success is revise, revise, revise.

Posted: Sun Jun 17, 2007 4:51 pm
by netmaestro
Update June 17, 2007:

- Replaced the linkedlist method of cataloguing events with Window Properties, making for much cleaner logic. The problem of removing the properties is solved by a subclass procedure all gadgets use, calling RemoveProp in the #WM_NCDESTROY message.

- Compiled library with source, resident and help now available in Announcements section

Posted: Sun Jun 17, 2007 7:48 pm
by rsts
What a contributor :)

Once again in your debt.

cheers