Userform.show on cursor position - excel

i wish to create code that will open form at any form object by mouse hover. For now i have form.show and unload form on mousemove event.
I can add some variable to each object to recognize their position on window and define top left like on example
Private Sub UserForm_Initialize()
If par_hoverForm = uf_Generator.com_MaxQty.Name Then
Me.Top = Application.Top + uf_Generator.Top + uf_Generator.fr_settings.Top + uf_Generator.com_MaxQty.Top + uf_Generator.com_MaxQty.Height + 30
Me.Left = uf_Generator.Left + uf_Generator.fr_settings.Left + uf_Generator.com_MaxQty.Left + 10
End If
End Sub
but is it possible to form.show at cursor position + some offset? this code getting cursor coordinates but i dont know how to convert it to top/left.
#If VBA7 Then
Declare PtrSafe Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
#Else
Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
#End If
' Create custom variable that holds two integers
Type POINTAPI
Xcoord As Long
Ycoord As Long
End Type
Sub GetCursorPosDemo()
Dim llCoord As POINTAPI
' Get the cursor positions
GetCursorPos llCoord
' Display the cursor position coordinates
MsgBox "X Position: " & llCoord.Xcoord & vbNewLine & "Y Position: " & llCoord.Ycoord
End Sub
any advice are welcome

I will show you one way to do it, which should allow you to customize it to your needs.
First, this is the base code to convert the pixels to point and move an object to the pointer. I've set it to be called with another object, but obviously you can code a specific object in there:
Private Type POINTAPI
X As Long
Y As Long
End Type
Private Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hdc As Long, ByVal nIndex As Long) As Long
Private Declare PtrSafe Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
Private Declare PtrSafe Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
Private Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal hdc As Long) As Long
Public Sub position(this As Object)
Dim lngCurPos As POINTAPI
Dim DocZero As POINTAPI
Dim PointsPerPixelY As Double
Dim PointsPerPixelX As Double
Dim hdc As Long
hdc = GetDC(0)
PointsPerPixelY = 72 / GetDeviceCaps(hdc, 90)
PointsPerPixelX = 72 / GetDeviceCaps(hdc, 88)
ReleaseDC 0, hdc
DocZero.Y = ActiveWindow.PointsToScreenPixelsY(0)
DocZero.X = ActiveWindow.PointsToScreenPixelsX(0)
GetCursorPos lngCurPos
this.Top = (lngCurPos.Y - DocZero.Y) * PointsPerPixelY
this.Left = (lngCurPos.X - DocZero.X) * PointsPerPixelX
End Sub
This worked well for my little test object, but not quite for my form.
For my form, I changed the last two rows to: (Your Mileage May Vary)
this.Top = (lngCurPos.Y - DocZero.Y) * PointsPerPixelY + this.Height * 2.2
this.Left = (lngCurPos.X - DocZero.X) * PointsPerPixelX - this.Width / 2.5
Then, in my Form1 TextBox, I use Call position(UserForm2) in the MouseMove event to update the position of UserForm2 constantly.
One problem I had, was that I was also calling UserForm2.show, which kept resetting the position, making it jump around.
To get around this, I added another function:
Public Function IsLoaded(formName As String) As Boolean
Dim frm As Object
For Each frm In VBA.UserForms
If frm.Name = formName Then
IsLoaded = True
Exit Function
End If
Next frm
IsLoaded = False
End Function
And then my UserForm1 Code looks like this:
Private Sub TextBox1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
If Not IsLoaded("UserForm2") Then UserForm2.show
Call position(UserForm2)
End Sub
I did not add any code to remove Form2 again, since you have that already.
This was my result:

Related

How to achieve a responsive mouseover effect on Controls in a modeless vba Userform on a large Worksheet

