How to: Animation (triple dot) on progress bar during working macro - excel

I have a progress bar, which is showing progress in percentage and on 'animated' rectangle.
I know how to show the progress of the macro based on "marks" in code, that's not the case. Example of code called as that "mark" in code to change the percentage on progress bar:
Sub progress(pctCompl As Long)
Progression.Text.Caption = pctCompl & "% Completed"
Progression.Bar.Width = pctCompl * 2
DoEvents 'update the userform
End Sub
I wonder if it's possible to do additional animation behind "Please wait" on that progress bar - triple dot:1 dot, 1 second pause, 2 dots, 1 second pause, 3 dots, 1 second pause. This is 1 loop for that animation.
I was trying to do something, mostly I was achieving infinite loops or macro doing nothing but that triple dot animation, which was freezing Excel application.
Private Sub UserForm_Activate()
Do Until Progression.Bar.Width = 200
Progression.Text2.Caption = "Please wait."
Progression.Repaint
Application.Wait Now + TimeValue("0:00:01")
Progression.Text2.Caption = "Please wait.."
Progression.Repaint
Application.Wait (Now + TimeValue("0:00:01"))
Progression.Text2.Caption = "Please wait..."
Progression.Repaint
Application.Wait (Now + TimeValue("0:00:01"))
Loop
End Sub
I thought it is good place to ask that kind of questions - is it possible and if yes how to achieve that?

I sometimes have an image that I like to 'animate' on a UserForm as a progress indicator, and I use the Win API timer for that. The code below may be a little 'overkill' for your needs, as image changes need to be triggered either by an event or by Repaint, the latter of which can cause flicker. I believe Labels update as soon as the property value changes. If this is the case then you could leave out the listener class shown below and adjust the code accordingly.
With the above caveat, a skeleton implementation could look like this:
Userform code
Note: my userform has a start button, a stop button and one label, called lblWait.
Option Explicit
Private WithEvents mTimerListener As cTimerListener
Private Sub btnStart_Click()
HandleStartTimer mTimerListener
End Sub
Private Sub btnStop_Click()
HandleStopTimer
End Sub
Private Sub mTimerListener_DotCountIncremented(count As Long)
Me.lblWait = "Please wait" & String(count, ".")
End Sub
Private Sub UserForm_Initialize()
Set mTimerListener = New cTimerListener
End Sub
Class code
Note: I've called this class cTimerListener.
Option Explicit
Public Event DotCountIncremented(count As Long)
Private mDotCount As Long
Public Property Let DotCount(RHS As Long)
mDotCount = RHS
If mDotCount > 3 Then mDotCount = 0
RaiseEvent DotCountIncremented(mDotCount)
DoEvents
End Property
Public Property Get DotCount() As Long
DotCount = mDotCount
End Property
And Module code
Option Explicit
#If VBA7 Then
Private Declare PtrSafe Function SetTimer Lib "user32" ( _
ByVal HWnd As LongPtr, _
ByVal nIDEvent As Long, _
ByVal uElapse As Long, _
ByVal lpTimerFunc As LongPtr) As Long
Private Declare PtrSafe Function KillTimer Lib "user32" ( _
ByVal HWnd As LongPtr, _
ByVal nIDEvent As Long) As Long
#Else
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
#End If
Private mTimerId As Long
Private mTimerListener As cTimerListener
Public Sub HandleStartTimer(timerListener As cTimerListener)
Set mTimerListener = timerListener
#If VBA7 Then
mTimerId = SetTimer(0&, 0&, 0.5 * 1000, AddressOf TimerProc64)
#Else
mTimerId = SetTimer(0&, 0&, 0.5 * 1000, AddressOf TimerProc32)
#End If
End Sub
Public Sub HandleStopTimer()
KillTimer 0&, mTimerId
End Sub
#If VBA7 Then
Private Sub TimerProc64(ByVal HWnd As LongPtr, ByVal uMsg As Long, _
ByVal nIDEvent As Long, ByVal dwTimer As Long)
TimerProc
End Sub
#Else
Private Sub TimerProc32(ByVal HWnd As Long, ByVal uMsg As Long, _
ByVal nIDEvent As Long, ByVal dwTimer As Long)
TimerProc
End Sub
#End If
Private Sub TimerProc()
If Not mTimerListener Is Nothing Then
With mTimerListener
.DotCount = .DotCount + 1
End With
End If
End Sub

