CTD in Excel VBA - excel

My macro is causing excel to crash to desktop with no debug or error message, the whole application shuts down, closing any open instances of excel.
The code that I've written so far is pretty light, so I have no idea what is causing the crash.
The crash appears to occur after StartGame() has finished. I assume it's being caused by the Timer which runs Main() every 50ms as the CTD exclusively happens after End Sub in StartGame().
I've included all the code below.
Main Module
Option Explicit
Public Sub StartGame()
InitialiseGame
InitialiseTimer
GameRunning = True
End Sub
Public Sub Main()
If GameInput.TabIsPressed Then TerminateTimer
If Not GameRunning Then Exit Sub
Graphics.DrawSquare
End Sub
Private Sub InitialiseGame()
Set GameInput = New ClassGameInput
Set Graphics = New ClassGraphics
Set Square = New ClassSquare
End Sub
Timer Module
Option Explicit
Private Const MILLISECONDS_IN_SECOND As Integer = 1000
Private Const GAME_TICKS_PER_SECOND As Integer = 20
Private GameTimerID As Long
Public Sub InitialiseTimer()
Dim GameTimerInterval As Double
GameTimerInterval = MILLISECONDS_IN_SECOND / GAME_TICKS_PER_SECOND
Sleep (500)
GameTimerID = SetTimer(0, 0, GameTimerInterval, AddressOf Main)
End Sub
Public Sub TerminateTimer()
If GameTimerID <> 0 Then
KillTimer 0, GameTimerID
GameTimerID = 0
GameRunning = False
End If
Set GameInput = Nothing
Set Graphics = Nothing
Set Square = Nothing
End Sub
Public Declarations Module
Option Explicit
#If Win64 Then
Public Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Public Declare PtrSafe Function SetTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr, ByVal uElapse As Long, ByVal lpTimerFunc As LongPtr) As LongPtr
Public Declare PtrSafe Function KillTimer Lib "user32" (ByVal hwnd As LongPtr, ByVal nIDEvent As LongPtr) As Long
Public Declare Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer
#Else
Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Public Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Public Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
Public Declare PtrSafe Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer
#End If
Public Const BORDER_START_X As Integer = 4
Public Const BORDER_START_Y As Integer = 4
Public Const BORDER_END_X As Integer = 185
Public Const BORDER_END_Y As Integer = 73
Public Const SQUARE_SIZE As Integer = 10
Public Const SQUARE_COLOUR As Long = rgbLightBlue
Public GameRunning As Boolean
Public GameInput As ClassGameInput
Public Graphics As ClassGraphics
Public Square As ClassSquare
Class GameInput
Option Explicit
Public Function UpIsPressed() As Boolean
UpIsPressed = (GetAsyncKeyState(vbKeyUp) <> 0)
End Function
Public Function DownIsPressed() As Boolean
DownIsPressed = (GetAsyncKeyState(vbKeyDown) <> 0)
End Function
Public Function LeftIsPressed() As Boolean
LeftIsPressed = (GetAsyncKeyState(vbKeyLeft) <> 0)
End Function
Public Function RightIsPressed() As Boolean
RightIsPressed = (GetAsyncKeyState(vbKeyRight) <> 0)
End Function
Public Function TabIsPressed() As Boolean
TabIsPressed = (GetAsyncKeyState(vbKeyTab) <> 0)
End Function
Class Graphics
Option Explicit
Const COL_WIDTH As Double = 0.83
Const ROW_HEIGHT As Double = 7.5
Private GameCanvas As Range
Private Sub Class_Initialize()
With shtGame
.cmdGo.Visible = False
.Cells.Columns.ColumnWidth = COL_WIDTH
.Cells.Rows.RowHeight = ROW_HEIGHT
.EnableSelection = xlNoSelection
.Protect AllowFormattingCells:=True
.Cells.Interior.Color = rgbDarkSlateGrey
Set GameCanvas = .Range(.Cells(BORDER_START_Y, BORDER_START_X), .Cells(BORDER_END_Y, BORDER_END_X))
GameCanvas.Interior.Pattern = xlNone
End With
End Sub
Public Sub DrawSquare(SquareX As Integer, SquareY As Integer)
Dim SquareRange As Range
With shtGame
Set SquareRange = .Range(.Cells(SquareY, SquareX), .Cells(SquareY + SQUARE_SIZE - 1, SquareX + SQUARE_SIZE - 1))
SquareRange.Interior.Color = SQUARE_COLOUR
End With
End Sub
Private Sub Class_Terminate()
With shtGame
.cmdGo.Visible = True
.EnableSelection = xlNoRestrictions
.Unprotect
.Cells.Clear
End With
End Sub
Class Square
Option Explicit
Private SquareX As Integer
Private SquareY As Integer
Private Sub Class_Initialize()
SquareX = BORDER_START_X
SquareY = BORDER_START_Y
End Sub
Sorry for the big fat code dump, but I really have no idea what is causing the problem!
Any help is greatly appreciated!