I have the following code, which works perfectly fine on a normal VBA Userform: whenever the mouse hovers anywhere over the label, the color of said label is red, otherwise it's white. This effect is very responsive and makes the label feel very Button-like.
Code of the Userform with 1 label on it:
Dim active As Boolean
Private Sub Label1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
If active = False Then
Label1.BackColor = RGB(255, 0, 0)
active = True
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 active = True Then
Label1.BackColor = RGB(255, 255, 255)
active = False
End If
End Sub
If i change the UserForm to be called modeless, from a module like this:
Sub loader()
UserForm1.Show vbModeless
End Sub
The Mouseover effect still works but it becomes very sluggish and unresponsive. It seems like the refreshrate has gone down massively.
Edit: I found out this problem only appears when the Active Worksheet is a big one, which obviously slows everything down a bit. The sheet that gives me headaches has around 1000 rows and 50 columns with many cells containing longer strings. I think the sheet itself is around 1MB of Data. Forumlas are set to manual refresh only. I'm on a Laptop with an i7 8550U and 8GB of ram using Office 32 bit.
My question is:
Is it possible to achieve the behaviour of the modal Userform in the modeless one?
I looked for ways to manipulate the refreshrate of a modeless Userform but couldn't find anything helpful.
An alternative solution would be to make scrolling in the Worksheet possible while the Userform is shown in modal mode.
Another solution might be making the UserForm modal while the mouse is on it and modeless once the mouse leaves a certain area (the UserForm borders). Is that possible?
Solution 1 - recommended
Add the following code to your UserForm:
Option Explicit
#If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
#Else
Private Declare Sub Sleep Lib "kernel32" (ByVal lngMilliSeconds As Long)
#End If
Dim m_isOpen As Boolean
Private Sub UserForm_Activate()
m_isOpen = True
Do While m_isOpen
Sleep 15 'this correlates to the "refresh rate" of the mouseover effect,
DoEvents 'sleep 100 leads to sluggish behaviour
Loop
End Sub
Private Sub UserForm_Terminate()
m_isOpen = False
End Sub
The mouseover effect should now be responsive again.
Solution 2
This is an implementation of the last of my proposed solution ideas.
It will make the UserForm automatically go modal while the mouse is inside the area of the UserForm and go modeless once the mouse leaves this zone. Just add this code to a plain UserForm:
Option Explicit
#If VBA7 Then
Private Declare PtrSafe Function GetCursorPos Lib "user32" (lpPoint As PointAPI) As Long
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Private Declare PtrSafe Function GetDC Lib "user32" (ByVal hWnd As LongPtr) As LongPtr
Private Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hdc As LongPtr, ByVal nIndex As Long) As Long
Private Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hWnd As LongPtr, ByVal hdc As LongPtr) As Long
#Else
Private Public Declare Function GetCursorPos Lib "user32" (lpPoint As PointAPI) As Long
Private Public Declare Sub Sleep Lib "kernel32" (ByVal lngMilliSeconds As Long)
Private Declare Function GetDC Lib "user32" (ByVal hWnd As Long) As Long
Private Declare Function GetDeviceCaps Lib "Gdi32" (ByVal hDC As Long, ByVal nIndex As Long) As Long
Private Declare Function ReleaseDC Lib "user32" (ByVal hWnd As Long, ByVal hDC As Long) As Long
#End If
Private Const LOGPIXELSX = 88
Private Const LOGPIXELSY = 90
Private Type PointAPI
x As Long
y As Long
End Type
Dim m_modal As Boolean
Dim m_modalityIndicator As Object
Private Function pointsPerPixelX() As Double
Dim hdc As LongPtr 'Used for transforming windows API Mouse-coordinates
hdc = GetDC(0) 'to vba coordinates
pointsPerPixelX = 72 / GetDeviceCaps(hdc, LOGPIXELSX)
ReleaseDC 0, hdc
End Function
Private Function pointsPerPixelY() As Double
Dim hdc As LongPtr 'Used for transforming windows API Mouse-coordinates
hdc = GetDC(0) 'to vba coordinates
pointsPerPixelY = 72 / GetDeviceCaps(hdc, LOGPIXELSY)
ReleaseDC 0, hdc
End Function
Private Function GetX() As Long 'Get current X coordinate of Mouse
Dim n As PointAPI
GetCursorPos n
GetX = n.x
End Function
Private Function GetY() As Long 'Get current y coordinate of Mouse
Dim n As PointAPI
GetCursorPos n
GetY = n.y
End Function
Sub MonitorMouse()
Dim x As Long, y As Long
On Error GoTo userform_closed
Do While True
Sleep 15: DoEvents
x = GetX(): y = GetY()
With Me
If m_modal Then
If x < .left / pointsPerPixelX() Or _
x > (.left + .Width) / pointsPerPixelX() Or _
y < .top / pointsPerPixelY() Or _
y > (.top + .Height) / pointsPerPixelY() Then
.Hide
.show vbModeless
m_modal = False
End If
Else
If x > .left / pointsPerPixelX() And _
x < (.left + .Width) / pointsPerPixelX() And _
y > .top / pointsPerPixelY() And _
y < (.top + .Height) / pointsPerPixelY() Then
.Hide
m_modal = True
.show
Exit Sub
End If
End If
End With
Loop
Exit Sub
userform_closed:
err.Clear: On Error GoTo 0
End Sub
Private Function isFormModeless() As Boolean
On Error GoTo EH
Me.show vbModeless: isFormModeless = True
Exit Function
EH:
isFormModeless = False
End Function
Private Sub UserForm_Activate()
If isFormModeless Then
m_modalityIndicator.Caption = "modeless"
Else
m_modalityIndicator.Caption = "modal"
End If
MonitorMouse
End Sub
Private Sub UserForm_Initialize()
Set m_modalityIndicator = Me.Controls.Add("Forms.Label.1", "ModalityIndicator", True)
With m_modalityIndicator
.left = Me.left
.top = Me.top
.Width = Me.Width
.Height = Me.Height
.Font.size = 36
End With
End Sub

