PDA

View Full Version : Solved: how to detect when the mouse leaves a control on a userform



xltrader100
08-05-2008, 04:03 PM
I have an image ctl on a userform, and I'm trying to detect when the mouse leaves the image ctl. I'm already using the image ctl mousemove events for something else and that's working well, so I thought I could use the userform mousemove events to give me what I wanted. But it turns out that these both use the same X and Y names and they interfere with each other. There are no mouse clicks involved. Looking for suggestions.

TomSchreiner
08-05-2008, 06:42 PM
Use a frame instead of an image control.

UserForm1 with Label1 and Frame1.

Option Explicit

Private Type POINTAPI
X As Long
Y As Long
End Type

Private Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
Private Declare Function WindowFromPoint Lib "user32" (ByVal xPoint As Long, ByVal yPoint As Long) As Long
Private Declare Function SetCapture Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function ReleaseCapture Lib "user32" () As Long
Private Declare Function GetCapture Lib "user32" () As Long

Private FrameHwnd As Long

Private Function GetFrameHwnd() As Long
Dim PT As POINTAPI
GetCursorPos PT
GetFrameHwnd = WindowFromPoint(PT.X, PT.Y)
End Function

Private Sub Frame1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
If FrameHwnd = 0 Then FrameHwnd = GetFrameHwnd()

If (X < 0) Or (Y < 0) Or (X > Frame1.Width) Or (Y > Frame1.Height) Then
ReleaseCapture
Call MouseExit
ElseIf GetCapture() <> FrameHwnd Then
SetCapture FrameHwnd
Call MouseEnter
End If
End Sub

Private Sub MouseExit()
Label1.Caption = "MouseExit"
End Sub

Private Sub MouseEnter()
Label1.Caption = "MouseEnter"
End Sub

mikerickson
08-05-2008, 08:58 PM
Another aproach would be to use the UserForm_MouseMove event to detect when
x<image.Top,
image.Left+image.Width < x,
y<image.Top,
or image.Top+image.Height < y

xltrader100
08-05-2008, 09:55 PM
Tom, thanks for the response. Functionally, this does EXACTLY what I want, but there are a couple of niggles on the practical side. And I'm really out of my depth here with API calls so please excuse the uninformed questions, but when the form is running it takes over my whole machine. I can scroll but not much else. I can't type, and can't even (manually) run the procedure to hide the form.

So, is that the expected behavior? And if that can be overcome, would it be possible to modify the code, since I'm only interested in the frame exit, so that the form doesn't start up until the mouse enters the frame (with my image ctl inside it). We could use the existing image ctl mousemove events to trigger the start up of the form running the frame. And then when the mouse exits the frame your code changes the label text (as now) and then shuts the form down until the next mouse entry. What do you think? The only thing is, when the mouse is inside the frame and your form is running, it has to leave the image ctl responsive to mouse clicks, which right now it doesn't do.

xltrader100
08-05-2008, 10:02 PM
Thanks, Mike but I tried that and ran into the problem that the image ctl mousemove events stopped working when the userform mousemove events started streaming.

TomSchreiner
08-06-2008, 02:27 AM
"So, is that the expected behavior? And if that can be overcome, would it be possible to modify the code, since I'm only interested in the frame exit, so that the form doesn't start up until the mouse enters the frame (with my image ctl inside it). We could use the existing image ctl mousemove events to trigger the start up of the form running the frame. And then when the mouse exits the frame your code changes the label text (as now) and then shuts the form down until the next mouse entry. What do you think? The only thing is, when the mouse is inside the frame and your form is running, it has to leave the image ctl responsive to mouse clicks, which right now it doesn't do."

In particular...
"with my image ctl inside it"

No Image control at all. Replace it with a frame seeing that the frame can render pictures as well as an image control.

I don't understand...
"so that the form doesn't start up until the mouse enters the frame"

The form would have to be running to begin with... Right?

Please summarize the behavior you are expecting. Maybe attach an example. :)