Related

Detecting Lost Focus in Excel Application, Workbook or Worksheet

Switching to another application via the system ALT-Tab hotkey, while working in MS Excel on MS-Windows causes Excel to lose the keyboard focus. How to detect this?
The Deactivate or WindowDeactivate events for the objects: Application or Workbook or Worksheet objects do not fire when MS Excel loses focus this way (of course, because losing the focus is not synonymous with Deactivating the window)
Try this code, please. I found it somewhere to the internet, three years ago and only adapted to serve my needs. For instance, it could not be stopped because of a wrong declaration of UnhookWinEvent API. Take care to not monitor the focus lost or got by using a MsgBox. In this way, pressing 'OK' the focus will be received again and you will be in an infinite loop. The focus status will be returned in the active sheet, range "A1" (received focus), respectively, "A2" (lost focus):
Copy the next code on top of a module (in the declarations area):
Option Explicit
Private Const EVENT_SYSTEM_FOREGROUND = &H3&
Private Const WINEVENT_OUTOFCONTEXT = 0
Private Declare PtrSafe Function SetWinEventHook Lib "user32.dll" (ByVal eventMin As Long, _
ByVal eventMax As Long, ByVal hmodWinEventProc As LongLong, ByVal pfnWinEventProc As LongLong, _
ByVal idProcess As Long, ByVal idThread As Long, ByVal dwFlags As Long) As Long
Private Declare PtrSafe Function GetCurrentProcessId Lib "kernel32" () As Long
Private Declare PtrSafe Function GetWindowThreadProcessId Lib "user32" (ByVal hWnd As Long, lpdwProcessId As Long) As Long
Private Declare PtrSafe Function UnhookWinEvent Lib "user32.dll" (ByVal hWinEventHook As Long) As Long
Private handlColl As Collection
Copy the next code inside the module:
Public Sub StartMonitoring() 'it can be called from a Workbook/Worksheet event
StartFocusMonitoring
End Sub
Public Function StartFocusMonitoring() As Long
If handlColl Is Nothing Then Set handlColl = New Collection
StartFocusMonitoring = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, 0&, _
AddressOf WinEventFunc, 0, 0, WINEVENT_OUTOFCONTEXT)
handlColl.aDD StartFocusMonitoring
End Function
Public Sub StopEventHook(lHook As Long)
Dim LRet As Long
If lHook = 0 Then Exit Sub`
LRet = UnhookWinEvent(lHook)
End Sub
Public Sub StopMonitoring() 'it must be called manualy or by an event when need to stop monitoring...
'it did not work until I changed the StopEventHook declaration, using ByVal instead of ByRef
Dim vHook As Variant, lHook As Long
For Each vHook In handlColl
lHook = vHook
StopEventHook lHook
Next vHook
End Sub
Public Function WinEventFunc(ByVal HookHandle As Long, ByVal LEvent As Long, _
ByVal hWnd As Long, ByVal idObject As Long, ByVal idChild As Long, _
ByVal idEventThread As Long, ByVal dwmsEventTime As Long) As Long
'In case of an error the application will crush. So, bypassing the error is good to be done...`
On Error Resume Next
Dim thePID As Long`
If LEvent = EVENT_SYSTEM_FOREGROUND Then
GetWindowThreadProcessId hWnd, thePID
If thePID = GetCurrentProcessId Then
'Do not use here a MsgBox, because after pressing OK Excel application
'will receive focus and you will stay in an infinite loop...
Application.OnTime Now, "Event_GotFocus"
Else
Application.OnTime Now, "Event_LostFocus"
End If
End If
On Error GoTo 0
End Function
Public Sub Event_GotFocus()
Range("a1").value = "Received Focus"
Range("a2").value = ""
End Sub
Public Sub Event_LostFocus()
Range("a2").value = "Lost focus"
Range("a1").value = ""
End Sub
You must start monitoring from StartMonitoring Sub which can be called directly or through an event (Workbook_Open, for instance).
The monitoring can be stopped calling StopMonitoring Sub...

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

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

Using timer to get Excel title

I have following code to get title of current opened excel file this code working fine. I use timer to every 10 seconds if title change then add new title in list1.
So question is there any method or event to detect if title change then my code work otherwise it not work not check. timer check every 10 seconds my pc work slow if I run this code
Private Const GW_HWNDNEXT = 2
Private Declare Function GetWindow Lib "user32" (ByVal hWnd As Long, _
ByVal wCmd As Long) As Long
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" _
(ByVal hWnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
(ByVal hWnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
Sub ListWins(Optional Title = "*", Optional Class = "*")
Dim hWndThis As Long
hWndThis = FindWindow(vbNullString, vbNullString)
While hWndThis
Dim sTitle As String, sClass As String
sTitle = Space$(255)
sTitle = Left$(sTitle, GetWindowText(hWndThis, sTitle, Len(sTitle)))
sClass = Space$(255)
sClass = Left$(sClass, GetClassName(hWndThis, sClass, Len(sClass)))
If sTitle Like Title And sClass Like Class Then
Debug.Print sTitle, sClass
List1.AddItem (sTitle)
End If
hWndThis = GetWindow(hWndThis, GW_HWNDNEXT)
Wend
End Sub
Private Sub Timer1_Timer()
ListWins "*.xls*"
End Sub
The answer is No. AFAIK, No there is no event as such in vb6 which will trap the title change in Excel or any other window. Also unfortunately 10 second timer might not be good. What happens if the title changes every 2 seconds? It will not retrieve all the titles
However try this alternative which does not use the Timer Control. See if your pc is still slow...
Sub Sample()
'
' ~~> Rest of your code
'
Wait 2 '<~~ Wait for 2 seconds
'
' ~~> Rest of your code
'
End Sub
Private Sub Wait(ByVal nSec As Long)
nSec = nSec + Timer
While nSec > Timer
DoEvents
Wend
End Sub
You can use the Excel COM API to do this. Unfortunately, there is no way of getting the Excel window title - but you could easily manufacture it by appending " - Microsoft Excel". Use the FullName property if you want the complete path.
Option Explicit
Private WithEvents m_oApplication As Excel.Application
Private Sub Command_Click()
' Get a reference to the FIRST instance of the Excel application.
Set m_oApplication = GetObject(, "Excel.Application")
End Sub
Private Sub m_oApplication_NewWorkbook(ByVal Wb As Excel.Workbook)
List1.AddItem Wb.Name
End Sub
Private Sub m_oApplication_WorkbookAfterSave(ByVal Wb As Excel.Workbook, ByVal Success As Boolean)
'List1.AddItem "WorkbookAfterSave: " & Wb.FullName
List1.AddItem Wb.Name
End Sub
Private Sub m_oApplication_WorkbookOpen(ByVal Wb As Excel.Workbook)
List1.AddItem Wb.Name
End Sub

Controlling Excel via Midi controller

I've got one of these
(source: netdna-cdn.com)
and wanted to use the sliders on it to control Excel, just like one of the Excel form control scroll bars.
I've managed to modify this code for VBA, but it is extremely unstable.
Can anyone help me stabilize it? I think the function MidiIn_Event may crash if it doesn't return fast enough, but I may be wrong.
Thanks in advance.
Public Const CALLBACK_FUNCTION = &H30000
Public Declare Function midiInOpen Lib "winmm.dll"
(lphMidiIn As Long,
ByVal uDeviceID As Long, ByVal dwCallback As Any,
ByVal dwInstance As Long, ByVal dwFlags As Long) As Long
Public Declare Function midiInClose Lib "winmm.dll"
(ByVal hMidiIn As Long) As Long
Public Declare Function midiInStart Lib "winmm.dll"
(ByVal hMidiIn As Long) As Long
Public Declare Function midiInStop Lib "winmm.dll"
(ByVal hMidiIn As Long) As Long
Public Declare Function midiInReset Lib "winmm.dll"
(ByVal hMidiIn As Long) As Long
Private ri As Long
Public Sub StartMidiFunction()
Dim lngInputIndex As Long
lngInputIndex=0
Call midiInOpen(ri, lngInputIndex, AddressOf MidiIn_Event,
0, CALLBACK_FUNCTION)
Call midiInStart(ri)
End Function
Public Sub EndMidiRecieve()
Call midiInReset(ri)
Call midiInStop(ri)
Call midiInClose(ri)
End Sub
Public Function MidiIn_Event(ByVal MidiInHandle As Long,
ByVal Message As Long, ByVal Instance As Long,
ByVal dw1 As Long, ByVal dw2 As Long) As Long
'dw1 contains the midi code
If dw1 > 255 Then 'Ignore time codes
Call MsgBox(dw1) 'This part is unstable
End If
End Function
The problem is probably MsgBox:
Since the MIDI events use callbacks they are most likely run from another thread. VBA is inherently single-threaded (see e.g. Multi-threading in VBA), so trying to show a modal dialog from another thread will likely cause issues (undefined behavior, crash, anything else...)
MIDI usually triggers huge amounts of events (the tiniest movement of a slider or knob would trigger an event), so moving something a noticeable amount may cause hundreds of events. Showing a dialog (requiring an OK click) at each event could be a problem.
For testing, try to replace Call MsgBox(dw1) with Debug.Print dw1 so that the values are just printed in the Immediate Window, which should be much more stable. If you are trying to execute some simple action (e.g. update the value in a cell, scroll the window) you may be able to get away with it as long as each call to MidiIn_Event completes before the next event.
A much more complex but stable solution could be to push data points onto a queue in the event handler, and use a repeating timer in VBA that pops items from the queue and executes some action on the VBA thread.
This is so fantasticly cool :D
but the message box as mentioned above will kill it, but removing the messagebox will probably not help that much. You want to minimize the abount of traffic to excel too because the vba->excel will not be instantanious.
Soooo the solution would be
on workbook start macro
Public lngMessage As String
Private Sub Workbook_Open()
alertTime = Now + TimeValue("00:00:01")
Application.OnTime alertTime, "EventMacro"
End Sub
Sub EventMacro()
ActiveSheet.Cells(1, 1).Value = lngMessage
alertTime = Now + TimeValue("00:00:01")
End Sub
Public Function MidiIn_Event(ByVal MidiInHandle As Long, ByVal Message As Long, ByVal Instance As Long, ByVal dw1 As Long, ByVal dw2 As Long) As Long
'dw1 contains the midi code
If dw1 > 255 Then 'Ignore time codes
lngMessage = dw1 'This part is now happy
End If
End Function
You need a general Function that processes the data given by the MidiIn_Event one, in my example bellow that function is the runClock() one.
I did this that is able the uses the status bar to count the keys and clock type of messages.
Option Explicit
Private Const CALLBACK_FUNCTION = &H30000
'MIDI Functions here: https://learn.microsoft.com/en-us/windows/win32/multimedia/midi-functions
#If Win64 Then
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)
Private Declare PtrSafe Function timeGetTime Lib "winmm.dll" () As Long
'For MIDI device INPUT
Private Declare PtrSafe Function midiInOpen Lib "winmm.dll" (lphMidiIn As LongPtr, ByVal uDeviceID As LongPtr, ByVal dwCallback As LongPtr, ByVal dwInstance As LongPtr, ByVal dwFlags As LongPtr) As Long
Private Declare PtrSafe Function midiInClose Lib "winmm.dll" (ByVal hMidiIn As LongPtr) As Long
Private Declare PtrSafe Function midiInStart Lib "winmm.dll" (ByVal hMidiIn As LongPtr) As Long
Private Declare PtrSafe Function midiInStop Lib "winmm.dll" (ByVal hMidiIn As LongPtr) As Long
Private Declare PtrSafe Function midiInReset Lib "winmm.dll" (ByVal hMidiIn As LongPtr) As Long
#Else
Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Private Declare Function timeGetTime Lib "winmm.dll" () As Long
'For MIDI device INPUT
Private Declare Function midiInOpen Lib "winmm.dll" (lphMidiIn As Long, ByVal uDeviceID As Long, ByVal dwCallback As Long, ByVal dwInstance As Long, ByVal dwFlags As Long) As Long
Private Declare Function midiInClose Lib "winmm.dll" (ByVal hMidiIn As Long) As Long
Private Declare Function midiInStart Lib "winmm.dll" (ByVal hMidiIn As Long) As Long
Private Declare Function midiInStop Lib "winmm.dll" (ByVal hMidiIn As Long) As Long
Private Declare Function midiInReset Lib "winmm.dll" (ByVal hMidiIn As Long) As Long
#End If
#If Win64 Then
Private mlngCurDevice As Long
Private mlngHmidi As LongPtr
#Else
Private mlngCurDevice As Long
Private mlngHmidi As Long
#End If
Private ClockTicks As Integer
Private Notes As Integer
Private Looper As Long
Private LongMessage As Long
Private actualTime As Long
Public Sub runClock()
'When canceled become able to close opened Input devices (For ESC press)
On Error GoTo handleCancel
Application.EnableCancelKey = xlErrorHandler
With Application
.Calculation = xlCalculationManual
.ScreenUpdating = False
'.DisplayStatusBar = False
'.EnableEvents = False
End With
mlngCurDevice = 8 'My Device is 8 but yours is 0
Notes = 0
Looper = 0
'Open Input Device
Call midiInOpen(mlngHmidi, mlngCurDevice, AddressOf MidiIn_Event, 0, CALLBACK_FUNCTION)
'Ends only when Status is different from 0
Do While Notes < 10
'Reset Status count
ClockTicks = 0
'Begins lissinting the MIDI input
Call midiInStart(mlngHmidi)
'Loops until the right message is given <= 255 and > 0
Do While ClockTicks < 1000 And Notes < 10
'Sleep if needed
Sleep 10
Application.StatusBar = "Looper=" & Looper & " | Notes=" & Notes & " | ClockTicks=" & ClockTicks & " | Message=" & LongMessage
Looper = Looper + 1
'DoEvents enables ESC key
If Abs(timeGetTime - actualTime) > 3000 Then
DoEvents
actualTime = timeGetTime
End If
Loop
'Ends lisingting the MIDI input
Call midiInReset(mlngHmidi)
Call midiInStop(mlngHmidi)
Loop
'Closes Input device
Do While midiInClose(mlngHmidi) <> 0
Loop
With Application
.Calculation = xlCalculationAutomatic
.ScreenUpdating = True
.DisplayStatusBar = True
.EnableEvents = True
End With
MsgBox "ENDED WITH SUCCESS", , "Message:"
'Close all opened MIDI Inputs when canceled (ESC key pressed)
handleCancel:
If Err.Number = 18 Then
'Ends lisingting the MIDI input
Call midiInReset(mlngHmidi)
Call midiInStop(mlngHmidi)
Do While midiInClose(mlngHmidi) <> 0
Loop
With Application
.Calculation = xlCalculationAutomatic
.ScreenUpdating = True
.DisplayStatusBar = True
.EnableEvents = True
End With
MsgBox "ENDED WITH SUCCESS", , "Message:"
End If
End Sub
Private Function MidiIn_Event(ByVal mlngHmidi As Long, ByVal Message As Long, ByVal Instance As Long, ByVal dw1 As Long, ByVal dw2 As Long) As Long
'The value 963 is the MIM_DATA concerning regular MIDI messages
If Message = 963 Then
LongMessage = Message
If dw1 > 255 Then
Notes = Notes + 1
Else
ClockTicks = ClockTicks + 1
End If
End If
End Function
The issue comes when ESC key is presses while receiving MIDI data, like clock sync, for some reason, and despite everything else works well, the ESC key many times crashes the script. However if you don't use the ESC key during input MIDI messages you will not have this problem.
Nevertheless I would like to know why pressing the ESC key while receiving clock signals crashes the script.
You just need to adapt the global variable to your needs.
Hope I have helped.

Resources