InkPicture renders incorrectly when resized at specific Windows Display Scales

Using Excel/VBA I have made an Excel userform containing only an InkPicture control. I have managed to load a picture (Stretch mode), make the form resizable (API calls), resize the inkpicture upon resize. This is all working perfectly well.
I also need to resize the Ink manually, as it does not scale with the InkPicture. This should also be easily implemented with InkPicture1.Renderer.ScaleTransform and it works perfectly well - most of the time!
Problem: When resizing the userform the ScaleTransform function will stop scaling in either horizontal or vertical direction - but only at specific Windows Display Scales: 125%, 175%, 200% and 225% - whereas scaling 100%, 150% and 250% works perfectly.
The change of behavior at different Windows Display Scales is weird and I have looked for driver updates and performance bottlenecks.
I am uncertain if Display Scale only applies to touchscreens.
The have the same problem on both my computers:
- Microsoft Surface Pro 6 (i5), Windows 10, Office 365 - Excel 32bit
- Lenovo Yoga (i7), Windows 10, Office 365 - Excel 64bit.
Both are touchscreens, using onboard Intel Graphics. Running on external monitors makes no change.
I have investigated:
- Windows, Office and all Drivers should be up to date
- Disabling hardware acceleration (not applicable on my computers)
- Alternative code: using inkpicture.resize event instead
- Alternative code: ScaleTransforming one direction at a time
To reproduce the error you need to...
- Create a macro enabled workbook
- Create UserForm (UserForm1)
- Add the InkPicture ActiveX control to the project
- Insert an InkPicture control (InkPicture1)
- Copy VBA code below into the project
Paste into module and run as macro:
Public Sub OpenUserForm1()
UserForm1.Show
End Sub
Paste into userform1 code:
Private Declare PtrSafe Function GetForegroundWindow Lib "user32" () As LongPtr
Private Declare PtrSafe Function GetWindowLongPtr Lib "user32" Alias "GetWindowLongA" (ByVal hWnd As LongPtr, ByVal nIndex As Long) As LongPtr
Private Declare PtrSafe Function SetWindowLongPtr Lib "user32" Alias "SetWindowLongA" (ByVal hWnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As LongPtr) As LongPtr
Private Declare PtrSafe Function DrawMenuBar Lib "user32" (ByVal hWnd As LongPtr) As Long
Dim widthBefore As Double
Dim heightBefore As Double
Private Sub UserForm_Initialize()
Me.InkPicture1.Top = 0
Me.InkPicture1.Left = 0
widthBefore = Me.InkPicture1.Width
heightBefore = Me.InkPicture1.Height
Call DrawForm
End Sub
Private Sub UserForm_Activate()
Call MakeFormMaximizable
End Sub
Private Sub UserForm_Resize()
Call DrawForm
End Sub
Private Sub DrawForm()
If Me.InsideHeight = 0 Or Me.InsideWidth = 0 Then Exit Sub
Me.InkPicture1.Width = Me.InsideWidth
Me.InkPicture1.Height = Me.InsideHeight
Dim hMultiplier As Single, vMultiplier As Single
hMultiplier = Me.InkPicture1.Width / widthBefore
vMultiplier = Me.InkPicture1.Height / heightBefore
' This function messes up!
Me.InkPicture1.Renderer.ScaleTransform hMultiplier, vMultiplier
widthBefore = Me.InkPicture1.Width
heightBefore = Me.InkPicture1.Height
End Sub
Private Sub MakeFormMaximizable()
Dim BitMask As LongPtr
Dim Window_Handle As LongPtr
Dim WindowStyle As LongPtr
Dim Ret As LongPtr
Const GWL_STYLE As Long = -16
Const WS_THICKFRAME As Long = &H40000
Const MAX_BOX As Long = &H10000
Box_Type = MAX_BOX
Window_Handle = GetForegroundWindow()
WindowStyle = GetWindowLongPtr(Window_Handle, GWL_STYLE)
BitMask = WindowStyle Or Box_Type Or WS_THICKFRAME
Ret = SetWindowLongPtr(Window_Handle, GWL_STYLE, BitMask)
Ret = DrawMenuBar(Window_Handle)
End Sub
To get Wanted/Expected behavior:
- Set Graphic Display Scale to 100% (followed by logout/login)
- Open Excel workbook / Open Userform
- Draw ink on userform
- Resizing the userform will be completely smooth and seamless - perfect!
To get Weird behavior:
- Set Graphic Display Scale to 200% (followed by logout/login)
- Open Excel workbook / Open Userform
- Draw ink on userform
- When resizing the userform the drawn ink no longer follows. It either only scales in one direction, or scales in a direction that is not being scaled.
I hope someone can reproduce the same error/behavior, has had similar experience, has an idea or ideally a fix.
Thanks a lot.
I found a work around. You need to ignore the calculations the InkPicture Control makes on its Rendering Transform Matrix and instead use the Inkpicture.SetViewTransform and the InkTransform.SetTranform functions manually. The code is quite clear and now it will make your UserForm, InkPicture and your Ink resize coordinated and smoothly across all display settings (those tested anyways).
However, the scale factor will not be consistent across display settings - you need to calibrate the coordinate systems! I have done this by creating an initial scaling factor with the function Inkpicture.GetViewTransform. This needs to be called from Form_Init and I have wrapped the code in function GetInitScale in the code below.
Here is the full modified code except UserForm1.show:
Private Declare PtrSafe Function GetForegroundWindow Lib "user32" () As LongPtr
Private Declare PtrSafe Function GetWindowLongPtr Lib "user32" Alias "GetWindowLongA" (ByVal hWnd As LongPtr, ByVal nIndex As Long) As LongPtr
Private Declare PtrSafe Function SetWindowLongPtr Lib "user32" Alias "SetWindowLongA" (ByVal hWnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As LongPtr) As LongPtr
Private Declare PtrSafe Function DrawMenuBar Lib "user32" (ByVal hWnd As LongPtr) As Long
Private Declare PtrSafe Function GetDC Lib "user32" (ByVal hWnd As Long) As Long
Private Declare PtrSafe Function ReleaseDC Lib "user32" (ByVal hWnd As Long, ByVal hdc As Long) As Long
Private Declare PtrSafe Function GetDeviceCaps Lib "Gdi32" (ByVal hdc As Long, ByVal nIndex As Long) As Long
Const HWND_DESKTOP As Long = 0
Const LOGPIXELSX As Long = 88
Const LOGPIXELSY As Long = 90
Const TWIPSPERINCH = 1440
Dim widthBefore As Double, heightBefore As Double
Dim xInitScale As Double, yInitScale As Double
Private Sub UserForm_Initialize()
widthBefore = Me.InkPicture1.Width
heightBefore = Me.InkPicture1.Height
Me.InkPicture1.Top = 0
Me.InkPicture1.Left = 0
Call GetInitScale
Call DrawForm
End Sub
Private Sub UserForm_Activate()
Call MakeFormMaximizable
End Sub
Private Sub UserForm_Resize()
Call DrawForm
End Sub
Private Sub DrawForm()
Me.InkPicture1.Width = Me.InsideWidth
Me.InkPicture1.Height = Me.InsideHeight
Call ScaleInk
End Sub
Private Sub GetInitScale()
Dim aTransform As New InkTransform
Dim eM11 As Single, eM12 As Single, eM21 As Single, eM22 As Single, eDx As Single, eDy As Single
' Remember initial transform to ensure robustness for diffrent display settings
Me.InkPicture1.Renderer.GetViewTransform aTransform
aTransform.GetTransform eM11, eM12, eM21, eM22, eDx, eDy
xInitScale = eM11
yInitScale = eM22
End Sub
Private Sub ScaleInk()
Dim aTransform As New InkTransform
Dim eM11 As Single, eM22 As Single
' Set transformation matrix manually
eM11 = xInitScale * Me.InkPicture1.Width / widthBefore
eM22 = yInitScale * Me.InkPicture1.Height / heightBefore
' Set new Transform
aTransform.SetTransform eM11, 0, 0, eM22, 0, 0
Me.InkPicture1.Renderer.SetViewTransform aTransform
End Sub
Private Sub MakeFormMaximizable()
Dim BitMask As LongPtr
Dim Window_Handle As LongPtr
Dim WindowStyle As LongPtr
Dim Ret As LongPtr
Const GWL_STYLE As Long = -16
Const WS_THICKFRAME As Long = &H40000
Const MAX_BOX As Long = &H10000
Box_Type = MAX_BOX
Window_Handle = GetForegroundWindow()
WindowStyle = GetWindowLongPtr(Window_Handle, GWL_STYLE)
BitMask = WindowStyle Or Box_Type Or WS_THICKFRAME
Ret = SetWindowLongPtr(Window_Handle, GWL_STYLE, BitMask)
Ret = DrawMenuBar(Window_Handle)
End Sub
Hope this becomes useful to someone. It certainly was to me :-)
/Cheers

