Excel Userform forcing label update before continuing sub - excel

I have a sub in a spreadsheet to remove individual line items. Because it takes a few seconds to process, I'd like to have a little Ready indicator.
At the moment I've got a small label (see above) called Lab_Indic.
When the user clicks remove, it runs the _Click sub, which opens with this:
With Lab_Indic
.BackColor = &HFF&
.Caption = "WORKING"
End With
...and closes with this:
With Lab_Indic
.BackColor = &H8000&
.Caption = "READY"
End With
But the initial "Working" change only seems to happen if I step through; if I run it normally, it seems to skip past it.
Is there any way to force it to update the label before continuing with the rest? DoEvents sounds like it might do the job, but some sources I've looked at suggest that there might be a downside to that approach.

It seems to work nicely, so I'll post it as an answer, but I'm still very interested in any indication of whether it's a good solution or not...
With Lab_Indic
.BackColor = &HFF& ' &H00008000&
.Caption = "WORKING"
End With
DoEvents
I've just added a single DoEvents to refresh the label, and it seems to work nicely. I don't know if there are any knockon effects.

Related

My vba code closes a document before the print command can be executed

The code I am trying to run is in excel vba and its supposed to open a word document print it and then close the document. For some reason it seems that the code doesn't finish sending the document to the printer and yet it still closes. So the code runs to completion and doesn't generate an error message but nothing manages to print.
When I run the code step by step the document does manage to print. I tried adding: Application.Wait(Now + TimeValue("0:00:05")) to give it time to work. I tried a another form of that line in case the program was telling word to wait instead of excel: Excel.Application.Wait(Now + TimeValue("0:00:05")). I've also tried playing around with the time making it wait 10 second instead of 5.
Any help would be great
If ENG28 = "" Then
Else
Set objWord = CreateObject("Word.Application")
Set objDoc = objWord.Documents.Open(ENG28)
objWord.Visible = True
objDoc.PrintOut
Application.Wait(Now + TimeValue("0:00:05"))
objWord.Quit 0
End If
I see that you made use of the .Wait method however there is a better way as you can never guarantee that your document is in a printed state within the 5-10 seconds threshold.
The PrintOut method basically has an argument you may add named Background. The default value of this argument is True which would mean that printing occurs in the background and the code continues to run which is causing your file to close before completing the print function. In this case, if you set the Background argument value to False, the macro will not take any more instructions until the printing is done.
try changing the PrinOut line to the following, see if that helps:
objDoc.PrintOut Background:=False

Consistently receiving user input through a long-running procedure (DoEvents or otherwise)

