Excel/VBA DoEvents and control return - excel

I am trying to make a macro that returns at least some partial control to the user when it runs. The macro behavior cannot be changed dangerously if the user edits cells, as there is few dependent content.
I am using this method to allow the user to occupy the execution thread and commit their cell changes before the macro gains control back and proceeds to the next statement:
Sub retCtrl(Optional ms As Long = 350)
Dim l As Long: l = Timer
Do While Timer - l < (0# + ms / 1000)
DoEvents
Loop
End Sub
I put this after some long statements and inside loops.
It's choppy, but functional, with only one problem:
if the user takes to long to edit the cell, the macro silently stops.
Can anyone explain why this happens? Is it because the timer elapses the threshold? I thought all macro execution stops when the workbook is in edit mode?
Any advice on how to handle this otherwise?

You can't & shouldn't return control to the user while code is running, and your issue is an example of why.
Excel expects certain elements to be in certain states, and when the user starts changing things, unexpected things can happen. For example, what would happen if the user tries to change data the same time Excel needs it? There is no multi-processing built in, "this isn't that kind of programming..."
So Excel ceases running the macro when it notices something happening. If you need to run a different process simultaneously, do it in a separate instance of Excel.
More Information:
Running Macros in the Background
Stack Overflow : How can I execute a long running process in VBA without making pc crawl?
Mr. Excel : Can VBA Run in the Background?

Related

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.

VBA Excel Shell function

I have a Fortran routine which reads the data from a file, processes the data, and records the results in few *.csv files. It has perfectly worked for years.
Now I made a VBA Excel Macro which prepares the data files, calls the Fortran routine
TemporFile = Directory & "\" & "VTFeV4.exe"
Call Shell(TemporFile, 0)
, and forms an Excel workbook using the *.csv files generated by the Fortran routine VTFeV4.exe.
The problem is, it doesn't work consistently. Sometimes, the resulting files do not appear, the VBA Macro indeed canot find the file, and interrups. The Windows' Start Task Manager shows that the EXE file is still running.
(Please note, "Directory" is the correct Path).
I found that if to run the Macro stepwisely (F8), it works better. So I added time delay
Dim PauseTime, Start, Finish, TotalTime
PauseTime = pt
Start = Timer
Do While Timer < Start + PauseTime
DoEvents
Loop
which worked well with pt=10 until TODAY (02/22/17) when it stopped working at all even when pt=20s though the EXE file works well and fast (apparently much quicker than 10s, less to say!).
I also tried
ABC = Shell(Directory & "\NewVTF1.exe", 1)
with same result.
What is the problem with Shell? Please help!

How can I programatically copy and paste with source formatting in PowerPoint 2010?

I am currently automating some PowerPoint 2010 functions in Groovy using Scriptom - though this problem may be generic to any PowerPoint automation approach (ie more of a "VBA macro" issue than the particular environment I'm using?).
(Scriptom allows you to use ActiveX or COM Windows components from Groovy. Underneath the hood it uses the Jacob library (Java COM Bridge), I believe. The underlying code is similar to what I'd use in a VBA macro or other Microsoft automation component, and is based on the PowerPoint 2010 Object API.)
My current code works well and opens PowerPoint visibly and does a range of functions on it - except for a component where I "copy and paste" slides from one document to another, "keeping source formatting".
I have tried two attemps to do this copy and paste step, both leading to a different problem. I wonder if anyone has thoughts on solving either (or both?) of these problems:
Method 1: I use a basic "copy" and "paste" methods, suggested by various others, namely:
sourceSlide.Copy()
destinationSlide = destinationPresentation.Slides.Paste(slideIndex+i-1)
destinationSlide.Design = sourceSlide.Design
destinationSlide.ColorScheme = sourceSlide.ColorScheme
destinationSlide.FollowMasterBackground = sourceSlide.FollowMasterBackground
... and so on copying formats...
That is, I manually copy all the formats across to keep slide-formatting. This is the method used prior to PowerPoint 2010. I've actually got this working, however to copy the formats I loop over each slide in the "source" slidepack and do the copy/paste code above. In this loop, the following line (alone) is problematic:
destinationSlide.Design = sourceSlide.Design
This line runs incredibly slowly once the destination SlidePack has a large number of "Designs" in the SlideMaster. I am copying a source slide-pack of 19 slides, each of which has a different SlideMaster Design Theme (that's how it comes to me). This single line of code takes about 0.01 seconds for copying the first slide over, but by the time it comes to the final slide in the loop, the single line of code takes over 20 seconds to run each time. Thus, copying the first five slides might take <1 second, but the total 20 slides takes around 100 seconds in total, with all the latter slides taking longer and longer to run just this single line. The rest of the code races by!
The slow-down isn't linear, and gets even worse beyond 20 slides. It isn't related to the content on the final slide(s), but seems to be that as the number of SlideMaster "designs/themes" increases it slows exponentially to copy across the "sourceSlide.Design". I realise having a different "Design" object for each slide is a bit of a waste, but I don't own the initial source presentation, and often they do come to me like this, with just slight difference between the Designs for each slide. If I remove the "destinationSlide.Design" line the time it takes can reduce from 100+ seconds to around 1 second!
Method 2: In order to avoid this, and given I'm using PowerPoint 2010, I tried to use the following code, instead:
sourceSlide.Copy()
def destinationPresentation = objPpt.Presentations.Open(destinationFilename)
destinationPresentation.CommandBars.ExecuteMso("PasteSourceFormatting")
I believe this should provide direct access to PowerPoint 2010 "Paste with Source Formatting" functionality. However, this fails with a "null pointer exception" at the ExecuteMso("PasteSourceFormatting") line.
What am I doing wrong? Is there any way to speed up the slow line in Method 1? Why is method 2 not working at all? It looks like "destinationPresentation.CommandBars" is not null, but that the "ExecuteMso" line throws a null pointer exception.
Are there any other suggestions for efficiently "copy and paste" slides that should work in a reasonable time frame for 20-100 slides, even if there are multiple different designs/themes?
Thanks, in advance, for any ideas.
The problem with method 2 is I was using:
destinationPresentation.CommandBars.ExecuteMso("PasteSourceFormatting")
Whereas this should have been:
destinationPresentation.Application.CommandBars.ExecuteMso("PasteSourceFormatting")
Using this code, I no longer get the null pointer exception.
I've submitted this as an answer to assist anyone who makes a similar error in future.
That being said, I still find that the performance of this approach (method 2) is not significantly better than the manual "copy and paste formats" method (method 1). In both cases, the performance with "paste with source formatting" functionality is many, many times slower than a normal "paste" - and takes around 2 minutes to do the paste of about 20 slides (each with its own design template). This is reduced to less than a second if I use "destination formatting", or don't have each slide with its an individual design template.
This may just be an issue with PowerPoint 2010 performance, however, so I will accept this answer unless someone has more information that would provide a better solution to the performance aspect of the original query.
Not sure it helps, but you can do this sort of thing with Apache POI:
#Grab( 'org.apache.poi:poi-ooxml:3.10-beta1' )
import org.apache.poi.xslf.usermodel.XMLSlideShow
new File( '/tmp/Presentation1.pptx' ).withInputStream { p1 ->
new File( '/tmp/Presentation2.pptx' ).withInputStream { p2 ->
// Load our 2 presentations
inpptx = new XMLSlideShow( p1 )
outpptx = new XMLSlideShow( p2 )
// Add slide 1 from inpptx to the end of outpptx
outpptx.createSlide().importContent( inpptx.slides[ 0 ] )
// Save it out again to a 3rd presentation
new File( '/tmp/Presentation3.pptx' ).withOutputStream { out ->
outpptx.write( out )
}
}
}

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