Show all ControlTipText's in a UserForm on pressing 'Alt'

I'm trying to add ControlTipText's to all MSForms.Control in all Userforms, that can be displayed.
Once all of them are Added, I want to Show all ControlTipText's on pressing 'Alt', so I can easily edit the Controltips on the Excel-Sheet.
Taking Baby-Steps, I first 'tried' to instantly make the ControlTip Visible when adding the Value to it.
What I have now is:
Dim tips As Worksheet
Set tips = Worksheets("CONTROLTIPS")
Dim i As Integer
Dim ctrl As MSForms.Control
i = 0
For Each ctrl In uf.Controls
ctrl.ControlTipText = tips.Cells(i + 3, 2).Value
' ctrl .... ("TIPTEXT").Visible = True ?!?
i = i + 1
Next ctrl
There is no direct way to show the tooltip of a control. The only way is to simulate the mouse hover using APIs. Here is a very basic example. Feel free to amend it to suit your needs.
Prep:
Create a blank userform
Place a commandbutton on the userform and set it's control tip text to whatever you want.
Logic:
When the ALT key is pressed, move the mouse over the relevant control thereby triggering the control tip text
Code
Paste this code in the userform
Option Explicit
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function GetDeviceCaps Lib "Gdi32" _
(ByVal hDC As Long, ByVal nIndex As Long) As Long
Private Declare Function ReleaseDC Lib "user32" _
(ByVal hwnd As Long, ByVal hDC As Long) As Long
Private Declare Function ClientToScreen Lib "user32" _
(ByVal hwnd As Long, lpPoint As POINTAPI) As Long
Private Declare Function SetCursorPos Lib "user32" _
(ByVal X As Long, ByVal Y As Long) As Long
Private Const Xindex = 88
Private Const Yindex = 90
Private Type POINTAPI
X As Long
Y As Long
End Type
'~~> Trap the Alt key in the keydown eveent
Private Sub CommandButton1_KeyDown(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer)
If KeyCode = 18 Then MoveMouseOnTopOf Me, CommandButton1
End Sub
'~~> Simulate mouse hover
Public Sub MoveMouseOnTopOf(frm As Object, ctl As Object)
Dim P As POINTAPI
Dim usrfrmHwnd As Long
Dim hDC As Long
Dim X As Double, Y As Double
hDC = GetDC(0)
X = 72 / GetDeviceCaps(hDC, Xindex)
Y = 72 / GetDeviceCaps(hDC, Yindex)
ReleaseDC 0, hDC
P.X = (ctl.Left + (ctl.Width \ 2)) / X
P.Y = (ctl.Top + (ctl.Height \ 2)) / Y
usrfrmHwnd = FindWindow(vbNullString, frm.Caption)
ClientToScreen usrfrmHwnd, P
SetCursorPos P.X, P.Y
End Sub
You can read and understand about the APIs used above in the AllAPI site.
I want to Show all ControlTipText's on pressing 'Alt'
As far as I can tell, this seems to be the reason for you starting this thread.
The values from control tips are stored in the .ControlTipText function. AFAIK the ALT button will not show all control tips, nor is there a similar alternative. What you CAN do, is show all control tips together with their respective controls in a message box:
Private Sub UserForm_Initialize
Dim ctrl As Control
For Each ctrl In Me.Controls
txt = txt & ctrl.Name & ": " & ctrl.ControlTipText & vbNewLine
Next ctrl
MsgBox txt
End Sub