The CTD was being caused by missing arguments when calling DrawSquare().
I had incorrectly missed the x and y co-ordinates of the sub.
Normally, the compiler would pick-up on this error, however as the Main module was being called automatically by my timer no error checking was taking place.
Public Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long

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

VBA - Error 438 object doesn't support... CenterAlign captions of command buttons

I'm getting an error 438 "object doesn't support property..." screenshot below. My code below: Objective is to center-align the caption/text of all command buttons on Userform1. Why I'm getting this error? How to fix it?
Option Explicit
'<<<<<<<<<<< Adapted from http://www.freevbcode.com/ShowCode.asp?ID=330
#If VBA7 Then
Private Declare PtrSafe Function GetWindowLong& Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long)
Private Declare PtrSafe Function SetWindowLong& Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long)
#Else
Private Declare Function GetWindowLong& Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long)
Private Declare Function SetWindowLong& Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long)
#End If
'--------------------------------------------
Public Enum BUTTON_STYLE
BS_CENTER = &H300&
BS_LEFT = &H100&
BS_RIGHT = &H200&
BS_TOP = &H400&
End Enum
'--------------------------------------------
Private Const GWL_STYLE& = (-16)
Public Sub AlignCommandButtonText(Button As Object, Style As BUTTON_STYLE)
Dim lHwnd As Long
On Error Resume Next
lHwnd = Button.hwnd
Dim lWnd As Long
Dim lRet As Long
If lHwnd = 0 Then Exit Sub
lWnd = GetWindowLong(lHwnd, GWL_STYLE)
lRet = SetWindowLong(Command1.hwnd, GWL_STYLE, Style Or lWnd)
Button.Refresh
End Sub
'--------------------------------------------
Public Sub CenterAlignButtons()
For Each control_Object In UserForm1.Controls
If TypeName(control_Object) = "CommandButton" Then
Set Button = control_Object
Button.Refresh '<<<<<<< This method of calling is wrong, causing the error...?
DoEvents
End If
Next
End Sub
'--------------------------------------------
Error 438...
Before running language code
After running language code

Focus to MgsBox when Excel is not active

