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

Developed or developing a new product in PureBasic? Tell the world about it.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8425
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

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

Post 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

Last edited by netmaestro on Thu Aug 11, 2016 10:00 pm, edited 38 times in total.
BERESHEIT
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post by srod »

Nice idea.

Have managed to shorten the code a little:

Code: Select all

REMOVED
Hope you don't mind?

:)
Last edited by srod on Tue Aug 22, 2006 9:29 pm, edited 4 times in total.
I may look like a mule, but I'm not a complete ass.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8425
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post 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
BERESHEIT
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post by srod »

Done. :oops:

Have not added my name 'cause it's your code, your idea.
I may look like a mule, but I'm not a complete ass.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8425
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post 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.
BERESHEIT
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post 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. :)
I may look like a mule, but I'm not a complete ass.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8425
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post 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)
BERESHEIT
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post 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.
I may look like a mule, but I'm not a complete ass.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8425
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post 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)
BERESHEIT
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post 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!
Last edited by srod on Tue Aug 22, 2006 9:30 pm, edited 1 time in total.
I may look like a mule, but I'm not a complete ass.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8425
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post 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?
BERESHEIT
srod
PureBasic Expert
PureBasic Expert
Posts: 10589
Joined: Wed Oct 29, 2003 4:35 pm
Location: Beyond the pale...

Post 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.
I may look like a mule, but I'm not a complete ass.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8425
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post 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.
BERESHEIT
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8425
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post 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
BERESHEIT
rsts
Addict
Addict
Posts: 2736
Joined: Wed Aug 24, 2005 8:39 am
Location: Southwest OH - USA

Post by rsts »

What a contributor :)

Once again in your debt.

cheers
Post Reply