Hand Cursor for Label vba excel

I'm developing an application with many controls. I want to change the mouse cursor when it passes over a Label. I took a look in the option but there you have a limited choice and not what I want. I tried also to upload a mouse icon but I faced two difficulties: the first one is finding an icon under license cc0 and the second one is that Excel doesn't accept the format that I found. Can you please help? Thanks in advance
You can use the Windows API to change the cursor appearance. I'm assuming this is in an Excel UserForm, so you can use the MouseMove event to know when the mouse is over the label.
Here's the code that you would add in the code behind in a form.
Option Explicit
'Api Declarations
Private Declare Function GetCursorInfo Lib "user32" (ByRef pci As CursorInfo) As Boolean
Private Declare PtrSafe Function LoadCursor Lib "user32" Alias "LoadCursorA" (ByVal hInstance As Long, ByVal lpCursorName As Long) As Long
Private Declare PtrSafe Function SetCursor Lib "user32" (ByVal hCursor As Long) As Long
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
'You can use the default cursors in windows
Public Enum CursorTypes
IDC_ARROW = 32512
IDC_IBEAM = 32513
IDC_WAIT = 32514
IDC_CROSS = 32515
IDC_UPARROW = 32516
IDC_SIZE = 32640
IDC_ICON = 32641
IDC_SIZENWSE = 32642
IDC_SIZENESW = 32643
IDC_SIZEWE = 32644
IDC_SIZENS = 32645
IDC_SIZEALL = 32646
IDC_NO = 32648
IDC_HAND = 32649
IDC_APPSTARTING = 32650
End Enum
'Needed for GetCursorInfo
Private Type POINT
X As Long
Y As Long
End Type
'Needed for GetCursorInfo
Private Type CursorInfo
cbSize As Long
flags As Long
hCursor As Long
ptScreenPos As POINT
End Type
'Event that handles knowing when the mouse is over the control
Private Sub Label1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
AddCursor IDC_HAND
End Sub
'To set a cursor
Private Function AddCursor(CursorType As CursorTypes)
If Not IsCursorType(CursorType) Then
SetCursor LoadCursor(0, CursorType)
Sleep 200 ' wait a bit, needed for rendering
End If
End Function
'To determine if the cursor is already set
Private Function IsCursorType(CursorType As CursorTypes) As Boolean
Dim CursorHandle As Long: CursorHandle = LoadCursor(ByVal 0&, CursorType)
Dim Cursor As CursorInfo: Cursor.cbSize = Len(Cursor)
Dim CursorInfo As Boolean: CursorInfo = GetCursorInfo(Cursor)
If Not CursorInfo Then
IsCursorType = False
Exit Function
End If
IsCursorType = (Cursor.hCursor = CursorHandle)
End Function