xltrader100
08-06-2008, 06:21 AM
Well, the reason I'm using both the image ctl as well as the frame is firstly that the frame is tying several textboxes together as well as containing the image ctl. But the main reason I need to continue using the image ctl is that it has a backstyle property which the frame doesn't have, and I'm using this property (opaque/transparent) to cover and uncover another (same sized) image ctl underneath the top one that periodically shows a different picture but with the same mouse coordinates which I continue to get from the mousemove events of top image ctl.

So here's a summary of the desired behavior.
Before the mouse enters the frame there's nothing going on except a static picture, and the label shows msg1. Then the mouse enters the frame and the label starts to read out it's x-y location as provided by the mousemove events of the image ctl. There are some mouse clicks on the picture(s) and then the mouse leaves the frame and the label goes back to msg1.

All of this is working great except for detecting the final leaving of the frame. Currently I'm simulating this by detecting the mouse crossing a thin (few pixel wide) strip around the border of the picture where it should never be except on it's way out of the frame, but this only works half-assed because if the mouse crosses this strip too quickly then the mousemove events don't have time to pick it up and the label continues to show the last coordinates instead of switching back to msg1.

So it isn't the actual mouse crossing of the frame border that's important, but only the fact that the mouse is no longer in the frame. In fact I could even live with a few seconds delay after the frame crossing before the label switches back. I've tried several other things, like periodically sampling the mouse x-y on the form but I'm not getting it right.

Back to your code, and my comment about the form starting up. That was very bad wording on my part, and what I meant to say was, is it possible to prevent your code from starting up until the mouse enters the frame, and then stop it running after it detects the exit and changes the label? Because except for the overwhelming processor burden, your code does just what's needed (and btw, I thought it was prophetic when your code used the same names (label1 and frame1) as the existing controls so it just dropped right in. I thought, boy, this was meant to work).

TomSchreiner
08-06-2008, 06:46 AM
Unfortunately, I think you would need to subclass the form or the frame to get your desired results. This can be done with help from another component but not within VBA alone as VBA is just to slow to process all of the messages. If you are interested, I can post some more about this.

Besides the above, using Mike's reccomendation is probably the best way to go.

"I've tried several other things, like periodically sampling the mouse x-y on the form but I'm not getting it right."

What problems are you having here?

See the attached example using Mike's idea...
Option Explicit

Private OverImage As Boolean

Private Sub Image1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
If Not OverImage Then
OverImage = True
Call ImageMouseEnter
End If
End Sub

Private Sub Frame1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
If OverImage Then
OverImage = False
Call ImageMouseExit
End If
End Sub

Private Sub UserForm_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
If OverImage Then
OverImage = False
Call ImageMouseExit
End If
End Sub

Private Sub ImageMouseExit()
Label1.Caption = "ImageMouseExit"
End Sub

Private Sub ImageMouseEnter()
Label1.Caption = "ImageMouseEnter"
End Sub

Kenneth Hobs
08-06-2008, 08:40 AM
Tom's methods worked for me.

To use Mike's method, one needs to add a fudge factor. I called it b for a border width. Not sure if the control adds it or it comes from the cursor. Depending on how fast you move the cursor, it makes a difference in the reported x and y. Also, if you overlap controls, even this method won't work.

Just add the same controls and another label2 to see actual coordinates.


Private Sub UserForm_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Dim b As Integer
b = 3 'border size
With Image1
Select Case True
Case (X > .Left - b And X < (.Left + .Width + b)) And _
(Y > .Top - b And Y < (.Top + .Height + b))
Label1.Caption = "Mouse Enter Image"
Case Else
Label1.Caption = "Mouse Exit Image"
End Select

Label2.Caption = "x=" & X & " " & "y=" & Y & " " & "Left=" & .Left & " " & _
"Top=" & .Top & " " & "Right=" & .Left + .Width & " " & "Bottom=" & .Top + .Height
End With
End Sub

Bob Phillips
08-06-2008, 09:17 AM
You get the border width using InsideWidth



(Me.Width - Me.InsideWidth) / 2

Kenneth Hobs
08-06-2008, 09:58 AM
Thanks, xld. I wonder if all controls have the same border size?

b = (Me.Width - Me.InsideWidth) / 2
For me, b=2 using the code above. However, if I move the mouse quickly into image1, it will not show as being in it. b=3 seems a bit better but it still fails if I move the mouse too fast.

