Automatically run VBA macro without a message box - excel

I have an Excel macro that I would like to run automatically when the file is opened. The only way I have gotten this to work is by adding a msgbox before calling to my subroutines. However, this requires me to click OK or close the box for the macros to run. I have tried using a timed msgbox sub, but this also does not work.
For some reason, the msgbox pops up before Excel is fully opened, at which point the macro gets stuck here (code for this is below). From here, I tried waiting for the file itself to be opened until it is in write-mode (Workbook.ReadOnly = false). This also did not work.
Public Sub msgBoxTimer()
Const timeout = 2
Dim objShell As Object
Set objShell = CreateObject("WScript.Shell")
objShell.Popup "Measurement will begin shortly", timeout
End Sub
Private Sub Workbook_Open()
Call msgBoxTimer
Call init ' initiate device
Call updateIndex ' collect & record measurements
End Sub

I get from your comment that you are probably running other shell commands in init and updateIndex.
What needs to be clear is that when you execute a Shell command via a Shell object in VBA, the default behavior is not to wait for the shell command to complete before running the next line of codes.
If you want Excel to wait for the Shell command to be completed before continuing, you can have a look at the answers to this question.
That being say, if you want Excel to be fully open before running any shell commands, you can use a MsgBox like you originally intended, but it has to be VBA's MsgBox that you would simply call like this:
MsgBox "Measurement will begin shortly"
The VBA thread will wait for the "OK" button to be pressed before continuing the execution.

Related

How does Excel's Workbook_Open() event actually work?

I'm using an Excel file to operate a small business system and have noticed some quirks with the Workbook_Open() event. The file contains dozens of modules (including class modules representing business logic such as invoices and customers) and I have not been able to reproduce the issue with an MCVE but am hoping someone can help me learn something from this.
The setup is straightforward, the Workbook_Open() event in the ThisWorkbook object calls a sub named Init placed in a regular model called Main:
Private Sub Workbook_Open()
Call Main.Init
End Sub
The Init sub in the Main module prints a start-up message to the immediate window and calls some other subs to initiate a couple of global variables to hold some data:
Public Sub Init()
' Called by ThisWorkbook.Workbook_Open().
Debug.Print "Initializing variables..."
Debug.Print "The system contains:"
Call Init_Items
End Sub
Public Sub Init_Items()
' This sub populates a collection of objects of a CInvoice class by looking
' up data in an Invoices table (a VBA ListObject); it produces output
' similar to the following:
Debug.Print "230 invoices."
End Sub
If the above code is placed in a sample file, it will print the following start-up message to the Immediate window when that file is opened:
Initialzing variables...
The system contains:
230 invoices.
This simple setup basically corresponds to the operational business file. However, when the business file is opened the output is printed twice to the Immediate window. To figure out what's going on, I placed a Stop in Workbook_Open(), in the hope that I could step through the code. To my surprise this revealed that the first iteration of the start-up message was written to the Immediate window even before the call to Main.Init.
Private Sub Workbook_Open()
Stop ' <--- The output produced by Main.Init is written to the
' Immediate window twice, both before and after this Stop.
Call Main.Init
End Sub
Moreover, Workbook_Open() also writes the number 3 to the Immediate window. This 3 is typically written after each iteration of the start-up message, but occasionally the first 3 is written before the first iteration of the start-up message and then after the second iteration. Sometimes it's written only once, after the first iteration of the start-up message but I have not noticed any pattern to this behavior.
This isn't really a big issue since the system is working properly but I'm curious to know what's going on.

How to detect if user clicks on File >> Print as option or presses CTRL+P from within MS word or Excel using VBA code?