VBA- MouseMove to open and close another userform

I have a userform with several label controls, all belong to a class that on mouseover, another userform containing some information about that label will be shown. Now I wanted that form to be closed after mouse leaves the control. Now I am using application.ontime and closing the second form after 2 seconds, which makes the form flickers when the mouse is still over the label. I am wondering if there is anyway better? Here is my code so far.
My Code on the class Module
Private Sub Label1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, _
ByVal X As Single, ByVal Y As Single)
Dim m
On Error Resume Next
If Button = XlMouseButton.xlPrimaryButton And LabelBase.Edit.Caption = "Done" Then
Label1.Left = Label1.Left + X - x_offset
Label1.Top = Label1.Top + Y - y_offset
ElseIf LabelBase.Edit.Caption = "Edit" Then
With CurrentJob
.Caption = "Current Job of " & Label1.Caption
.LBcurr.list = openJobs
.LLast = LastJob
.LClsd = WorksheetFunction.CountIfs(oprecord.Range("e:e"), Label1.Caption, oprecord.Range("f:f"), Date, oprecord.Range("s:s"), "CLOSED")
.LAc = Fix(Right(Label1.Tag, Len(Label1.Tag) - 1) / 24) + 70006
m = WorksheetFunction.VLookup(Label1.Caption, rooster.Range("b:e"), 4, 0)
.LSkill = Right(m, Len(m) - InStr(1, m, " "))
.StartUpPosition = 0
.Top = X + 10
.Left = Y + 10
.Show
End With
With Label1
If X < .Left Or X > (.Left + .Width) Or Y > (.Top + .Height) Or Y < .Top Then closeee
End With
End If
End Sub
My code on the second userform
Private Sub UserForm_Activate()
Application.OnTime Now + TimeValue("00:00:03"), "closeee"
End Sub
Private Sub UserForm_Terminate()
On Error Resume Next
With Me
clearallcontrols
End With
Application.OnTime Now + TimeValue("00:00:03"), "closeee", , False
End Sub
Here is a picture of MAin userform when the Info Form is loaded.
Regards,
M
You don't need a timing ... if you want to work with mouse moves, the code to close the info display form (I suppose its name is CurrentJob) should be fired by a UserForm_MouseMove event on the main form, as when leaving the label, the mouse will next be over the form itself (unless you position labels next to each other without any space - which will make the next comment appear as it should).
Private Sub UserForm_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
CurrentJob.Hide
End Sub
I also recommend to pack the info display code in a private sub of its own to keep the code for the various labels clean.
example: I have a form with Label1, Label2, Label3, Textbox1 and following code:
Private Sub ShowInfo(InfoText As String)
' code to query info and show in seperate window
' make sure window doesn't get focus
' I prefer to use non editable text boxes in my main window
Me.TextBox1 = InfoText
End Sub
Private Sub Label1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
ShowInfo "Mouse is over Label1"
End Sub
Private Sub Label2_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
ShowInfo "Mouse is over Label2"
End Sub
Private Sub Label3_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
ShowInfo "Mouse is over Label3"
End Sub
Private Sub UserForm_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
' this is the exit code
' as here we left all labels
ShowInfo "Mouse is not on a label"
End Sub
Here is the Answer I got on another forum (MrExcel). All the credits go to Jaafar Tribak:
1- Code in a Standard module:
Option Explicit
Private Type POINTAPI
x As Long
y As Long
End Type
Private Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
#If VBA7 Then
#If Win64 Then
Private Declare PtrSafe Function AccessibleObjectFromPoint Lib "oleacc" (ByVal arg1 As LongPtr, ppacc As Any, pvarChild As Variant) As Long
Private Declare PtrSafe Function PtInRect Lib "user32" (lpRect As RECT, ByVal arg2 As LongPtr) As Long
#Else
Private Declare PtrSafe Function AccessibleObjectFromPoint Lib "oleacc" (ByVal lX As Long, ByVal lY As Long, ppacc As IAccessible, pvarChild As Variant) As Long
Private Declare PtrSafe Function PtInRect Lib "user32" (lpRect As RECT, ByVal x As Long, ByVal y As Long) As Long
#End If
Private Declare PtrSafe Function WindowFromAccessibleObject Lib "oleacc" (ByVal pacc As IAccessible, phwnd As LongPtr) As Long
Private Declare PtrSafe Function SetTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr
Private Declare PtrSafe Function KillTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr) As Long
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
Private Declare PtrSafe Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
#Else
Private Declare Function AccessibleObjectFromPoint Lib "Oleacc" (ByVal lX As Long, ByVal lY As Long, ppacc As IAccessible, pvarChild As Variant) As Long
Private Declare Function PtInRect Lib "user32" (lpRect As RECT, ByVal x As Long, ByVal y As Long) As Long
Private Declare Function WindowFromAccessibleObject Lib "oleacc" (ByVal pacc As IAccessible, phwnd As Long) As Long
Private Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Private Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
Private Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long
#End If
Private tCursPos As POINTAPI, tControlRect As RECT
Private bFlag As Boolean
Public Function EnableMouseLeaveEevent(ByVal MainUserForm As UserForm, ByVal Ctrl As Control, ByVal TargetUserForm As Object, Optional ByVal TimeOutInSeconds As Long) As Boolean
Dim oIA As IAccessible
Dim w As Long, h As Long
TargetUserForm.StartUpPosition = 0 '<=== (for testing only .. edit out this line if required)
If bFlag = False Then EnableMouseLeaveEevent = True
Ctrl.Tag = IIf(TimeOutInSeconds > 0, ObjPtr(TargetUserForm) & "*" & TimeOutInSeconds & "*" & Timer, ObjPtr(TargetUserForm))
GetCursorPos tCursPos
#If VBA7 Then
Dim Formhwnd As LongPtr
#If Win64 Then
Dim lngPtr As LongPtr
CopyMemory lngPtr, tCursPos, LenB(tCursPos)
Call AccessibleObjectFromPoint(lngPtr, oIA, 0)
#Else
Call AccessibleObjectFromPoint(tCursPos.x, tCursPos.y, oIA, 0)
#End If
#Else
Dim Formhwnd As Long
Call AccessibleObjectFromPoint(tCursPos.x, tCursPos.y, oIA, 0)
#End If
WindowFromAccessibleObject MainUserForm, Formhwnd
With tControlRect
oIA.accLocation .Left, .Top, w, h, 0&
.Right = w + .Left
.Bottom = h + .Top
End With
SetTimer Formhwnd, CLng(ObjPtr(Ctrl)), 0, AddressOf TimerProc
End Function
Private Sub TimerProc(ByVal hwnd As Long, ByVal MSG As Long, ByVal nIDEvent As Long, ByVal dwTimer As Long)
Static tPrevCurPos As POINTAPI
Dim tCurrCurPos As POINTAPI
Dim sArray() As String
Dim oCtrolObj As Object, oTargetFormObj As Object
Dim lTimeOut As Long, lStartTimer As Long
CopyMemory oCtrolObj, nIDEvent, LenB(nIDEvent)
sArray = Split(oCtrolObj.Tag, "*")
CopyMemory oTargetFormObj, CLng(sArray(0)), LenB(nIDEvent)
If UBound(sArray) = 2 Then
lTimeOut = CLng(sArray(1))
lStartTimer = CLng(sArray(2))
End If
GetCursorPos tCurrCurPos
#If VBA7 Then
Dim lngPtr As LongPtr
#If Win64 Then
CopyMemory lngPtr, tCurrCurPos, LenB(tCurrCurPos)
If PtInRect(tControlRect, lngPtr) = 0 Then
#Else
If PtInRect(tControlRect, tCurrCurPos.x, tCurrCurPos.y) = 0 Then
#End If
#Else
Dim lngPtr As Long
If PtInRect(tControlRect, tCurrCurPos.x, tCurrCurPos.y) = 0 Then
#End If
bFlag = False
KillTimer hwnd, nIDEvent
Unload oTargetFormObj
Debug.Print "Mouse Cursor outside button!"
GoTo Xit
Else
If lTimeOut > 0 Then
With tCurrCurPos
If .x = tPrevCurPos.x And .y = tPrevCurPos.y Then
If Timer - lStartTimer > lTimeOut Then
bFlag = True
lStartTimer = Timer
KillTimer hwnd, nIDEvent
Unload oTargetFormObj
Debug.Print "TimeOut!"
End If
Else
bFlag = False
oCtrolObj.Tag = IIf(lTimeOut > 0, ObjPtr(oTargetFormObj) & "*" & lTimeOut & "*" & Timer, ObjPtr(oTargetFormObj))
GoTo Xit
End If
End With
End If
End If
Xit:
CopyMemory oCtrolObj, 0, LenB(nIDEvent)
CopyMemory oTargetFormObj, 0, LenB(nIDEvent)
GetCursorPos tPrevCurPos
End Sub
2- Code usage in UserForm Module:
Private Sub CommandButton1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal x As Single, ByVal y As Single)
If EnableMouseLeaveEevent(MainUserForm:=Me, Ctrl:=Me.CommandButton1, TargetUserForm:=UserForm2, TimeOutInSeconds:=5) Then ' 5 Sec timeout
UserForm2.Show
End If
End Sub
Thats was a perfect answer.
Links:
VBA- how to have a secondary userform behaviours just like controltiptext
Also a
Demo Excel File

Resources