I think some of this may have to do with the cursor size and some with the mouse speed.

Similar to Tom's 2nd idea, one can do something like this. I created a Userform2 with the same controls in Tom's attachment.



Private Sub Image1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Label1.Caption = Image1.Name & " has a mousemove."
End Sub

Private Sub Frame1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Label1.Caption = Frame1.Name & " has a mousemove."
End Sub

Private Sub TextBox1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Label1.Caption = TextBox1.Name & " has a mousemove."
End Sub

Private Sub TextBox2_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Label1.Caption = TextBox2.Name & " has a mousemove."
End Sub

Private Sub UserForm_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Label1.Caption = UserForm2.Name & " has a mousemove."
End Sub

xltrader100
08-06-2008, 02:52 PM
Well, Tom I've had a very instructive morning playing with various versions of your sheet. I still haven't got it doing what I want but I know a whole lot more about mousemove events than I did before. I'm attaching a modified version of your form which gives a pretty good view of what's happening in the Immediate window.

One big problem that showed up is that every time the mouse goes over another control the mousemove events for the image, frame and form all stop, which totally accounts for the erratic behavior I was seeing because mine is a very busy form that's crowded with other controls.

I notice however that if I disable a control then the mousemove events don't stop when the mouse is over it and just keep on streaming, so one line of attack would be to detect when the mouse is over my image and then loop through all the other controls on the form and disable them. I haven't checked yet but I assume they all have an enabled property. This seems pretty clunky but would probably work ok as a practical matter in this case because if the mouse is over the image then it can't be doing anything anywhere else, so it doesn't matter if the other controls are disabled or not. But I think an even more promising line which I'm going to try next is to make a mask out of 4 transparent rectangles that mask out my image, and then use the mousemove events of those rectangles to detect when the mouse leaves the image. Since they're always on top then nothing else is going to interfere with their continued operation. And I'd only have to disable those 4 rectangles when the mouse leaves the image and not loop through and disable 30-odd individual controls.

I haven't had time to look over the other suggestions above, but I will. I just know there's a 3-liner out there somewhere waiting to blossom.:yes

TomSchreiner
08-06-2008, 05:39 PM
"But the main reason I need to continue using the image ctl is that it has a backstyle property which the frame doesn't have, and I'm using this property (opaque/transparent) to cover and uncover another (same sized) image ctl underneath the top one that periodically shows a different picture but with the same mouse coordinates which I continue to get from the mousemove events of top image ctl."

See attached.
Besides my last attempt below, Iv'e run out of viable ideas using native functionality. From the quote above... Could you use frames instead of image controls and use the visible property instead of the transparent background? That is, in conjunction with the method in my first reply?

The only other idea, though I would prefer subclassing over this, would be to use a doevents or timer loop.

1. Get the rectangle of your image in screen coords using several API functions.

2. Use a timer or loop to juxtaposition the cursor position and rectangle.

You could then determine if the cursor is within the rectangle or not. This might work fine, but I think there might be a performance problem.

Add a class module named: MouseEnterLeave
The spinbutton and scrollbar do not have mouse move events. I figured this out after writing the code and did bother removing them...

Option Explicit

Private WithEvents C0 As MSForms.UserForm
Private WithEvents C1 As MSForms.CheckBox
Private WithEvents C2 As MSForms.ComboBox
Private WithEvents C3 As MSForms.CommandButton
Private WithEvents C4 As MSForms.Frame
Private WithEvents C5 As MSForms.Label
Private WithEvents C6 As MSForms.Image
Private WithEvents C7 As MSForms.ListBox
Private WithEvents C8 As MSForms.MultiPage
Private WithEvents C9 As MSForms.OptionButton
Private WithEvents C10 As MSForms.ScrollBar
Private WithEvents C11 As MSForms.SpinButton
Private WithEvents C12 As MSForms.TabStrip
Private WithEvents C13 As MSForms.TextBox
Private WithEvents C14 As MSForms.ToggleButton

Private ParentForm As Object

Friend Sub InitNew(c As Object)

Set ParentForm = c.Parent
Do Until TypeOf ParentForm Is UserForm And Not TypeOf ParentForm Is Frame
Set ParentForm = ParentForm.Parent
Loop

