Controlling Excel via Midi controller - excel

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.

Related

Input Midi Sync fails ONLY on Excel version 64bit for VBA scripting

I did a small script that works extremely well on any computer with Excel version 32bits, and becomes extremely sluggish on Excel version 64bits on the same computer where an Excel version 32bits was successfully tried!
This is extremely puzzling, besides being unpredictable on 64bits, when it works it stops almost always after 33 seconds. This duration on the 64bits is consistent among different BPM tested from 20 up to 300bpm, above that tempo the VBA script stops or crashes! I suspect it has something to do with memory. However the MidiIn_Event becomes so sluggish on 64bits that it may be a bug on Windows itself!
UPDATE: The script below breaks out the cycle Do While countdown > 0 despite the countdown being greater than 0! So, something is disrupting the cycle while before the countdown reaches zero (for 64bits only).
This is the script:
Option Explicit
Private Const CALLBACK_FUNCTION = &H30000
'INPUT DEVICE ID ENTERED HERE
Private Const MIDI_DEVICE_ID As Long = 1
'MIDI Functions here: https://learn.microsoft.com/en-us/windows/win32/multimedia/midi-functions
#If Win64 Then
'For MIDI device INPUT
Private Declare PtrSafe Function midiInClose Lib "winmm.dll" (ByVal hMidiIn As LongPtr) As Long
Private Declare PtrSafe Function midiInOpen Lib "winmm.dll" (lphMidiIn As LongPtr, ByVal uDeviceID As LongPtr, ByVal dwCallback As Any, ByVal dwInstance As LongPtr, ByVal dwFlags 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
'For MIDI device INPUT
Private Declare Function midiInClose Lib "winmm.dll" (ByVal hMidiIn As Long) As Long
Private 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
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 mlngHmidi As LongPtr
Private mlngRc As LongPtr
Private mlngMidiMsg As LongPtr
#Else
Private mlngHmidi As Long
Private mlngRc As Long
Private mlngMidiMsg As Long
#End If
'Counters
Private countdown As Integer
Private function_calls As Integer
Private function_actions As Integer
Private pendent_goes As Integer
'Main Function externally Called
Public Sub runClock()
'When canceled become able to close opened MIDI Channels!
On Error GoTo handleCancel
Application.EnableCancelKey = xlErrorHandler
'Counters Reset
countdown = 5000 'Press Esc to Stop
function_calls = 0
function_actions = 0
pendent_goes = 0
'Starts listening the Midi Input
Call midiInOpen(mlngHmidi, MIDI_DEVICE_ID - 1, AddressOf MidiIn_Event, 0, CALLBACK_FUNCTION)
Call midiInStart(mlngHmidi)
Application.StatusBar = "Started"
'Processes the Countdown for each Midi Message
Do While countdown > 0
If pendent_goes > 0 Then
'Shows up the counting down each time a Midi Message is processed
Application.StatusBar = "Countdown=" & countdown & " | Pendent=" & pendent_goes
countdown = countdown - 1
pendent_goes = pendent_goes - 1
End If
Loop
'Ends listening the Midi Input
Call midiInReset(mlngHmidi)
Call midiInStop(mlngHmidi)
Call midiInClose(mlngHmidi)
'Shows the total amount of calls and the amount of messages processed
Application.StatusBar = "Finish (" & function_calls & ", " & function_actions & ")"
handleCancel: 'Handles the Esc key
If Err.Number = 18 Then
'Ends listening the Midi Input
Call midiInReset(mlngHmidi)
Call midiInStop(mlngHmidi)
Call midiInClose(mlngHmidi)
'Shows the total amount of calls and the amount of messages processed
Application.StatusBar = "Finish (" & function_calls & ", " & function_actions & ")"
End If
End Sub
'CALLBACK FUNCTION PROCESSED ON EACH MIDI MESSAGE GIVENT BY THE INPUT DEVICE
Private Sub MidiIn_Event(ByVal mlngHmidi As LongPtr, ByVal Message As LongPtr, ByVal instance As LongPtr, ByVal dw1 As LongPtr, ByVal dw2 As LongPtr)
'Excel 32bit: Works without any problem up to 600bpm;
'Excel 64bit: Sluggish, doesn't work more than 33 seconds and crashes a lot!
function_calls = function_calls + 1 'Counts all Calls
If Message = 963 Then
function_actions = function_actions + 1 'Counts all Message Inputs
pendent_goes = pendent_goes + 1 'Adds quantity of Messages to be processed
End If
End Sub
I would like to know how to make it work on Excel 64bits like it works on 32bits.
For testing I use the MIDI-OX to generate the Clock Sync midi messages and the loopMIDI as the midi Device input.
Any help is appreciated. Thanks.
This is the last updated code accordingly to suggestions with the following results:
Still disrupts the While cycle for 64bit excel;
Added the version const, now the VBA7 makes it true even for 32 bit
Excel versions. Nevertheless the Excel 32bit still works fine.
Updated Script:
Option Explicit
Private Const CALLBACK_FUNCTION = &H30000
'INPUT DEVICE ID ENTERED HERE
Private Const MIDI_DEVICE_ID As Long = 1
'MIDI Functions here: https://learn.microsoft.com/en-us/windows/win32/multimedia/midi-functions
#If VBA7 Then
Private Const BIT_VERSION = 64
'For MIDI device INPUT
Private Declare PtrSafe Function midiInClose Lib "winmm.dll" (ByVal hMidiIn As LongPtr) As Long
Private Declare PtrSafe Function midiInOpen Lib "winmm.dll" (ByRef lphMidiIn As LongPtr, ByVal uDeviceID As Long, ByVal dwCallback As LongPtr, ByVal dwInstance As LongPtr, ByVal dwFlags As Long) 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 Const BIT_VERSION = 32
'For MIDI device INPUT
Private Declare Function midiInClose Lib "winmm.dll" (ByVal hMidiIn As Long) As Long
Private 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
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 VBA7 Then
Private mlngHmidi As LongPtr
Private mlngRc As LongPtr
Private mlngMidiMsg As LongPtr
#Else
Private mlngHmidi As Long
Private mlngRc As Long
Private mlngMidiMsg As Long
#End If
'Counters
Private countdown As Integer
Private function_calls As Integer
Private function_actions As Integer
Private pendent_goes As Integer
'Main Function externally Called
Public Sub runClock()
'When canceled become able to close opened MIDI Channels!
On Error GoTo handleCancel
Application.EnableCancelKey = xlErrorHandler
'Counters Reset
countdown = 5000 'Press Esc to Stop
function_calls = 0
function_actions = 0
pendent_goes = 0
'Starts listening the Midi Input
Call midiInOpen(mlngHmidi, MIDI_DEVICE_ID - 1, AddressOf MidiIn_Event, 0, CALLBACK_FUNCTION)
Call midiInStart(mlngHmidi)
Application.StatusBar = "Started"
'Processes the Countdown for each Midi Message
Do While countdown > 0
If pendent_goes > 0 Then
'Shows up the counting down each time a Midi Message is processed
Application.StatusBar = "VERSION=" & BIT_VERSION & " | " & "Countdown=" & countdown & " | Pendent=" & pendent_goes
countdown = countdown - 1
pendent_goes = pendent_goes - 1
End If
Loop
'Ends listening the Midi Input
Call midiInReset(mlngHmidi)
Call midiInStop(mlngHmidi)
Call midiInClose(mlngHmidi)
'Shows the total amount of calls and the amount of messages processed
Application.StatusBar = "Finish (" & function_calls & ", " & function_actions & ")"
handleCancel: 'Handles the Esc key
If Err.Number = 18 Then
'Ends listening the Midi Input
Call midiInReset(mlngHmidi)
Call midiInStop(mlngHmidi)
Call midiInClose(mlngHmidi)
'Shows the total amount of calls and the amount of messages processed
Application.StatusBar = "Finish (" & function_calls & ", " & function_actions & ")"
End If
End Sub
'CALLBACK FUNCTION PROCESSED ON EACH MIDI MESSAGE GIVENT BY THE INPUT DEVICE
Private Sub MidiIn_Event(ByVal mlngHmidi As LongPtr, ByVal Message As Long, ByVal instance As LongPtr, ByVal dw1 As LongPtr, ByVal dw2 As LongPtr)
'Excel 32bit: Works without any problem up to 600bpm;
'Excel 64bit: Sluggish, doesn't work more than 33 seconds and crashes a lot!
function_calls = function_calls + 1 'Counts all Calls
If Message = 963 Then
function_actions = function_actions + 1 'Counts all Message Inputs
pendent_goes = pendent_goes + 1 'Adds quantity of Messages to be processed
End If
End Sub

HeapFree is crashing

Summary:
I have VBA code that collects lots of info and writes it out into one or more worksheets. To improve perf writing lots of info into sheets, I created a class that acts kind of like a buffered copy/paste stream: the caller sends it CSV format strings which it buffers in memory until the buffer is full; when full, it pastes into a sheet, clears the buffer and continues.
Initially, I used Global memory, but then saw on MSDN a recommendation to use Heap rather than Global or Local due to less overhead. So now I'm using Heap.
I'm in the process of adapting everything for 64-bit Office. After doing all the PtrSafe stuff, I can run the code. But now Excel crashes when HeapFree() is called.
Question: Why is it crashing, and what do I need to change to avoid it?
Details:
I've come up with the following that is a minimized sample that repro's the crash: there's a module with a sub I can run for the repro, and the class. This doesn't do the buffering; every call to .SendText will put the text on the clipboard and paste into the active cell.
First the module. This has the following declare statements
' memory APIs
Public Const HEAP_ZERO_MEMORY = &H8
Declare PtrSafe Function GetProcessHeap Lib "kernel32" () As LongPtr 'returns HANDLE
Declare PtrSafe Function HeapAlloc Lib "kernel32" (ByVal hHeap As LongPtr, ByVal dwFlags As Long, ByVal dwBytes As LongPtr) As LongPtr 'returns HANDLE
Declare PtrSafe Function HeapFree Lib "kernel32" (ByVal hHeap As LongPtr, ByVal dwFlags As Long, lpMem As Any) As Long 'returns BOOL
Declare PtrSafe Function lstrcpy Lib "kernel32" Alias "lstrcpyW" (ByVal lpDestString As Any, ByVal lpSrcString As Any) As LongPtr 'returns HANDLE
' clipboard APIs
Public Const CF_UNICODETEXT = 13
Declare PtrSafe Function OpenClipboard Lib "user32" (ByVal hwnd As LongPtr) As Long 'returns BOOL
Declare PtrSafe Function EmptyClipboard Lib "user32" () As Long 'returns BOOL
Declare PtrSafe Function CloseClipboard Lib "user32" () As Long 'returns BOOL
Declare PtrSafe Function SetClipboardData Lib "user32" (ByVal wFormat As Long, ByVal hMem As LongPtr) As LongPtr 'returns HANDLE
Then the sub:
Private Sub TestMemoryBugRepro()
Dim s As String
Dim clip As clsHeapBugRepro
s = Chr(34) & "a" & vbLf & "b" & Chr(34) & ",c" & vbCrLf & "d" & vbTab & ",e" & vbCrLf
Set clip = New clsHeapBugRepro
clip.Initialize &H100
clip.SendText s
'Crash happens during the following
Set clip = Nothing
End Sub
Now the class. The crash occurs during Class_Terminate() when HeapFree is called.
Option Explicit
Private m_hHeap As LongPtr 'handle to the process heap
Private m_hMem As LongPtr 'handle to memory
Private m_pMem As LongPtr 'pointer to locked memory
Private m_cbMem As Long 'size of the memory buffer
Private m_BytesWritten As Long '
'**************************************
' Event procedures
Private Sub Class_Initialize()
m_hHeap = GetProcessHeap()
End Sub
Private Sub Class_Terminate()
If m_hMem <> 0 And m_hHeap <> 0 Then
HeapFree m_hHeap, 0, m_hMem 'CRASH OCCURS HERE
End If
End Sub
'**************************************
' Public methods
Public Function Initialize(Optional bufferSize As Long = &H8000) As Boolean
Initialize = False
m_BytesWritten = 0
If m_hHeap <> 0 Then
m_cbMem = bufferSize
m_hMem = HeapAlloc(m_hHeap, (HEAP_ZERO_MEMORY), m_cbMem)
End If
If m_hMem <> 0 Then Initialize = True
End Function
Public Function SendText(text As String) As Boolean
Dim nStrLen As Long
nStrLen = LenB(text) + 2&
Debug.Assert nStrLen < (m_cbMem + m_BytesWritten)
m_pMem = m_hMem 'in lieu of locking heap memory
lstrcpy m_pMem, StrPtr(text)
m_pMem = 0 'in lieu of unlocking heap memory
m_BytesWritten = m_BytesWritten + nStrLen
DoEvents
OpenClipboard 0&
EmptyClipboard
SetClipboardData CF_UNICODETEXT, m_hMem
CloseClipboard
ActiveCell.PasteSpecial
DoEvents
SendText = True
End Function
There's an issue in how you're calling HeapFree, but your real issue is that you shouldn't be using HeapAlloc/HeapFree at all.
Memory allocated by HeapAlloc is not movable, whereas SetClipboardData requires it to be moveable.
Another consideration is that SetClipboardData transfers the ownership of the memory to the system, which means you should not free it yourself. (The application may not write to or free the data once ownership has been transferred to the system.)
So, I would try to rewrite your logic using GlobalAlloc, not HeapAlloc, and don't try to free memory after it is put on the clipboard.
Now, there was an issue in how you were calling HeapFree, if you were going to use it. HeapFree wants the pointer returned by HeapAlloc.
You were instead passing a pointer to that pointer, because you declared the third argument of HeapFree As Any, which means ByRef As Any.
Either redeclare the argument ByVal As LongPtr / ByVal As Any, e.g.:
Declare PtrSafe Function HeapFree Lib "kernel32" (ByVal hHeap As LongPtr, _
ByVal dwFlags As Long,
ByVal lpMem As LongPtr) As Long 'returns BOOL
or specify ByVal when calling it:
HeapFree m_hHeap, 0, ByVal m_hMem

Changing background color for Status Bar of Excel

I'm using Excel 2016 (Office Theme:Colorful) and unfortunately when I write some code with a user defined text for displaying in status bar, the status bar changes its background color to dark green instead of remaining in vbButtonFace (&H8000000F). The result is an unreadable status bar text message, considered that the font color remains dark grey as expected.
I know it directly can't be done by VBA (please, don't suggest to me of changing Office theme... it's not an option!)
Googling around I found some code which uses API functions SendMessage and GetSysColor calls that I rearranged as follow:
#If VBA7 Then
Public Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, lParam As Any) As LongPtr
Public Declare PtrSafe Function GetSysColor Lib "user32" (ByVal nIndex As Long) As Long
#Else
Public 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
Public Declare Function GetSysColor Lib "user32" (ByVal nIndex As Long) As Long
#End If
Private Const CCM_FIRST As Long = &H2000 'Common Control Messages
Private Const CCM_SETBKCOLOR As Long = (CCM_FIRST + 1)
Private Const PBM_SETBKCOLOR As Long = CCM_SETBKCOLOR 'Progress Bar Messages
Private Const COLOR_BTNFACE = &H8000000F
#If VBA7 Then
Public Sub SetStatusBackColour(hwndStatBar As LongPtr, ByVal clrref As Long)
Call SendMessage(hwndStatBar, PBM_SETBKCOLOR, 0&, ByVal clrref)
End Sub
#Else
Public Sub SetStatusBackColour(hwndStatBar As Long, ByVal clrref As Long)
Call SendMessage(hwndStatBar, PBM_SETBKCOLOR, 0&, ByVal clrref)
End Sub
#End If
Public Function EvalCol(ByVal inCol As Long) As Long ' Returns the RGB of a long colour value (System colour aware)
If ((inCol And &HFFFFFF00) = &H80000000) Then EvalCol = GetSysColor(inCol And &HFF) Else EvalCol = inCol
End Function
Private Sub Test()
Call SetStatusBackColour(StatusBar1.hwnd, EvalCol(vbButtonFace))
'Call SetStatusBackColour(StatusBar1.hwnd, COLOR_BTNFACE) 'without GetSysColor API function call
End Sub
Now the problem is... How can I find the hwnd of the Excel Status Bar?
Obviously, if this approach doesn't apply anymore or a different approach can be used instead, please tell me!
You may work around this issue by updating screen before setting the value of status bar then turning it back to False.
For example:
Application.ScreenUpdating = True
Application.StatusBar = "Transferring Records: " & I & " of " & X & " completed..."
Application.ScreenUpdating = False

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

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