I've written a macro which is time consuming (it works for a few hours); that's why I want to add two things to my UserForm, to manually stopping the macro.
First button starts the macro. Let's assume that that code of this macro looks like:
For i = 1 to 10000
DoEvents
If isCancelled Then Exit Sub
Next i
I was thinking about adding an additional "Stop" button, which changes isCancelled from False to True, but the button is locked and can't be clicked during macro execution. Is there any way to enable this button? Or maybe there is a better way to manually stop the macro?
Conceptually, yes this is possible and can be illustrated with a simple example. This is essentially the type of code you alluded to.
Assume your UserForm has two buttons, which start (or resume) and stop the procedure respectively.
Option Explicit
Public isCancelled As Boolean
Public iVal As Long
Private Sub CommandButton1_Click()
Dim i As Long
If iVal = 0 Then iVal = 1 'Allows the user to resume if it's been "stopped"
isCancelled = False
For i = iVal To 100000
iVal = i
If i Mod 1000 = 1 Then
Debug.Print i
End If
If isCancelled Then
GoTo EarlyExit
Else
DoEvents
End If
Next
EarlyExit:
End Sub
Private Sub CommandButton2_Click()
isCancelled = True
End Sub
Of course, implementing the "continuation" option which I did here is a neat little trick, but it may be increasingly complicated depending on the complexity of your procedure, it's dependencies, etc. and if your form is displayed vbModeless you'll need to ensure the user doesn't alter the environment in such a manner as to introduce a runtime error, etc.
You may also look to optimize your procedure if runtime is several hours.
you need to use DoEvents in the loop to catch user responds.
see this answer, which might help you as well.
Related
Problem
I have a macro (I'll call it launch_macro) which is launched by double-clicking in an Userform ListBox (ListBox1_DblClick).
My problem is that if the user double-click again while the macro is still running, the macro will be launched again as soon as the first execution is finished, regardless of the fact that I'm disabling ListBox while the macro is running.
Code and tests
Private sub ListBox1_DblClick(Byval Cancel as MSForms.ReturnBoolean)
(....Logging...)
If Not Cancel Then
Me.ListBox1.Enabled = False
(...DisplayStatusBar / ScreenUpdating / ListBox1.BackColor...)
launch_macro
(...DisplayStatusBar / ScreenUpdating / ListBox1.BackColor...)
Me.ListBox1.Enabled = True
End If
End sub
It seems like Excel records/queues the ListBox1_DblClick events (for future execution) while the associated ListBox is disabled. Why that ? How can I prevent this ?
I also tried with no success :
Locked : Me.ListBox1.Locked = True
Doevents : Adding DoEvents after Me.ListBox1.Enabled = False
EnableEvents :Application.EnableEvents = False
macroLaunched variable :Using a variable to check if the macro is already launched (macroLaunched = True at the beginning of the ListBox1_DblClick event and macroLaunched = False at the end). This doesn't work since the second execution is launched after the end of the first event (thus the variable is set back toFalse). (And setting the variable back to False outside the scope of the Dbl_Click event is not acceptable since the user need to be able to launch the macro immediately again (but just not while the first execution is still running)).
Adding delay (for test purpose only) : I added a 10s delay (Application.Wait) right back after the launch_macro. I then double-clicked twice within 1s. The second execution still launched. I checked by logging : the 2nd ListBox1.Dbl_Click event is 'recorded' by Excel 12s after the first event.
Note : I'm using Office Standard 2013
Current 'solution'
This trick is adapted (to reduce delay) from A.S.H answer :
Private sub ListBox1_DblClick(Byval Cancel as MSForms.ReturnBoolean)
Static nextTime As Single
If Timer < nextTime then
Log_macro "Event canceled because Timer < nextTime : " & Timer
Exit Sub
End if
(....Logging...)
If Not Cancel Then
(...DisplayStatusBar / ScreenUpdating / ListBox1.BackColor...)
launch_macro
(...DisplayStatusBar / ScreenUpdating / ListBox1.BackColor...)
End If
nextTime = Timer + 0.5
Log_macro "nextTime = " & nextTime
End sub
It 'does the trick' but but I still don't like that ListBox1 is still enabled and Excel is still queueing events, thus I need to estimate how many time the user might Dbl_Click (depending on how long the macro takes) to estimate how much a delay I need (currently 0.5s to be able to handle (and log) at least 10 canceled events). Also, it seems like Excel doesn't really like (in regards to performance) queuing events while the macro is running.
Well I will post my suggestion, I hope you try it because may be it was misunderstood. The idea is that once the macro is finished, we set a delay of n seconds (say 2 seconds) before handling again the double-click event. This way, the dbl-clicks that were queued during the macro's execution are handled with no effect during these two seconds.
Private Sub ListBox1_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
Static NextTime As Variant ' Will set a barrier for launching again the macro
If Not IsEmpty(NextTime) Then If Now < NextTime Then Exit Sub
ListBox1.Enabled = False
' Any event code
launch_macro
' ...
ListBox1.Enabled = True
NextTime = Now + TimeSerial(0, 0, 2) ' dbl-click events will have no effects during next 2 seconds
End Sub
You could use a variable to lock the critical section of code for a set amount of time.
The example below locks the critical part of Test() function in Sheet2 of an Excel workbook.
Option Explicit
Private booIsRunning As Boolean
Private Sub Test()
If Not booIsRunning Then
booIsRunning = True
Debug.Print "Hello."
Application.OnTime Now + TimeValue("00:00:02"), "Sheet2.UnlockTest"
End If
End Sub
Public Sub UnlockTest()
booIsRunning = False
End Sub
I have an Excel VBA macro that runs for a long time (potentially for days, it performs data acquisition). It was originally written for Excel 2003 and has custom toolbars and menus.
I recently updated it to use a ribbon interface using RibbonXML.
When the macro is running I want to disable some interface elements (such as "start test"), and enable others (such as the "stop test" button.)
The problem I have is that calls to ribbon.invalidate are only processed after the macro code has run to completion.
You can see this effect quite easily with a simple test program
Sub test()
ribbon.Invalidate
DoEvents
Sleep (5000)
End Sub
A debug.print in the ribbon "getEnabled" callback will be seen to only be actioned at the end of the 5 second sleep.
Is there any way to force a ribbon.Invalidate to be activated there and then?
:: Edit 1 ::
I have created a small demo workbook to ilustrate the issue:
http://www.bodgesoc.org/Button_Demo.xlsm
:: Edit 2 ::
A member of a different forum found a solution, though it is a slightly ugly one.
I guess this can now be marked as "answered" but a more elegant solution would be appreciated.
Application.ScreenUpdating = False
Application.ExecuteExcel4Macro "Show.ToolBar(""Ribbon"",False)"
Application.ExecuteExcel4Macro "Show.ToolBar(""Ribbon"",True)"
Application.ScreenUpdating = True
I don't know if this is any less ugly than what you have, but...
'Callback for Run onAction
Sub Run_r(control As IRibbonControl)
SetRunning "Run_r_procedure"
DoEvents
End Sub
Sub Run_r_procedure()
Dim T As Single
Sheet1.Range("A10").Value = "Run pressed, button states changed, and ribbon invalidated. Waiting 5 seconds in loop"
T = Timer
Do While Timer - T < 5
DoEvents
Loop
Sheet1.Range("A10") = ""
End Sub
And then in SetRunning
Sub SetRunning(ByVal ProcToRun As String)
Sheet1.Range("B1") = "Disabled"
Sheet1.Range("B2") = "Disabled"
Sheet1.Range("B3") = "Enabled"
Sheet1.Range("B4") = "Enabled"
Sheet1.Range("B5") = "Enabled"
Sheet1.Range("B6") = "Enabled"
Sheet1.Range("B7") = "Enabled"
myRibbon.Invalidate
myRibbon.InvalidateControl ("Run")
Application.OnTime Now, ProcToRun
End Sub
So you have to have two procedures per callback - the callback and then whatever SetRunning will call that does the actual work. The code is just as ugly, but the UI is little less strange looking to the user.
I have a procedure that consists of several do and for loops and i would like to find an easy way to 'pause' the routine and allow the user to edit the sheet, with a msgbox or userform to resume execution where it left off.
I would like to do something like this
dim pause as boolean
pause=false
For i = 1 To 40
Worksheets("sheet1").Range("A" & i) = i
If i = 20 Then
UserForm1.Show vbmodeless
Pause = true
Do until pause = false
loop
Else
End If
Next i
End Sub
Where the pause condition would be set by a sub on the userform. This do loop just crashes.
Ideally i would like the userform to have buttons that can run subs but also allow direct editing of cells while execution is paused.
Here is a typical control structure that allows the user to perform some actions in the middle of a macro. When the user is done, they run OKToContinue to allow the macro to continue with the second part:
Dim AllowedToContinue As Boolean
Sub FirstPartSecondPart()
AllowedToContinue = False
MsgBox "allow user to perform actions"
Do Until AllowedToContinue
DoEvents
Loop
MsgBox "doing second part"
End Sub
Sub OKToContinuw()
AllowedToContinue = True
End Sub
i have created macro for excel but it seems somewhere i have done something wrong,
i want to fetch an image from a URL and then update it up to 1 second (more or less)
Sub GetPicture()
PictureURL = "This is where i put the URLi want"
Set MyPict = ActiveSheet.Pictures.Insert(PictureURL)
Cells(1).Value = Now
nextTime = Now + TimeValue("00:00:01")
End Sub
when i run the macro doesn't do anything,only when i press f5 the it updates as fast as i press f5,also what is the value to update less than 1 second ("00:00:01"),when i try ("00:00:0.5") it comes up with "run time error 13" "type mismatch"
Any help is very much apreciated.
In Excel, you can use VBA to trigger code that updates a Worksheet on specific intervals. The code below shows how you would activate a Timer each time the Worksheet is activated by a user. Whenever the Timer fires (on 1 second intervals here) this code updates Cell A1 in the ActiveSheet with the current Time.
To further customize, you would add code to the OnTimerMacro in order to update a Picture or whatever else you recurring task might be.
(Props to Hartmut Gierke for his post on the topic.)
Option Explicit
Dim Execute_TimerDrivenMacro As Boolean
Sub Start_OnTimerMacro()
Execute_TimerDrivenMacro = True
Application.OnTime Time + TimeValue("00:00:01"), ActiveSheet.Name & ".OnTimerMacro"
End Sub
Sub Stop_OnTimerMacro()
Execute_TimerDrivenMacro = False
End Sub
Public Sub OnTimerMacro()
If Execute_TimerDrivenMacro Then
' Do something e.g. put the actual time into cell A1 of the active sheet
ActiveSheet.Cells(1, 1).Value = Time
' At the end restart timer
Application.OnTime Time + TimeValue("00:00:01"), ActiveSheet.Name & ".OnTimerMacro"
End If
End Sub
Private Sub Worksheet_Activate()
'Start the timer driven method when opening the sheet
Start_OnTimerMacro
End Sub
Private Sub Worksheet_Deactivate()
'Stop the timer driven method when opening the sheet
Stop_OnTimerMacro
End Sub
if you would like the macro to repeat you have to put it in a do...until loop. The only problem, is that you can't really have the macro run all the time. There has to be a way to stop it. The do...until loop will help with this, but you have to come up with a reasonable exit from the loop. Can you give a little more background as to what you ultimately want this to do?
Also it sounds like you want the running of the macro to be triggered by something other than the pressing of F5. Can you explain when you would like to see it start?
How can I make excel continuously calculate a sheet/range in realtime (not 1 calc/sec) and do it in the background?
I want this metric clock to run like a stopwatch....
=IF(LEN(ROUND((HOUR(NOW())*(100/24)),0))=1,"0"&ROUND((HOUR(NOW())*(100/24)),0),ROUND((HOUR(NOW())*(100/24)),0))&":"&IF(LEN(ROUND((MINUTE(NOW())*(100/60)),0))=1,"0"&ROUND((MINUTE(NOW())*(100/60)),0),ROUND((MINUTE(NOW())*(100/60)),0))&":"&IF(LEN(ROUND((SECOND(NOW())*(100/60)),0))=1,"0"&ROUND((SECOND(NOW())*(100/60)),0),ROUND((SECOND(NOW())*(100/60)),0))
I've used the following to produce the effect you are looking for:
Option Explicit
Public TimerRunning As Boolean
Dim CalculationDelay As Integer
Public Sub StartStop_Click()
If (TimerRunning) Then
TimerRunning = False
Else
TimerRunning = True
TimerLoop
End If
End Sub
Private Sub TimerLoop()
Do While TimerRunning
'// tweak this value to change how often the calculation is performed '
If (CalculationDelay > 500) Then
CalculationDelay = 0
Application.Calculate
Else
CalculationDelay = CalculationDelay + 1
End If
DoEvents
Loop
End Sub
StartStop_Click is the macro that I tie to the Start/Stop button for the stopwatch. You can get fancy, and change its name to "Start" or "Stop" depending on the value of TimerRunning, but I kept things simple to illustrate the concept.
The two key things here are:
Application.Calculate
Which forces Excel to calculate the worksheet, and:
DoEvents
Which allows VBA to run in the background (i.e. Excel does not stop responding to user input). This is what allows you to still press the "Stop" Button even though the timer is running.
I think this might fail your "(not 1 calc/sec)" criteria, but I achieved something similar as follows. Assumes your formula is in cell A1 of a worksheet named Sheet1.
In the ThisWorkbook code module:
Private Sub Workbook_Open()
Application.OnTime Now + TimeValue("00:00:01"), "RecalculateRange"
End Sub
... and in a regular code module:
Public Sub RecalculateRange()
Sheet1.Range("A1").Calculate
Application.OnTime Now + TimeValue("00:00:01"), "RecalculateRange"
End Sub