Set C0 = ParentForm

Select Case TypeName(c)
Case "CheckBox": Set C1 = c
Case "ComboBox": Set C2 = c
Case "CommandButton": Set C3 = c
Case "Frame": Set C4 = c
Case "Label": Set C5 = c
Case "Image": Set C6 = c
Case "ListBox": Set C7 = c
Case "MultiPage": Set C8 = c
Case "OptionButton": Set C9 = c
Case "ScrollBar": Set C10 = c
Case "SpinButton": Set C11 = c
Case "TabStrip": Set C12 = c
Case "TextBox": Set C13 = c
Case "ToggleButton": Set C14 = c
End Select
End Sub

Private Sub MouseMove(HoverControl As Object)
If Not HoverControl Is ParentForm.HoverControl Then
If Not ParentForm.HoverControl Is Nothing Then Call ParentForm.MouseLeave(ParentForm.HoverControl)
Call ParentForm.MouseEnter(HoverControl)
End If
Set ParentForm.HoverControl = HoverControl
End Sub

Private Sub C0_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C0)
End Sub
Private Sub C1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C1)
End Sub
Private Sub C2_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C2)
End Sub
Private Sub C3_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C3)
End Sub
Private Sub C4_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C4)
End Sub
Private Sub C5_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C5)
End Sub
Private Sub C6_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C6)
End Sub
Private Sub C7_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C7)
End Sub
Private Sub C8_MouseMove(ByVal Index As Long, ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C8)
End Sub
Private Sub C9_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C9)
End Sub
Private Sub C10_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C10)
End Sub
Private Sub C11_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C11)
End Sub
Private Sub C12_MouseMove(ByVal Index As Long, ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C12)
End Sub
Private Sub C13_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C13)
End Sub
Private Sub C14_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
Call MouseMove(C14)
End Sub


Integrate this into your userform:

Option Explicit

Private MelCollection As Collection
Private pHoverControl As Object

Public Property Get HoverControl() As Object
Set HoverControl = pHoverControl
End Property

Public Property Set HoverControl(c As Object)
Set pHoverControl = c
End Property

Public Sub MouseEnter(c As Object)
If Me Is c Then
Label1.Caption = "MouseEnter: " & Me.Name
Else
Label1.Caption = "MouseEnter: " & c.Name
End If
End Sub

Public Sub MouseLeave(c As Object)
If Me Is c Then
Label2.Caption = "MouseLeave: " & Me.Name
Else
Label2.Caption = "MouseLeave: " & c.Name
End If
End Sub

Private Sub UserForm_Initialize()
Dim Mel As MouseEnterLeave
Dim c As Control

Set MelCollection = New Collection

For Each c In Me.Controls
Set Mel = New MouseEnterLeave
Mel.InitNew c
MelCollection.Add Mel
Set Mel = Nothing
Next
End Sub

Private Sub UserForm_Terminate()
Set MelCollection = Nothing
Set pHoverControl = Nothing
End Sub

xltrader100
08-06-2008, 08:59 PM
Tom, that was a Herculean effort and I'm most grateful for the time you spent on this. I don't quite know how to say this after all that, but the eventual solution turned out to be 3-liner after all (well, almost). I'm too tuckered out to go into it right now after spending 12 hours on it myself today but the attached wkb is pretty clear.

Briefly, cover the entire form with an image ctl sized to the form, initialized visible = true, BackStyle = fmBackStyleTransparent, and bring it to the top in the VBE. Then bring the "real" image ctl to the top. And that's pretty well it, with a little code to enable/disable the transparent form. I'm going to bed. Thanks again.

TomSchreiner
08-06-2008, 09:38 PM
I like mine better. :yes

xltrader100
08-07-2008, 09:55 AM
Tom, you were ragging on me from the start of this epic to use frames instead of image ctls, and I just discovered one big benefit of putting a frame around my image ctl is that it totally eliminates screen flashing when the mouse goes in and out of of the image, (which sets the form-sized transparent image enabled/disabled). Without the frame there's a lot of commotion going on, but with the frame around it the transition is silky smooth.