Cleanly cancelling a long API-Ridden procedure is hellish, and I'm attempting to work out the best way to navigate the hellishness.
I'm using excel 2016 (with manual calculations and no screen updates) - I may take some time to attempt to run the procedure on 2010 to see if any issues resolve in the coming days (I'm aware of the slowdowns).
Over time, my procedure LongProcedure has lost its ability to successfully use its cancel feature (presumably due to increasing complexity). It was initially inconsistent and required significant spam-clicking to cancel, and now it fails altogether
Here's the setup:
First, LongProcedure is in a class module LongClass with a public property checked against for early cancelling, allowing it to clean up.
Public Sub LongProcedure()
' [Set up some things] '
For Each ' [Item In Some Large Collection (Est. 300 Items)] '
' [Some Code (ETA 5 Seconds) Sprinkled with 3-4 DoEvents] '
' [Export workbook (ETA 10 Seconds)] '
If (cancelLongProcedure) Then Exit For
Next
' [Clean up some things] '
GeneratorForm.Reset ' Let the UserForm know we're finished
End Sub
Second, I have a UserForm shown from a macro, which instantiates the procedure class, and runs the procedure. It contains a run button, a status label, and a cancel button.
Private MyLong As LongClass
Public Sub ButtonRunLongProcedure_Click()
Set myLong = New LongClass
myLong.LongProcedure()
End Sub
So the issue overall is twofold.
The ExportAsFixedFormat call opens a "Publishing..." progress bar which freezes excel for around ten seconds - fine. In all of my efforts, I haven't found a single way to process user input while this is happening.
On top of this, the DoEvents calls seemingly no longer do anything to allow the cancel button to be clicked. The process inconsistently freezes excel, tabs into other open programs, and (when not freezing) updates the status label.
I've Tried:
Appending DoEvents to the SetStatusLabel method instead of sprinkling - while the form still often freezes, it otherwise updates the status label consistently (while still not allowing the cancel button)
Using winAPI Sleep in place of, and in addition to DoEvents with delays of 1, 5, 10, 50, and 250ms - The form simply stopped updating at all without doevents, and with both it froze more.
Using a Do While loop to run DoEvents constantly for one second (Froze)
Overriding QueryClose to cancel the form. This one helped significantly. For some reason, the close [x] button can be clicked far more consistently than the userform buttons - Still not as consistently as I'd like. The problem? during publishing, Excel stops responding, and as such, modern windows will end the process if you click the close button twice... without cleanup.
Using Application.OnTime to regularly call DoEvents. Didn't seem to improve the situation overall
Alt-Tabbing. No, really. for some reason, while alt-tabbing occasionally just makes the UserForm freeze harder, sometimes it makes it stop freezing and update.
This is an issue I'm willing to do significant refactor work for, including smashing up the idea of the long procedure into separate methods, performing setup initially, and cleanup on class termination. I'm looking for something that provides consistent results. - I'll accept anything from excel versions to excel settings to refactors to winAPI calls.
Thanks for any insight into this one.
As it turns out simply combining together some of the useful improvements, along with a new one, made all the difference.
QueryClose is up to personal preference. Leave it in to catch more terminations, leave it out to ensure users use the new solution
Stick to sprinkling doEvents in places you feel are logical (not just when the status bar updates - like before and after an Application.Calculate call)
Optimize the long-running process as best you can, avoiding excel calls
And, most significantly
The integrated cancel key feature (CTRL+Break by default) is significantly more responsive than UserForm buttons and the form close button, without the chance of accidentally ending the excel task.
Here's the process to polish that for a finished product
First, set up a debugMode, or the inverse handleErrors, module-level variable to control whether to implement break-to-cancel and error handling. (Error handling will make your code harder to debug, so you'll appreciate the switch)
If your process is handling errors, you'll set Application.EnableCancelKey to xlErrorHandler, and On Error GoTo [ErrorHandlingLabel]. The error handling label should be directly before cleanup, and immediately set EnableCancelKey to xlDisabled to avoid bugs. Your handler should check the stored Err.Number and act accordingly, before continuing on to the cleanup steps.
Ensure that if you defer to any other complex vba in your script (such as using Application.Calculate on a sheet with UDFs), you set On Error GoTo 0 beforehand, and On Error GoTo [ErrorHandlingLabel] after, to avoid catching cellbound errors.
Unfortunately, the drawback is that for the UX to be consistently readable, you'll have to leave the cancel key on xlDisabled until the form is closed.
And in code:
Public Sub LongProcedure()
If handleErrors Then
On Error GoTo ErrorHandler
Application.EnableCancelKey = xlErrorHandler
End If
' [Set up some things] '
For Each ' [Item In Some Large Collection (Est. 300 Items)] '
' [Some Code (ETA 5 Seconds) Sprinkled with 3-4 DoEvents] '
' [Export workbook (ETA 10 Seconds)] '
Next
ErrorHandler:
If handleErrors Then
Application.EnableCancelKey = xlDisabled
If (Err.Number <> 0 And Err.Number <> 18) Then
MsgBox Err.Description, vbOKOnly, "Error " & CStr(Err.Number)
End If
Err.Clear
On Error GoTo 0
End If
' [Clean up some things] '
GeneratorForm.Reset ' Let the UserForm know we're finished
End Sub
and in the UserForm
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If MyLong.handleErrors Then Application.EnableCancelKey = xlInterrupt
End Sub
A small note that this method will likely generate a few bugs you weren't expecting to encounter because the execution jumps directly to the specified label. Your cleanup code will need to have required variables instantiated from the start.
Overall, once these issues are fixed, this setup ensures that the user can click CTRL+Break as many times as they could possibly want without causing crashes or popups.

Out of Stack Space Issue - Can I Repeat a Sub Without Recursing It?

I'm working on a fun project in Excel making a game. It works perfectly except that after about a minute of run time it errors because it's out of stack space. I realized this was because technically I'm using recursion in the stepEvent function, which looks like this:
Private Sub stepEvent()
' All the game code
start = Timer
Do While Timer < start + 0.15
DoEvents
Loop
stepEvent
End Sub
None of the instances of stepEvent ever resolve until either the player loses, or it runs out of stack space and errors. My question is if there's a way to either resolve the recursed subs, but somehow retain control, or if there's an alternate method to repeat that sub without using recursion.
Try looping
Sub asDoWhileLoop()
Do While True
Call stepEvent
Loop
End Sub
Sub stepEvent()
'Do game stuff and wait at end
End Sub
But remember to clean up at the end of your stepEvent(), i.e. set your objects to NOTHING, close recordsets, whatever you are playing with. You can quit using END instead of EXIT SUB. But it's all up to you to ensure it terminates.

Wait for CSV file to open?

I am trying to download and open a CSV file using VBA in Excel. When I step through using the debugger my code works fine but when I try to run it normally it won't work, it is trying to copy the info from the newly opened CSV file into the existing .xlsm file but it isn't finding anything. I have found ways of checking that the file is open but I need it to keep checking and once it does exist to continue with the code.
Here is how Im getting the file to download:
Sub getFile(address)
On Error Resume Next
Set ie = CreateObject("InternetExplorer.Application")
ie.Visible = False
ie.navigate address
ie.Quit
Set ie = Nothing
On Error GoTo 0
End Sub
And then after this in the sub that calls getFile,
Do While Workbooks(Workbooks.Count).Name <> "file.csv"
Loop
Ive tried a few variations that all work fine when taking it slow in the debugger but crashes otherwise. Does anyone know a way to have it keep checking and wait until the file is open? It requires the user to do something so what it to make sure that file.csv is open. Should I put my check in sub getFile or keep it after it is called?
SOULTION:
Taking hnk's advice below I tried this and it worked perfectly:
Sub getFile(address)
On Error Resume Next
Set ie = CreateObject("InternetExplorer.Application")
ie.Visible = False
ie.navigate address
Do While Workbooks(Workbooks.Count).Name <> "file.csv"
DoEvents
Loop
ie.Quit
Set ie = Nothing
On Error GoTo 0
End Sub
Moved the check to before closing IE and added DoEvents to my loop.
Usually such behavior happens not because of the code logic but because of interrupts. Depending on the nature of your environment it could be one of two things
some background refreshing process is getting executed sometime in between your code execution time causing some issues. e.g. in a financial services environment, your Bloomberg API could be doing its timely BlpUpdate event
your code blocks up a large chunk of time and causes unexpected behavior because event which was triggered during your loop was blocked too long waiting for your code to complete on the main thread.
So you need to try the following two solutions, one of which should work
For the second problem, inside your
Do While Condition
' ... your code ...
Loop
add a line of code that checks for and clears any pending Event queues, making it...
Do While Condition
DoEvents ' either put it at the start or at the end of your code
' ... your existing code ...
Loop
This will give the system some 'breathing space' to wrap up pending events. You'll need to experiment with the location of your DoEvents code.
For the first problem, you can try enclosing your entire loop within an Event Protected area of code, to ensure it finishes running before anything disturbs it.
Application.EnableEvents = False
'your loop and other code comes here
Application.EnableEvents = True
Once again, you might need a bit of trial and error to see how much of the code needs to be thrown into the Event-free zone before it works as expected.

Side-Step Application.MsgBox in VBA (Excel)

In order to head off a storm of "comment it out" replies, here is my situation:
I have a process is normally run 1 iteration by 1 iteration. A user manually hits a button that calls a macro which, upon completion, pops up a message box that reports the total time length the macro ran for. It's pretty handy for diagnosing issues. This code is locked down and I cannot modify it.
I am trying to do this at scale. Because the code in the main spreadsheet and workbook are locked, I have a separate workbook open in the same instance of excel with a macro that operates the locked down workbook. Rather than 1 by 1, I've got a set of 300 I'm trying to run through. Right now I have to babysit the thing and hit space to get past the MsgBox. Does anyone know of any tricks to prevent me having to monitor the thing? Either disabling the pop-ups or some way to make them non-modal. Maybe a trick to make the mouse click?
You're right in knowing that the best way to fix the issue is to correct the code. In which case you would probably make the pop-ups toggle-able.
However, I wrote this for you which could be used as a potential work around. It utilizes VBScript to "sort-of" simulate multithreading so that you can send a key to the modal Msgbox. Assuming you can do what you want to do via code, simply call SendDelayedKeys before the action that will cause a Msgbox. You may have to tinker with the Delay based upon your circumstances as 100 milliseconds may not be enough. To change the Delay, just call like this: SendDelayedKeys 500 for 500 milliseconds.
Sub SendDelayedKeys(Optional Delay As Long = 100, Optional keys As String = """ """)
Dim oFSO As Object
Dim oFile As Object
Dim sFile As String
sFile = "C:\SendKeys.vbs" 'Make this a valid path to which you can write.
'Check for the .vbs file.
If Not Len(Dir$(sFile)) Then
'Create the vbs file.
Set oFSO = CreateObject("Scripting.FileSystemObject")
Set oFile = oFSO.CreateTextFile(sFile)
oFile.WriteLine "Set WshShell = WScript.CreateObject(""WScript.Shell"")"
oFile.WriteLine "WScript.Sleep CLng(WScript.Arguments(0))"
oFile.WriteLine "WshShell.SendKeys WScript.Arguments(1)"
oFile.Close
End If
Shell "wscript C:\SendKeys.vbs " & Delay & " " & keys
End Sub
Sub ProofOfConcept()
'Using default parameters which sends a space after 100 milliseconds
SendDelayedKeys
MsgBox "I disappear on my own!"
End Sub
A word of warning: Any solution that utilizes SendKeys is a fragile solution and should be avoided when possible. However, when your options are limited and you need to avoid a manual process, sometimes it's your only option.
Since SiddhartRout rightly pointed out that this could be solved using API calls: here's a link with C# code that would close your msgbox every second.
The problem here really isn't strictly a problem more code can (or indeed should) solve.
There are a great many things to consider and any solution will be more complex AND less reliable than the problem it is initially trying to solve. But lets look at your options...
SendKeys is not reliable for that kind of use, what happens if the dialogue says "would you like me to save this workbook?" just after making a change that was meant to be temporary or "would you like to play global thermonuclear war?" Plus with a batch process like that you want to get on with something else while you wait, even if it's only to come here to downvote trolls. If nothing else you may not be in control of this code so what kind of mess will it cause when the maintainers realise msgbox is bad UX and kill it?
FindWindow API calls would let you check the content in the window to make sure it says what you're expecting but then you're potentially asking some bit of quick & dirty vbscript to go into a race condition until the right window comes up. Can you guarantee that the threads won't lock up?. What about platform issues - what happens if someone wants to run your code on their shiny new surface? What happens when your 64 bit modal Excel dialogue window can't be seen by the 32-bit api calls you were making? What about a new version of office that doesn't present modal dialogues in the same way? None of those problems are insurmountable but each adds complexity (aka opportunity for failure.)
The best solution is fix the actual problem you have identified from the outset which is that the original code throws up an unnecessary modal dialogue. Someone needs to fix that - it doesn't have to be you but if you estimate how much time that modal dialogue wastes in lost productivity that should get you a solid business case for getting it sorted.

Resources