I have checked related questions such as this or this one but the solutions there do not seem to solve my problem.
I am running a VBA script on my computer. The script takes a few minutes to execute and while waiting I am checking other things in my computer. To get my attention once the script has finished running, I have included a MsgBox at the end of my script. However, because Excel is not active/selected when the script finishes, I cannot see it - only when I reactivate/select Excel.
How can I bring into focus the MsgBox when Excel is not active? I have already tried the following tweaks but they do not work:
ThisWorkbook.Activate:
...
ThisWorkbook.Activate
MsgBox "..."
...
AppActivate() (this command threw an error):
...
AppActivate("Microsoft excel")
MsgBox "..."
...
How about playing a sound when the program finishes?
Place this declaration at the top of a standard code module, above any procedures existing there.
Public Declare Function Beep Lib "kernel32" _
(ByVal dwFreq As Long, _
ByVal dwDuration As Long) As Long
If you place this procedure in the same module you may not need it to be public. Adjust pitch and duration to your preference.
Sub EndSound()
Beep 500, 1000
End Sub
Then place the procedure call at the end of your program.
Call EndSound
I suppose you might use a more elaborate sound - may I suggest a couple of bars from Beethoven's 5th? Modify the EndSound procedure. Chip Pearson has more on this idea.
Try:
Application.WindowState = xlMaximized
Disclaimer: This is not my code and I do not know who the author is. I had this code in my database.
Put your code in Sub Sample(). I have shown where you can insert your code. Once the code is run, Excel will flash 5 times. you can change this number by changing Private Const NumberOfFlashes = 5
Paste this in a Module.
Option Explicit
Private Type FLASHWINFO
cbSize As Long
Hwnd As Long
dwFlags As Long
uCount As Long
dwTimeout As Long
End Type
Private Const FLASHW_STOP As Long = 0
Private Const FLASHW_CAPTION As Long = &H1
Private Const FLASHW_TRAY As Long = &H2
Private Const FLASHW_ALL As Long = (FLASHW_CAPTION Or FLASHW_TRAY)
Private Const FLASHW_TIMER As Long = &H4
Private Const FLASHW_TIMERNOFG As Long = &HC
Private FLASHW_FLAGS As Long
Private Declare Function LoadLibrary Lib "kernel32" _
Alias "LoadLibraryA" (ByVal lpLibFileName As String) As Long
Private Declare Function GetProcAddress Lib "kernel32" _
(ByVal hModule As Long, ByVal lpProcName As String) As Long
Private Declare Function FreeLibrary Lib "kernel32" _
(ByVal hLibModule As Long) As Long
Private Declare Function FlashWindowEx Lib "user32" _
(FWInfo As FLASHWINFO) As Boolean
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" _
(ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Const NumberOfFlashes = 5
Private Function APIFunctionPresent(ByVal FunctionName _
As String, ByVal DllName As String) As Boolean
Dim lHandle As Long
Dim lAddr As Long
lHandle = LoadLibrary(DllName)
If lHandle <> 0 Then
lAddr = GetProcAddress(lHandle, FunctionName)
FreeLibrary lHandle
End If
APIFunctionPresent = (lAddr <> 0)
End Function
Sub Sample()
'
' Put your code here. Once that code finishes, Excel will FLASH
'
Dim udtFWInfo As FLASHWINFO
If Not APIFunctionPresent("FlashWindowEx", "user32") Then Exit Sub
With udtFWInfo
.cbSize = Len(udtFWInfo)
.Hwnd = Application.Hwnd
.dwFlags = FLASHW_FLAGS Or FLASHW_TRAY
.uCount = NumberOfFlashes
.dwTimeout = 0
End With
Call FlashWindowEx(udtFWInfo)
MsgBox "Done"
End Sub
The easiest way is to probably to create a userform instead then set the focus to this when it initialises.
Code in the userform to show as modal:
Private Declare Function FindWindow Lib "User32" Alias "FindWindowA" (ByVal lpClassName _
As String, ByVal lpWindowName As String) As Long
Private Declare Function SetWindowPos Lib "User32" (ByVal hwnd As Long, ByVal hWndInsertAfter As Long, _
ByVal X As Long, ByVal Y As Long, ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As Long
Private Const SWP_NOSIZE = &H1
Private Const SWP_NOMOVE = &H2
Private Const FLAGS As Long = SWP_NOMOVE Or SWP_NOSIZE
Private Const HWND_TOPMOST = -1
Private Sub UserForm_Initialize()
Dim hwnd As Long: hwnd = FindWindow(vbNullString, Me.Caption)
If hwnd > 0 Then SetWindowPos hwnd, HWND_TOPMOST, 0, 0, 0, 0, FLAGS ' Set Modal
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

Set TabStops on a ListBox control in VBA Excel

I first came onto that post here by Randy Birch about listing clipboard formats. As you can see, he is using Visual Basic 6 and also a .Refresh method on List1 after sending the LB_SETTABSTOPS messages to the WNDPROC handling the window corresponding to his "List1" ListBox
Since the .Refresh method is not available in VBA (and also the .Hwnd, but that is less a problem withing this post by C. PEARSON and Private Declare Function GetFocus Lib "user32" () As Long), I tried to 'mimic' it.
Apparently, the .Refresh method invalidates the whole client area of the ListBox Window, and then sends a WM_PAINT message to the WNDPROC bypassing any other pending messages in message queue, causing an immediate repaint of the update region, which should be the entire visible "List1" ListBox in this particular case.
My config :
Debug.Print Application.Version
Debug.Print Application.VBE.Version
Debug.Print Application.OperatingSystem
#If VBA6 Then
Debug.Print "VBA6 = True"
#Else
Debug.Print "VBA6 = False"
#End If
#If VBA7 Then
Debug.Print "VBA7 = True"
#Else
Debug.Print "VBA7 = False"
#End If
Results in:
16.0
7.01
Windows (32-bit) NT 10.00
VBA6 = True
VBA7 = True
Now my attempt #1 :
Option Explicit
Private Const LB_SETTABSTOPS As Long = &H192
Private Const EM_SETTABSTOPS As Long = &HCB
Private Const RDW_ALLCHILDREN = &H80
Private Const RDW_ERASE = &H4
Private Const RDW_ERASENOW = &H200
Private Const RDW_FRAME = &H400
Private Const RDW_INTERNALPAINT = &H2
Private Const RDW_INVALIDATE = &H1
Private Const RDW_NOCHILDREN = &H40
Private Const RDW_NOERASE = &H20
Private Const RDW_NOFRAME = &H800
Private Const RDW_NOINTERNALPAINT = &H10
Private Const RDW_UPDATENOW = &H100
Private Const RDW_VALIDATE = &H8
Private hWndList1 As Long
Private Type Rect
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As Long
Private Declare Function PostMessage Lib "user32" Alias "PostMessageA" (ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) 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 GetFocus Lib "user32" () As Long
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
Private Declare PtrSafe Function UpdateWindow Lib "user32" (ByVal hWnd As LongPtr) As Boolean
Private Declare Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" (ByRef lpRect As Rect) As Long
Private Declare Function RedrawWindow Lib "user32" (ByVal hWnd As Long, ByRef lprcUpdate As Rect, ByVal hrgnUpdate As Long, Optional ByVal flags As Integer) As Boolean
Private Declare Function InvalidateRect Lib "user32" (ByVal hWnd As Long, ByRef lpRect As Rect, ByVal bErase As Boolean) As Long
Private Declare Function GetUpdateRect Lib "user32" (ByVal hWnd As Long, ByRef lpRect As Rect, ByVal bErase As Boolean) As Boolean
Private Declare Function GetWindowRect Lib "user32" (ByVal hWnd As Long, ByRef lpRect As Rect) As Boolean
Private Declare Function CreateRectRgnIndirect Lib "gdi32" (ByRef lpRect As Rect) As Long
Private Declare Function GetClientRect Lib "user32" (ByVal hWnd As Long, ByRef lpRect As Rect) As Long
Private Declare Function GetDesktopWindow Lib "user32" () As Long
Private Sub UserForm_Initialize()
Dim ListWindowUpdated As Boolean
Dim ListWindowRedrawn As Boolean
ReDim TabStop(0 To 1) As Long
TabStop(0) = 90
TabStop(1) = 130
With List1
.Clear
.SetFocus
hWndList1 = GetFocus
Call SendMessage(hWndList1, LB_SETTABSTOPS, 0&, ByVal 0&)
Call SendMessage(hWndList1, LB_SETTABSTOPS, 2, TabStop(0))
Dim rectList1 As Rect
Call GetWindowRect(hWndList1, rectList1)
Dim lprcList1 As Long
lprcList1 = VarPtrArray(rectList1)
ListWindowRedrawn = RedrawWindow(hWndList1, rectList1, lprcList1, RDW_INVALIDATE)
ListWindowRedrawn = RedrawWindow(hWndList1, rectList1, 0, RDW_INVALIDATE)
MsgBox "ListWindowRedrawn = " & ListWindowRedrawn & " and RDW_INVALIDATE message sent"
'Call RedrawWindowAny(hWndForm2, vbNull, 1&, RDW_INVALIDATE Or RDW_UPDATENOW Or RDW_ALLCHILDREN)
ListWindowUpdated = UpdateWindow(hWndList1)
MsgBox "ListWindowUpdated = " & ListWindowUpdated
End With
End Sub
My attempt #2 :
Dim ScreenRect As Rect
Dim hClientRect As Long
hClientRect = GetClientRect(hWndList1), ScreenRect)
Dim udtScrDim As Rect
Dim lReturn As Long
Dim hRegion As Long
udtScrDim.Left = 0
udtScrDim.Top = 0
udtScrDim.Right = ScreenRect.Right - ScreenRect.Left
MsgBox "Screen width = " & ScreenRect.Right - ScreenRect.Left
udtScrDim.Bottom = ScreenRect.Bottom - ScreenRect.Top
MsgBox "Screen height = " & ScreenRect.Bottom - ScreenRect.Top
hRegion = CreateRectRgnIndirect(udtScrDim)
If hRegion <> 0 Then
lReturn = RedrawWindow(0, udtScrDim, hRegion, RDW_ERASE Or RDW_FRAME Or RDW_INVALIDATE Or RDW_UPDATENOW Or RDW_INTERNALPAINT Or RDW_ALLCHILDREN)
End If
After many attemps, I still can't get the client area to be updated with the custom tabstop positions. But the attempt #1 above still seems to be the more logical to me. It works fine, no errors, but nothing changes, any item (containing vbTab) in the ListBox won't be affected, even with a later UserForm1.Repaint.
Please help :)
This is not quite an answer but more a workaround :
My understanding of the problem (and of Randy Birch) :
The only explaination is that the VBA Listbox control simply can't deal with LB_SETTABSTOPS messages. Indeed I also tried sending the LB_SETTABSTOPS message later, but it's still ignored. Same thing with the invalidate message and WM_PAINT.
That might be why the Office devs implemented the .ColumnWidth property in VBA Excel which can do exactly the same things that what I was trying to do above.

Resources