Closed SanbiVN closed 2 months ago
Hi @SanbiVN ,
Thanks. I won't have access to a computer for the next 2 weeks but I will test this then.
Feel free to write me at cristian.buse@yahoo.com
Cheers
Hi @SanbiVN
Just tested this and for 10000 formulas, the FindWindow
takes over 3 seconds while the current TriggerFastUDFCalculation
approach takes only a few milliseconds.
Thanks!
cristianbuse,
When testing on my computer, with FindWindow sometimes with 40k formulas, it only takes a few milliseconds, but sometimes it takes a few seconds. And I tried your method, it always takes more than 1 to a few seconds. My computer uses a Ryzen 7 CPU.
@SanbiVN
My solution does two things:
1) it forces Excel out of UDF mode to avoid the flickering bug
2) it runs a loop until the Application.CalculationState
is equal to xlDone
For a large number of UDFs, it is critical that the calculation is done outside of UDF mode hence the 2 step solution.
The disadvantage with this approach is that if you happen to have volatile functions in your opened workbooks (e.g. OFFSET
, INDIRECT
, TODAY
etc.) then those will also get calculated and sometimes lead to another bug which keeps the state at xlPending
. So, I never use volatile functions - this could be why you're seeing different results. I am curious, how long does my solution take on your computer, if you run the demo workbook dropdowns, without having other workbooks open?
I have not tested your solution on x32 but on x64 there is an API overhead which might be why it's taking so long for me to run.
@cristianbuse
In your solution: SendInput only executes on the current window. So stopping the calculation with this method will not be correct, when the current window is another window.
The PostMessage command can execute to close the UserForm, when the Application stops calculating when writing the calculation phase message Calculating:(xx Processor(s)): xx%
. When I print LastCallerRange with 40,000 expressions, the last cell address is near the end of 40,000. That means the process of exiting UDF mode is quite late. I see that the stronger the CPU configuration, the later the process of exiting UDF mode.
I don't understand what you mean by "exiting UDF mode". So if you exit earlier, will it be more effective?
With Application.Volatile
, as I understand, it only works on the UDF containing this command when set to TRUE. Any change in the value or cell containing the formula forces that UDF to calculate. With the two test functions, you have set Application.Volatile False
, which seems like a redundant line of code, even when volatile functions are called.
@SanbiVN
SendInput only executes on the current window. So stopping the calculation with this method will not be correct, when the current window is another window.
It does not matter what window. It only needs to do a calculation interrupt.
The PostMessage command can execute to close the UserForm, when the Application stops calculating when writing the calculation phase message Calculating:(xx Processor(s)): xx%. When I print LastCallerRange with 40,000 expressions, the last cell address is near the end of 40,000. That means the process of exiting UDF mode is quite late. I see that the stronger the CPU configuration, the later the process of exiting UDF mode.
Not all PostMessage
calls will be processed. However, if all UDFs have this on, then eventually one will trigger. The rest are ignored anyway. The idea is to run a single FastCalculate
per calculation session, regardless if in that session are 40000 UDF calls or just one. When the number is low, like a single UDF call, then it does not really matter if the trigger does not work - because the whole goal of this repo is to make lots of calls run faster.
I don't understand what you mean by "exiting UDF mode". So if you exit earlier, will it be more effective?
Maybe this article will give you a better idea.
With Application.Volatile, as I understand, it only works on the UDF containing this command when set to TRUE. Any change in the value or cell containing the formula forces that UDF to calculate. With the two test functions, you have set Application.Volatile False, which seems like a redundant line of code, even when volatile functions are called.
All my UDFs have Application.Volatile = False
. The default is True
as per Microsoft. If the line is missing, or is explicitly set to True
then all the cells containing that UDF will recalculate all the time (the definition of volatility), even if the change happened somewhere eles completely unrelated to the cell. If you haven't read this, then you should - I highly reccomend you do.
@cristianbuse
The code below I saw Jaafar share on MrExcel forum. It is capable of triggering VBA function calls even when UDFs are in the process of calculating. I tested with 40,000 expressions, and LastCell is located in a cell not far from the starting position.
The method below turns on event listening for an API call. Here I choose the rtcDoEvents method in VBE7.Dll. The RedirectWaitingProcCalled function will be called, when the DoEvents method is called in VBA.
Do you consider this option feasible in your project.
Before applying the below code for testing, you need to add a condition so that the event is not fired a second time.
onWaitingProcCalled "VBE7.dll", "rtcDoEvents": DoEvents
Option Explicit
#If VBA7 = 0 Then
Private Enum LongLong:[_]:End Enum
Private Enum LongPtr:[_]:End Enum
#End If
#If Win64 Then
Private Const PTR_LEN = 8&
#Else
Private Const PTR_LEN = 4&
#End If
Private Const NULL_PTR As LongPtr = 0
Private Const UPPER_BOUND = PTR_LEN * 1.5
Private Type HOOK_DATA
OriginBytes(0& To UPPER_BOUND - 1) As Byte
HookBytes(0& To UPPER_BOUND - 1) As Byte
pFunc As LongPtr
pHooker As LongPtr
End Type
#If VBA7 Then
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
Private Declare PtrSafe Function SetProp Lib "user32" Alias "SetPropA" (ByVal hwnd As LongPtr, ByVal lpString As String, ByVal hData As LongPtr) As Long
Private Declare PtrSafe Function GetProp Lib "user32" Alias "GetPropA" (ByVal hwnd As LongPtr, ByVal lpString As String) As LongPtr
Private Declare PtrSafe Function RemoveProp Lib "user32" Alias "RemovePropA" (ByVal hwnd As LongPtr, ByVal lpString As String) As LongPtr
Private Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long
Private Declare PtrSafe Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" (ByVal lpModuleName As String) As LongPtr
Private Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr
Private Declare PtrSafe Function VirtualProtect Lib "kernel32.dll" (ByVal lpAddress As LongPtr, ByVal dwSize As LongPtr, ByVal flNewProtect As Long, lpflOldProtect As Long) As Long
Private Declare PtrSafe Function LoadLibrary Lib "kernel32" Alias "LoadLibraryW" (ByVal lpLibFileName As LongPtr) As LongPtr
Private Declare PtrSafe Function EnumThreadWindows Lib "user32" (ByVal dwThreadId As Long, ByVal lpfn As LongPtr, lParam As LongPtr) As Long
Private Declare PtrSafe Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As LongPtr, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
#Else
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As Long)
Private Declare Function SetProp Lib "user32" Alias "SetPropA" (ByVal hwnd As Long, ByVal lpString As String, ByVal hData As Long) As Long
Private Declare Function GetProp Lib "user32" Alias "GetPropA" (ByVal hwnd As Long, ByVal lpString As String) As Long
Private Declare Function RemoveProp Lib "user32" Alias "RemovePropA" (ByVal hwnd As Long, ByVal lpString As String) As Long
Private Declare Function GetCurrentThreadId Lib "kernel32" () As Long
Private Declare Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" (ByVal lpModuleName As String) As Long
Private Declare Function GetProcAddress Lib "kernel32" (ByVal hModule As Long, ByVal lpProcName As String) As Long
Private Declare Function VirtualProtect Lib "kernel32.dll" (ByVal lpAddress As Long, ByVal dwSize As Long, ByVal flNewProtect As Long, lpflOldProtect As Long) As Long
Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryW" (ByVal lpLibFileName As Long) As Long
Private Declare Function EnumThreadWindows Lib "user32" (ByVal dwThreadId As Long, ByVal lpfn As Long, lParam As Long) As Long
Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
#End If
' _______________________________________ PRIVATE ROUTINES __________________________________________
Private Sub onWaitingProcCalled_test()
onWaitingProcCalled "VBE7.dll", "rtcDoEvents": DoEvents
End Sub
Private Function RedirectWaitingProcCalled() As Long
On Error GoTo ErrHandler
Call stopWaitingProcCalled
Debug.Print "RedirectDelegate "; Timer
Exit Function
ErrHandler:
End Function
Private Sub onWaitingProcCalled(ByVal moduleName$, ByVal ProcName$)
Const PAGE_EXECUTE_READWRITE As Long = &H40&
Dim hData As HOOK_DATA, hmod As LongPtr, OriginProtect As Long, i As Long, h As LongPtr
h = iiiVBEHandle
If GetProp(h, "FuncPtr") Then Call stopWaitingProcCalled
With hData 'LoadLibrary
hmod = GetModuleHandle(moduleName): If hmod = 0 Then If LoadLibrary(StrPtr(moduleName)) = 0 Then Exit Sub
.pFunc = GetProcAddress(hmod, ProcName): If .pFunc = 0 Then Exit Sub
Call SetProp(h, "FuncPtr", .pFunc)
If VirtualProtect(ByVal .pFunc, UPPER_BOUND, PAGE_EXECUTE_READWRITE, OriginProtect) = 0& Then Exit Sub
Call CopyMemory(ByVal VarPtr(.OriginBytes(0&)), ByVal .pFunc, UPPER_BOUND)
For i = 0 To UPPER_BOUND - 1
Call SetProp(h, "OrignPtr" & i, .OriginBytes(i))
Next i
.pHooker = Choose(1&, AddressOf RedirectWaitingProcCalled)
#If Win64 Then
.HookBytes(0&) = &H48
.HookBytes(1&) = &HB8: Call CopyMemory(.HookBytes(2&), .pHooker, PTR_LEN)
.HookBytes(10&) = &HFF
.HookBytes(11&) = &HE0
#Else
.HookBytes(0&) = &H68: Call CopyMemory(.HookBytes(1&), .pHooker, PTR_LEN)
.HookBytes(5&) = &HC3
#End If
Call CopyMemory(ByVal .pFunc, ByVal VarPtr(.HookBytes(0&)), UPPER_BOUND)
End With
End Sub
Private Sub stopWaitingProcCalled()
Dim bytes(0& To UPPER_BOUND - 1) As Byte, i As Long, h As LongPtr
h = iiiVBEHandle
If GetProp(h, "FuncPtr") Then
For i = 0& To UPPER_BOUND - 1
bytes(i) = CByte(GetProp(h, "OrignPtr" & i))
Next i
Call CopyMemory(ByVal GetProp(h, "FuncPtr"), ByVal VarPtr(bytes(0&)), UPPER_BOUND)
Call RemoveProp(h, "FuncPtr")
Call RemoveProp(h, "OrignPtr")
End If
End Sub
Private Function iiiVBEHandle() As LongPtr
Static l As LongPtr
If l = 0 Then EnumThreadWindows GetCurrentThreadId, AddressOf EnumThreadWndProc, l
iiiVBEHandle = l
End Function
Private Function EnumThreadWndProc(ByVal hwnd As Long, lParam As LongPtr) As Long
Dim l1 As Long, h As LongPtr, s1 As String * 100, s$
l1 = GetClassName(hwnd, s1, 100): s = Left$(s1, l1)
Select Case s
Case "wndclass_desked_gsk": lParam = hwnd
End Select
EnumThreadWndProc = True
End Function
I don't like this approach.
Try this:
1) call onWaitingProcCalled "VBE7.dll", "rtcDoEvents"
2) Edit some code OR insert a new module
3) Call DoEvents
This leads to a crash.
I moved away from SetTimer
API and into PostMessage
precisely because crashes can occur. Thank you for the suggestion!
However, if a non-crash solution is available, it makes sense to choose it over the crash solution.
Moreover, I would rather use SetTimer
instead of hooking like in Jaafar's example.
Finally, if some other code is dependant on DoEvents
, then this approach ruins that. It can lead to unexpected side effects
@cristianbuse
onWaitingProcCalled "VBE7.dll", "rtcDoEvents": DoEvents
This line of code calls DoEvents immediately, causing the event to be closed immediately, so the problem you describe is unlikely.
Below is the Timer code, which can be somewhat safer. I based this code on Jaafar's shared code. Jaafar created a virtual callback function, and swapped memory with the main function. You can test them.
(***create new userform with name ZZZZZUnknownHandleIdEvents)
Private Const ProjectName = "VBSafeTimerStateLoss"
Private Const ProjectVersion = "1.0.0"
Option Explicit
#If VBA7 = 0 Then
Private Enum LongLong:[_]:End Enum
Private Enum LongPtr:[_]:End Enum
#End If
#If Win64 Then
Private Const PTR_LEN = 8&
#Else
Private Const PTR_LEN = 4&
#End If
Private Const NULL_PTR As LongPtr = 0
Private Const UPPER_BOUND = PTR_LEN * 1.5
Private Type HOOK_DATA
OriginBytes(0& To UPPER_BOUND - 1) As Byte
HookBytes(0& To UPPER_BOUND - 1) As Byte
pFunc As LongPtr
pHooker As LongPtr
End Type
#If -VBA7 And -Win64 Then
Private Declare PtrSafe Function SetTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As LongPtr, ByVal lpTimerFunc As LongPtr) As LongPtr
Private Declare PtrSafe Function KillTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr) As LongPtr
#ElseIf VBA7 Then
Private Declare PtrSafe Function SetTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As LongPtr, ByVal lpTimerFunc As LongPtr) As Long
Private Declare PtrSafe Function KillTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr) As Long
#Else
Private Declare Function setTimer Lib "user32" Alias "SetTimer" (ByVal HWnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Private Declare Function killTimer Lib "user32" Alias "KillTimer" (ByVal HWnd As Long, ByVal nIDEvent As Long) As Long
#End If
#If VBA7 Then
Private Declare PtrSafe Function IUnknown_GetWindow Lib "shlwapi" Alias "#172" (ByVal pIUnk As Any, ByVal hwnd As LongPtr) As LongPtr
Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (Destination As Any, Source As Any, ByVal Length As LongPtr)
Private Declare PtrSafe Function SetProp Lib "user32" Alias "SetPropA" (ByVal hwnd As LongPtr, ByVal lpString As String, ByVal hData As LongPtr) As Long
Private Declare PtrSafe Function GetProp Lib "user32" Alias "GetPropA" (ByVal hwnd As LongPtr, ByVal lpString As String) As LongPtr
Private Declare PtrSafe Function RemoveProp Lib "user32" Alias "RemovePropA" (ByVal hwnd As LongPtr, ByVal lpString As String) As LongPtr
Private Declare PtrSafe Function getTickCount Lib "kernel32.dll" Alias "GetTickCount" () As Long
Private Declare PtrSafe Function CreateWindowEx Lib "user32" Alias "CreateWindowExA" (ByVal dwExStyle As Long, ByVal lpClassName As String, ByVal lpWindowName As String, ByVal dwStyle As Long, ByVal X As Long, ByVal Y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal hwndParent As LongPtr, ByVal hMenu As LongPtr, ByVal hInstance As LongPtr, lpParam As Any) As LongPtr
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
Private Declare PtrSafe Function GetWindowThreadProcessId Lib "user32" (ByVal hwnd As LongPtr, ByRef lpdwProcessId As Long) As Long
Private Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long
Private Declare PtrSafe Function DestroyWindow Lib "user32" (ByVal hwnd As LongPtr) As Long
Private Declare PtrSafe Function IsWindowEnabled Lib "user32" (ByVal hwnd As LongPtr) As Long
Private Declare PtrSafe Function IsWindow Lib "user32" (ByVal hwnd As LongPtr) As Long
Private Declare PtrSafe Function GetWindow Lib "user32" (ByVal hwnd As LongPtr, ByVal wCmd As Long) As LongPtr
Private Declare PtrSafe Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" (ByVal lpModuleName As String) As LongPtr
Private Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr
Private Declare PtrSafe Function VirtualProtect Lib "kernel32.dll" (ByVal lpAddress As LongPtr, ByVal dwSize As LongPtr, ByVal flNewProtect As Long, lpflOldProtect As Long) As Long
Private Declare PtrSafe Function LoadLibrary Lib "kernel32" Alias "LoadLibraryW" (ByVal lpLibFileName As LongPtr) As LongPtr
Private Declare PtrSafe Function EnumThreadWindows Lib "user32" (ByVal dwThreadId As Long, ByVal lpfn As LongPtr, lParam As LongPtr) As Long
Private Declare PtrSafe Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As LongPtr, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Private Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageW" (ByVal hwnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, lParam As Any) As Long
Private Declare PtrSafe Function GetDlgItem Lib "user32" (ByVal hDlg As LongPtr, ByVal nIDDlgItem As Long) As LongPtr
Private Declare PtrSafe Function GetLastActivePopup Lib "user32" (ByVal hwndOwnder As LongPtr) As LongPtr
#Else
Private Declare Function IUnknown_GetWindow Lib "shlwapi" Alias "#172" (ByVal pIUnk As Any, ByVal hwnd 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 SetProp Lib "user32" Alias "SetPropA" (ByVal hwnd As Long, ByVal lpString As String, ByVal hData As Long) As Long
Private Declare Function GetProp Lib "user32" Alias "GetPropA" (ByVal hwnd As Long, ByVal lpString As String) As Long
Private Declare Function RemoveProp Lib "user32" Alias "RemovePropA" (ByVal hwnd As Long, ByVal lpString As String) As Long
Private Declare Function getTickCount Lib "kernel32.dll" Alias "GetTickCount" () As Long
Private Declare Function CreateWindowEx Lib "user32" Alias "CreateWindowExA" (ByVal dwExStyle As Long, ByVal lpClassName As String, ByVal lpWindowName As String, ByVal dwStyle As Long, ByVal X As Long, ByVal Y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal hwndParent As Long, ByVal hMenu As Long, ByVal hInstance As Long, lpParam As Any) As Long
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function GetWindowThreadProcessId Lib "user32" (ByVal Hwnd As Long, ByRef lpdwProcessId As Long) As Long
Private Declare Function GetCurrentThreadId Lib "kernel32" () As Long
Private Declare Function DestroyWindow Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function IsWindowEnabled Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function IsWindow Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function GetWindow Lib "user32" (ByVal hwnd As Long, ByVal wCmd As Long) As Long
Private Declare Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" (ByVal lpModuleName As String) As LongPtr
Private Declare Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr
Private Declare Function VirtualProtect Lib "kernel32.dll" (ByVal lpAddress As LongPtr, ByVal dwSize As LongPtr, ByVal flNewProtect As Long, lpflOldProtect As Long) As Long
Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryW" (ByVal lpLibFileName As Long) As Long
Private Declare Function EnumThreadWindows Lib "user32" (ByVal dwThreadId As Long, ByVal lpfn As Long, lParam As Long) 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 SendMessage Lib "user32" Alias "SendMessageW" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long
Private Declare Function GetDlgItem Lib "user32" (ByVal hDlg As Long, ByVal nIDDlgItem As Long) As Long
Private Declare Function GetLastActivePopup Lib "user32" (ByVal hwndOwnder As Long) As Long
#End If
#If -VBA7 And -Win64 Then
Private Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongPtrA" (ByVal hwnd As LongPtr, ByVal nIndex As Long) As LongPtr
#ElseIf VBA7 Then
Private Declare PtrSafe Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As LongPtr, ByVal nIndex As Long) As Long
#Else
Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long) As Long
#End If
Private Enum idEventUseDefined
cIdEvent = 103322000
End Enum
Private Sub TestSubNow1()
Call test_userform
End Sub
Private Sub test_userform()
'callProcTimer True
callProcTimer False
Application.OnTime Now + TimeSerial(0, 0, 1), "'" & ThisWorkbook.Name & "'!test_MakeDebug"
End Sub
Sub test_MakeDebug()
Debug.Print 1 / 0
End Sub
Sub test_LoopWhenOnTimer()
Dim i%
While i < 10
DoEvents
Application.Wait Now + TimeSerial(0, 0, 3)
i = i + 1
If i = 3 Then Debug.Print 1 / 0
Debug.Print " Looping: "; i
Wend
End Sub
Private Sub callProcTimer(Optional useWinform As Boolean = True)
Dim h As LongPtr: h = Choose(1, AddressOf ProcTimer)
Call SetNewTimer(h, cIdEvent, 50, 5000, useWinform, True)
Call SetNewTimer(h, cIdEvent + 1, 50, 10000, useWinform, True)
'Debug.Print 1 / 0
'If Not useWinform Then End
End Sub
Private Sub endProcTimer()
Dim h As LongPtr
'killTimer
End Sub
Private Sub ProcTimer(ByVal hwnd As LongPtr, ByVal wMsg As Long, ByVal idEvent As LongPtr, ByVal dwTime As LongPtr)
On Error Resume Next
Static i%, i2%:
Dim b As Boolean: b = GetProp(hwnd, CStr(hwnd)) = 0
If onBreakTimer(hwnd, idEvent, 2) <> 0 Then
If b Then i = 0 Else i2 = 0
Exit Sub
End If
'-------------------------------------------------------
' Code Something
If b Then
i = i + 1
'Debug.Print " Userform: "; idEvent; i, "time: "; Int((dwTime - GetProp(hWnd, "C" & CStr(idEvent))) / 1000), Err, Application.Interactive
Else
i2 = i2 + 1: 'If i2 > 5 Then End
'Debug.Print "NewWindow: "; idEvent; i2, "time: "; Int((dwTime - GetProp(hWnd, "C" & CStr(idEvent))) / 1000), Err, Application.Interactive
End If
Debug.Print "IsWindowEnabled: "; IsWindowEnabled(Application.hwnd); i; i2
'-------------------------------------------------------
If repeatSetTimer(hwnd, idEvent, dwTime) Then
' Code Something
End If
End Sub
'======================================================================================================================
' Safe-Timer And StateLoss Callback
'======================================================================================================================
'-----------------------------------------------------------
' Method: SetNewTimer
'-----------------------------------------------------------
' SetProp Prefix+idEvent:
' Prefix E: Elapse Timer
' Prefix T: Time over
' Prefix X: Main ProcTimer Address
' Prefix P: Fake ProcTimer Address
' Prefix C: current time
' Prefix B: Enabled repeatSetTimer
' Prefix R: callback If VB Reset (StateLoss Callback)
' Prefix S: StateLoss
' Prefix I: idCreateWindow
'-----------------------------------------------------------
Sub SetNewTimer(ByVal ProcAddr As LongPtr, ByVal idEvent As LongPtr, Optional ByVal uElapse As LongPtr, _
Optional ByVal eElapse As LongPtr, Optional useWinform As Boolean = True, _
Optional callbackIfVBReset As Boolean, Optional ByVal createWindowByProcAddr As Boolean)
If idEvent <= 1000 Then Err.Raise 8550001, , "Enter idEvent great than 1000"
Dim h As LongPtr, p As LongPtr, u As LongPtr, s$, a, b, i%
p = ProcAddr: ProcAddr = Choose(1, AddressOf virtualProcTimer)
SwapMemoryAddresses ProcAddr, p
s = CStr(idEvent): u = uElapse
h = iiiVirtualWindow(useWinform, 1, , IIf(createWindowByProcAddr, ProcAddr, 0))
Select Case GetProp(h, "S" & s)
Case 0: SetProp h, "C" & s, getTickCount
Case 1: u = u - getTickCount + GetProp(h, "C" & s): If u < 0 Then u = 0
End Select
a = Array("P", "X", "E", "T", "B", "I", "S", "R")
b = Array(ProcAddr, p, uElapse, eElapse, 1, -createWindowByProcAddr, 0, -callbackIfVBReset)
For i = 0 To UBound(a)
SetProp h, a(i) & s, b(i)
Next
If Not useWinform Then
Dim l As LongPtr, n As LongPtr, o As LongPtr: n = h
Do: l = 0: l = GetProp(h, CStr(n)):
If l <> 0 Then n = l: If o = 0 Then o = l Else If o = l Then Exit Do
Loop Until l = 0
If n <> idEvent Then SetProp h, CStr(n), idEvent ' set multi idEvent
End If
h = SetTimer(h, idEvent, u, ProcAddr)
End Sub
Function repeatSetTimer(ByVal hwnd As LongPtr, ByVal idEvent As LongPtr, Optional ByVal dwTime As LongPtr) As Boolean
On Error Resume Next
Dim t As LongPtr, s$
s = CStr(idEvent): t = GetProp(hwnd, "T" & s) + GetProp(hwnd, "C" & s)
If dwTime <= t And dwTime > 0 And GetProp(hwnd, "B" & s) = 1 Then
Call SetTimer(hwnd, idEvent, GetProp(hwnd, "E" & s), GetProp(hwnd, "P" & s)): repeatSetTimer = True
Else
RemoveAllProp hwnd, idEvent
End If
End Function
Function stopTimer(ByVal ProcAddr As LongPtr, ByVal idEvent As LongPtr, _
Optional useWinform As Boolean, Optional ByVal newHandle As Boolean) As Boolean
On Error Resume Next
Dim h As LongPtr
h = iiiVirtualWindow(useWinform, 1, , IIf(newHandle, ProcAddr, 0))
KillTimer h, idEvent: RemoveAllProp h, idEvent
End Function
Private Sub RemoveAllProp(ByVal hwnd As LongPtr, ByVal idEvent As LongPtr)
On Error Resume Next
Dim h As LongPtr, l As LongPtr, n As LongPtr, s$, i
h = hwnd: s = CStr(idEvent)
For Each i In Array("P", "X", "E", "T", "B", "R", "C", "S", "I")
RemoveProp h, i & s
Next
n = GetProp(h, CStr(h))
If n = idEvent Then SetProp h, CStr(h), GetProp(h, s): Exit Sub
Do: l = GetProp(h, CStr(n)): If l = idEvent Then SetProp h, CStr(n), GetProp(h, s): Exit Do
If l > 0 Then n = l
Loop Until l = 0
RemoveProp h, s
End Sub
Function onBreakTimer(Optional ByVal hwnd As LongPtr, Optional ByVal idEvent As LongPtr, _
Optional ByVal onBreak&, Optional ByVal onDoEvents As Boolean) As Long
On Error Resume Next
KillTimer hwnd, idEvent
Dim h As LongPtr
l:
h = FindWindow("#32770", "Microsoft Visual Basic")
If h = 0 Then
h = FindWindow("#32770", "Microsoft Visual Basic for Applications")
If h <> 0 Then onBreakTimer = 1: GoTo c
Else
If (onBreak And 1) <> 0 Then onBreakTimer = 2:
GoTo c
End If
Err.Clear
Exit Function
c:
If (onBreak And 2) <> 0 Then
Const IDEnd = &H12C0, IDDebug = &H12C1, IDContinue = &H12C2, BM_CLICK = &HF5
Call SendMessage(GetDlgItem(h, IDEnd), BM_CLICK, 0, ByVal 0)
End If
End Function
Private Sub virtualProcTimer()
On Error Resume Next
Dim h As LongPtr, l As LongPtr, ll As LongPtr, o As LongPtr, s$, k%:
Dim h0 As LongPtr, p0 As LongPtr, o0 As LongPtr
h0 = iiiVirtualWindow(0): h = h0: ll = h0
Do While h > 0
l = h
Do:
l = GetProp(h, CStr(l)): If l = 0 Or o = l Then Exit Do
KillTimer h, l: s = CStr(l)
If GetProp(h, "R" & s) = 1 Then
k = k + 1: SetProp h, "S" & s, 1
Call SetNewTimer(GetProp(h, "X" & s), l, GetProp(h, "E" & s), GetProp(h, "T" & s), False, True, GetProp(h, "I" & s))
Else
RemoveAllProp h, l
End If
If o = 0 Then o = l
Loop Until l = 0
p0 = GetProp(h0, "VW" & CStr(h))
If k > 0 Then ll = h Else If h <> h0 Then SetProp h0, "VW" & ll, p0: DestroyWindow h
h = p0: If h = 0 Or o0 = h Then Exit Do
If o0 = 0 Then o0 = h
Loop
End Sub
Sub DestroyAllVirtualWindow()
On Error Resume Next
Dim h As LongPtr, h0 As LongPtr, o0 As LongPtr
h0 = iiiVirtualWindow(0): h = h0
Do While h > 0
If h <> h0 Then If IsWindow(h) <> 0 Then DestroyWindow h
h = GetProp(h0, "VW" & CStr(h)): If h = 0 Or o0 = h Then Exit Do
If o0 = 0 Then o0 = h
Loop
If h0 > 0 Then If IsWindow(h0) <> 0 Then DestroyWindow h0
End Sub
Function iiiVirtualWindow(Optional useWinform As Boolean = True, Optional createnew%, Optional laHwnd As LongPtr, Optional ByVal idCreateWindow As LongPtr) As LongPtr
If useWinform Then
' Use Userform to make handle
IUnknown_GetWindow ZZZZZUnknownHandleIdEvents, VarPtr(iiiVirtualWindow)
Else
' Use CreateWindow to make handle
Dim s$, h As LongPtr, h0 As LongPtr
If idCreateWindow <= 0 Then s = Choose(1, AddressOf virtualProcTimer) Else h0 = iiiVirtualWindow(0, 1, , 0): s = idCreateWindow
s = "_____XLVirtualWindow[" & s & "]": h = FindWindow("STATIC", s)
Select Case createnew
Case 1: If h = 0 Then GoTo n
Case -1, 2:
If h > 0 Then DestroyWindow h: h = 0
If createnew = 2 Then
n: h = CreateWindowEx(0, "STATIC", s, 0, 0, 0, 0, 0, 0, 0, GetWindowLong(Application.hwnd, (-6)), ByVal 0&)
If idCreateWindow <> 0 Then
Dim l As LongPtr, n As LongPtr, o As LongPtr: n = h0
Do: l = 0: l = GetProp(h0, "VW" & CStr(n)):
If l <> 0 Then n = l: If o = 0 Then o = l Else If o = l Then Exit Do
Loop Until l = 0
If n <> h Then SetProp h0, "VW" & CStr(n), h ' set multiHandle
End If
End If
End Select
iiiVirtualWindow = h
End If
End Function
Private Function iiiVBEHandle() As LongPtr
Static l As LongPtr
If l = 0 Then EnumThreadWindows GetCurrentThreadId, AddressOf EnumThreadWndProc, l
iiiVBEHandle = l
End Function
Private Function EnumThreadWndProc(ByVal hwnd As Long, lParam As LongPtr) As Long
Dim l1 As Long, h As LongPtr, s1 As String * 100, s$
l1 = GetClassName(hwnd, s1, 100): s = Left$(s1, l1)
Select Case s
Case "wndclass_desked_gsk": lParam = hwnd
End Select
EnumThreadWndProc = True
End Function
Private Function SwapMemoryAddresses(ByVal Addrss1 As LongPtr, ByVal Addrss2 As LongPtr)
Call CopyMemory(ByVal Addrss1 + PTR_LEN * 6& + 4&, ByVal Addrss2 + PTR_LEN * 6& + 4&, PTR_LEN)
End Function
@SanbiVN
This line of code calls DoEvents immediately, causing the event to be closed immediately, so the problem you describe is unlikely.
Sometimes state is lost while running a UDF, especially when a workbook is opened. If this leaves the application in an unwanted state (e.g. hooked DoEvents) then it's a problem. You are correct, it's unlikely, but can happen - so I rather not use it.
Jaafar created a virtual callback function, and swapped memory with the main function.
The swapping is only done to fix the stack parameters on x64. I know because I was the one that implemented this and explained to Jaafar here. I am glad he is succesfully using the workaround.
However, like I said, timers are still my second option because it's still not crash proof even with the fixed stack. Pressing Stop in the IDE or running End
while within the scope of the timer callback still leads to a crash and the only way to fix that is to have an assembly trampoline method. My PostMessage
approach is 100% crash free so I like this even though in my testing it's 2x-5x slower than timers.
Thanks for all the info!
@cristianbuse
Your method, once when I tested with 40k expressions, Excel on my computer crashed automatically. I only opened your file. I typed another UDF into the cell with Volatile as true. In my Excel there is an Add-in running, containing code to catch the WorksheetChange event.
The code above I built myself, I just saw Jaafar doing memory swap with virtual function
I have a pretty bold project idea, you can listen to my presentation. I am doing VBA code parsing and analysis to achieve the following goals:
Because, I used to spend a lot of time copying codes from different projects into new projects. I spent a lot of time debugging and fixing bugs. So I want to build this project. I have successfully built the project to the stage of separating each syntax in VBA code. Now it is time to parse. There is a lot of work to do. I have to spend more time to complete it.
If you have time, I'll create a new Github repository for the project for you to check out.
@SanbiVN, I am not sure if I have understood your project correct, but "parsing of VBA code" sounds like something that is/will be done in https://github.com/rubberduck-vba/Rubberduck or https://github.com/rubberduck-vba/Rubberduck3 respectively as well. Do you know it?
@SanbiVN
I tested the current PostMessage
approach a lot and I was never able to crash the app. Can you please provide me with a list of steps so that I can replicate the crash? Thanks!
Regarding your parsing project, I am afraid I won'd have time to get involved at least not for the next 4-6 months. Hopefully it all goes to plan. Thanks!
@cristianbuse I will try a few more times to see what happens.
@Mo-Gul Yes. I am doing this like Rubberduck. But I choose to do it with just VBA code. And target my own goals as I mentioned.
@SanbiVN
Just occured to me. Do you use the ForceVBALoadFromSource
hotfix? Sometimes, when you open a project that was saved in a previous app version, you get a crash because the app fails to recompile.
All you need to do is to add a registry entry. For example, for Excel:
"Computer\HKEY_CURRENT_USER\Software\Microsoft\Office\" & Application.Version & "\Excel\Options\"
you add a key called ForceVBALoadFromSource
with a DWORD
value of 1
.
@cristianbuse
The first time I heard this from you, I have projects with a lot of VBA code, many times I crashed Excel as soon as I opened the file. The file was corrupted and could not be opened again. I was forced to choose a backup copy of the file to start over.
I added the key to the registry, time will tell if the problem continues.
@SanbiVN
I added the key to the registry, time will tell if the problem continues.
I've been using this over 6 months now and I had zero crashes since. I'm reffering to crashes when opening the documents.
Hi @cristianbuse You just need to call FindWindow API in the function and the calculation process will be fast, you try to check it.
I would really like a chance to chat with you via social media, what do you think?