So, I'm wondering if there are other benefits of frames that I should be aware of, since you must have had reasons for your suggestion. Thanks.

TomSchreiner
08-07-2008, 11:54 AM
I did not realize that placing the image in a frame reduced flicker. I do know from my own experience that setting the transparent background at runtime has never been as sharp as I would like it to be. The only reason, relative to my first suggestion using set capture, was because a frame is a window control which means that is exposes a handle. (most msforms controls are windowless) I know my last post has a bit more code, but it does capture the enter and leave mouse moves. Have you tried it?

xltrader100
08-07-2008, 03:45 PM
Tom, odds and ends

I know my last post has a bit more code...
For me, that's not even a consideration. I quite literally don't care HOW big my programs get. That's why they make big hard drives and $30/Gb ram. My current VBA project just passed 40 Mb before it even starts to import data, with no performance issues at all.

Have you tried it?
I have now. :thumb And it works great. That's going into my toolbox as a generic forms manipulator. I haven't tried it yet in the actual form that started this whole mess, because so far my solution seems to work ok. But mine is a bit offbeat and could yet cause other interactions (or, as MS likes to refer to them "unexpected results"), and if so then I'll certainly reach out for yours.

Could you use frames instead of image controls and use the visible property instead of the transparent background? That is, in conjunction with the method in my first reply?
I did try that, but at the time I seemed to be seeing that even a non visible frame would interact with another control's mousemove events as the mouse passes over the frame. However trying that again on your demo form, that's not the case so I guess I was seeing ghosts, but that's why I didn't pursue that line any further. Also, I don't seem to be able to set the z-order of a frame, even in the VBE. Maybe there's no need to.

So, hopefully that's the end of the mouse moving saga and I can get on to other things.

xltrader100
08-08-2008, 11:35 AM
Tom, I'm finding your mouseEnterLeave form extremely useful. I've stripped off all the controls except for Label1 and Label2, and then saved the Wkb as UserformTemplate, and that becomes the starting point for all my new forms. Label1 and Label2 will live just out of sight to the right of the form window where they're always available, even months later if I want to edit the form. I also renamed your form UserformTemplate so it won't conflict with any existing userform1's when I drag it to it's new home.

It's like having Google Maps built into my forms! I think this is definitely one for the kb.

kt1978
09-10-2011, 07:35 AM
Tom

I know this ones an old post now but just wanted to thank you for this one.

Your code is exactly what I have been after for my project. This makes userforms so dynamic.

Excellent stuff, thanks again.

:thumb :thumb :thumb

viraga
11-22-2011, 09:49 AM
Dear Tom,

your solution works well but in my project I have many textboxes on userform in multipage control. I would like to get the name of the entered or leaved textboxes but in this case I get the following message: Object doesn't support this property or method. Debug, stopped at "MouseMove" Sub, "If Not HoverControl Is ParentForm.HoverControl Then" line. I try to modify the code but it is too complicated for me. Can anybody help me? Thanks and sorry for my English.

kt1978
11-23-2011, 04:00 PM
I'm sure this is not the correct way of doing but I had the same problem and worked around it like this.



Friend Sub InitNew(c As Object)

Set ParentForm = c.Parent

Do Until Left(ParentForm.Name, 8) = "UserForm"
Set ParentForm = ParentForm.Parent
Loop

--------


This works fine providing all your userforms are UserForm1,2,3 and so on.

Hope this helps. If anyone has the correct solution it would be good to know.

Thanks

viraga
11-24-2011, 09:07 AM
Dear kt1978,

thank you very much your answer. It works well! :bow: I didn't rename my userform because I have only 1 userform in this workbook, so it's name is still the default: UserForm1. I tried to modify this subroutin but I couldn't getting it working. Thank you again!




I'm sure this is not the correct way of doing but I had the same problem and worked around it like this.



Friend Sub InitNew(c As Object)

Set ParentForm = c.Parent

Do Until Left(ParentForm.Name, 8) = "UserForm"
Set ParentForm = ParentForm.Parent
Loop

--------


This works fine providing all your userforms are UserForm1,2,3 and so on.

Hope this helps. If anyone has the correct solution it would be good to know.

Thanks