I want to detect when user clicks File >> Print or CTRL+P inside ms word or excel and use this detection to run a batch file using vba code, is this possible?
This code should self start along with the program.
I tried to find similar code but was unable to find anything useful to my need.
Any help would greatly appreciated.
Thanks
The way your question is written seems ambiguous to me. At first read, it seems like you are trying to distinguish between these two methods of telling a file to print. I know of no way to do this in vba.
However, you can intercept the print event or command.
Another possible meaning is that you want your procedure to run whenever the user attempts to print. See Intercepting Events Like Save or Print by Word MVPs Dave Rado and Jonathon West. See also Application.WorkbookBeforePrint Event.
Note, this does not block screenshots or saving to another file. Do you mind sharing why you are trying to do this? What you hope to accomplish?
You can use the DocumentBeforePrint and WorkbookBeforePrint Events. Below quoted from linked pages on Intercepting Events and WorkBookBeforePrint documentation.
A DocumentBeforePrint event procedure looks like this:
Private Sub oApp_DocumentBeforePrint(ByVal Doc As Document, _
Cancel As Boolean)
'Your code here
End Sub
If you want to prevent printing from occurring in certain
circumstances, you can set the Cancel variable to True, e.g.:
Private Sub oApp_DocumentBeforePrint(ByVal Doc As Document, _
Cancel As Boolean)
Dim Result As Long
Result = MsgBox("Have you checked the " & "printer for letterhead paper?", vbYesNo)
If Result = vbNo Then Cancel = True
End Sub
From Excel documentation
This example recalculates all worksheets in the workbook before
printing anything.
Private Sub App_WorkbookBeforePrint(ByVal Wb As Workbook, _
Cancel As Boolean)
For Each wk in Wb.Worksheets
wk.Calculate
Next
End Sub
End Quoted Material
Intercepting the Command instead of the Event
Another, less effective, method is to Intercept the actual commands. You could name your procedure PrintPreviewAndPrint and have another called FilePrintQuick that calls your procedure PrintPreviewAndPrint. Earlier versions use FilePrint and FilePrintDefault. Thank #Timothy Rylatt for the command names. He adds: Note that neither of these will intercept the backstage command accessed via File | Print. For that you need to use an event.
Sub PrintPreviewAndPrint()
' Your code here
End Sub
Sub FileQuick()
FilePreviewAndPrint
End Sub
In Word, these would go in your template or in a Global Template.
In Word, you make a template a Global Template by placing it in your Word Startup Folder.
Dealing with making this Global in Excel
My understanding of how Excel handles global macros is far poorer than that for Word. To assist with this, I asked my own question in the Microsoft Answers Excel Programming forum. Here is a link to that question and the answers I received. Andreas Klinger, who is engaged in that thread, is an experienced and knowledgeable Excel programmer, which I am not.

Excel VBA hangs when expanding [+] workbooks object variable in locals window

I'm having a problem trying to debug some code I've written that opens a shared Excel file from SharePoint (in desktop Excel app), scrapes schedule data, makes a few changes, saves and closes that file, then populates the scraped data into the current workbook formatted as a calendar.
As is typical, the code executes fine for me, but only works about 50% of the time when another user runs it. I'm still searching for fixes to those bugs - they are not the reason for my question today. I'll try as best I can to describe my problem and hopefully someone can tell what I'm doing wrong.
First step I do is to check whether the SharePoint file exists and if so, open it (no problems/errors typically encountered here for myself or anyone else):
Sub getcalendardata()
...define variables...
Application.EnableEvents = False
Application.ScreenUpdating = False
schedule = "https://mycompany.sharepoint.com/sites/Schedule/Shared Documents/Schedule.xlsx"
If URLExists(schedule) = 0 Then
MsgBox "Schedule not found. Check internet connection, login to Office 365, and try again."
Exit Sub
End If
Set schedulewb = Workbooks.Open(schedule, False, False)
schedulewb.AutoSaveOn = False
Application.WindowState = xlMinimized
Set schedulews = schedulewb.Worksheets("Design")
...more code here...
Application.ScreenUpdating = True
Application.EnableEvents = True
End Sub
Function URLExists(url As String) As Boolean
Dim Request As Object
Dim ff As Integer
Dim rc As Variant
On Error GoTo EndNow
Set Request = CreateObject("WinHttp.WinHttpRequest.5.1")
With Request
.Open "GET", url, False
.Send
rc = .StatusText
End With
Set Request = Nothing
If rc = "OK" Then URLExists = True
Exit Function
EndNow:
On Error Goto 0
End Function
*** Occasionally, I run into issues with the cached file in MS Office's Upload Center becoming out-dated and asking to save a copy or discard changes, but that is a problem for another day.
The main problem I've come here for is that after stepping through the Workbooks.Open line, if I open the "Locals Window" to view the stored variables and try and expand the Workbook object "schedulewb" (Here:
Locals Window during code execution), code execution seems to stop or go into an infinite loop. It's like VBA is trying to expand the schedulewb object to display the properties, but isn't getting a response, so it just waits. I've not been patient enough to see if it ever self recovers.
The only thing I see is that the Locals Window schedulewb line goes blank (like it's trying to expand) and I can no longer step through, continue, or reset/end the code execution. I can however, break/pause the execution, at which point, schedulewb returns to the Locals Window list and I'm able to expand it with a very brief delay. Restarting or resetting code execution after this just puts the program into the previous stall pattern and the only recovery option (besides waiting potentially forever) is to close and restart excel via Task Manager. Interestingly, the actual Schedule Workbook I'm trying to peak inside now shows up in the Office Upload Center as a pending upload or having just been synced.
I've tried bypassing SharePoint by opening a local version of the Schedule Workbook saved to my computer, and as expected, I can interact with the Locals Window variables without consequence.
Is there something wrong with setting a SharePoint Workbook to a variable like I've done?
Has an upload/refresh been triggered by me trying to inspect the object properties, causing some other uncontrollable VBA/SharePoint event to infinitely loop in the background?
Could this be connected somehow to the Upload Center cache headache that I think is unrelated?
Thank you in advance for any insight...

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.

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.

Resources