I have the following macro. As a short description, every time you select a new cell, it puts a reference text in the status-bar, as a scrolling text. I make it so it does not disrupt user input by using "Do... While loop" with "Do events", will share the code below.
The problem is if i quickly click on multiple cells, it remembers previous loops and runs them all. How can I set the macro to stop running all the loops except for the last one?
Option Explicit
Public STATUSTEXT As String
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
STATUSTEXT = "Some text valid only for current selection"
'FILL THE TEXT AND GOTO EXIT SUB TO SKIP RELEASING STATUSBAR
charTotal = Len(STATUSTEXT)
charI = 1
Do
charI = charI + 1
Application.statusBar = Right(STATUSTEXT, charI)
DoEvents '# IMPORTANT
Loop Until charI >= charTotal
Application.statusBar = STATUSTEXT
End Sub
Ok I try a better explanation of what actually happens.
Your arrow down triggers the event Worksheet_SelectionChange. Then this macro runs and gets into the loop with DoEvents. This DoEvents allows you to move your cursor again and to do other events. That means your first SelectionChange gets actually halted at DoEvents and second SelectionChange triggers. After this second event is finished you will get back to the DoEvents of the first SelectionChange event and it continues until the end.
So what happens your events get stacked because VBA cannot multi-thread. It just halts the first macro at DoEvents triggers the next event and continues the first event after:
1. SelectionChange
2. SelectionChange
3. SelectionChange
…
3. SelectionChange Ends
2. SelectionChange Ends
1. SelectionChange Ends
So it looks like the first SelectionChange event run last just becaus it run the others in between. Note this is no multi-threading, the code was halted.
The actual issue came from your DoEvents if you remove it. Excel blocks the user interface untile the first SelectionChange event is finished and does not allow to run another event until the first finished. And whoops you will see it ends where your cursor stopped. But it is slow becaus it waits for the event until it performs the next arrow down.
So actually you just need to cancel the first event in case another event run. Try the following:
Option Explicit
Public GlobalStatusText As String
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim PrivateStatusText As String
PrivateStatusText = "Some text valid only for current selection " & Target.Address
GlobalStatusText = PrivateStatusText
Dim charTotal As Long, charI As Long
charTotal = Len(GlobalStatusText)
charI = 1
Do
charI = charI + 1
Application.StatusBar = Right(GlobalStatusText, charI)
DoEvents '# IMPORTANT
If GlobalStatusText <> PrivateStatusText Then Exit Sub 'cancel because another event run inbetween
Loop Until charI >= charTotal
Application.StatusBar = GlobalStatusText
End Sub
This will cancel the previous events if another event changed the status text in between.
Related
I have a code that works perfectly but I have to click on the command button to activate it.
What I want is for the timer to run automaticaly when I open the workboox/UserForm.
When the timer reach 0 seconds, I want the workbook to automatically close using
Workbooks("OUTIL_CRN.xlsm").Save
Workbooks("OUTIL_CRN.xlsm").Close
This is the code that works with a commmand button:
In a module:
Public Const AllowedTime As Double = 1
In the Userform:
Private Sub CommandButton1_Click()
Dim userClickedPause As Boolean ' Gets set to True by the Pause button
Dim stopTime As Date
userClickedPause = False
' If AllowedTime is the number of minutes with a decimal part:
stopTime = DateAdd("s", Int(AllowedTime * 600), Now) ' add seconds to current time
' If AllowedTime is the number of seconds:
'stopTime = DateAdd("s", AllowedTime, Now) ' add seconds to current time
Do
With UserForm1.TextBox1
.Value = Format(stopTime - Now, "Nn:Ss")
End With
DoEvents
If userClickedPause = True Then
Exit Do
End If
Loop Until Now >= stopTime
End Sub
Private Sub CommandButton2_Click()
userClickedPause = True
End Sub
A lot depends on how you're showing the form. If you have this:
UserForm1.Show
Then the form is modal, the default instance is shown, and you could handle the Activate event to execute code as soon as the dialog becomes active.
If you have this:
UserForm1.Show vbModeless
Then the form isn't modal, and the Activate event will be a problem, because the timer will then re-initialize whenever the user clicks on the form after deactivating it by clicking somewhere else on the screen.
If you have this:
With New UserForm1
.Show
End With
Then the TextBox1.Text property isn't being assigned on the instance that's being shown, and the box remains empty. However with that we now fully control when the form object gets created and destroyed, and now we can handle the Initialize event (fired once per instance, when the object is first created) because we know that the form object has just been created... but then, the form won't ever be shown, because the only available thread is busy with that Do...Loop, and while it allows the dialog's message pump to pump events (thanks to DoEvents), if the form isn't shown by the time that loop is entered, then no other code gets to run - so, firing that loop in an Initialize handler would be a very bad idea.
Consider looking into leveraging Application.OnTime to schedule the execution of a procedure: what you want to do, is to say "invoke DoStuff in 1 second", and then resume executing whatever - not busy-looping, rather letting the form's message loop do its job. See this post for one of many examples.
I have a workbook with multiple Worksheet_Change events in the sheet.
Each of these macroes are related to an active x combobox.
The problem is that when I change one of the comboboxes the macro fires (as expected), which in turn start another macro (which is not suppose to run). I have already set the Application.EnableEvents = False
But the issue might be that I am changing a cell.value, which is linked to another combobox and hence also linked to another worksheet_change event.
A workaround as I see it, might be to only run the macro, if the combobox is the one actually selected, but here comes the second problem. I can't find a way to have vba return the name of the active combobox.
Please note that these comboboxes is not connected to a userform, they are simply placed directly on the worksheet.
Is there anybody who has any idea on how to solve this??
Any help is much appreciated,
I see that there are 2 possible solutions...
1 - as stated by Gary's Student, you may have toggled the Application.EnableEvents somewhere unknowingly and you could try and trap where that happens.
2 - Maybe set a global boolean variable called, say DontRunMacros and set it to TRUE at the start of the first macro and to FALSE at the end. Then you simply have each other macro start with If DontRunMacros Then Exit Sub - That will prevent the others running regardless of the events that fire...
Hope that helps!
You could also set a global variable handling events. You check it at the beginning of each change event.
Dim ufEventsDisabled As Boolean
Sub YourSub()
ufEventsDisabled = False
Range("A1").Value=1 'This triggers the event
ufEventsDisabled = True
Range("A1").Value=1 'This doesn't trigger the event
End Sub
Private Sub Worksheet_Change(ByVal Target As Range)
If ufEventsDisabled=True Then Goto ExitEvent:
'Your regular worksheet code
ExitEvent:
ufEventsDisabled=False
End Sub
There is probably an error somewhere that is re-Enabling Events...........You can always insert:
MsgBox Application.EnableEvents
at points in your code to trap this.
Thanks a lot for the quick responses.
I ended up using a named cell in the worksheet, similar to what hstay sugested.
If ThisWorkbook.Sheets("MD").Range("AllowMacro").Value = 0 Or Me.Name <> ActiveSheet.Name Then Exit Sub
However as I need the worksheet to calculate some cells while the code is running, I can't set application.Calculation = xlManual.
So the code still tries to execute a bunch of other change events, but the code above stops them from running more than just the first line. This however still takes a lot of time, which is quite frustrating. Guess I'll just need to take this in to account another time.
This is how I begin and end all my worksheet_chnage events:
If ThisWorkbook.Sheets("MD").Range("AllowMacro").Value = 0 Or Me.Name <>
ActiveSheet.Name Then Exit Sub
ThisWorkbook.Sheets("MD").Range("AllowMacro").Value = 0
Application.ScreenUpdating = False
Application.EnableEvents = False
'some random code that trigger other change_events
Application.EnableEvents = True
Application.ScreenUpdating = True
ThisWorkbook.Sheets("MD").Range("AllowMacro").Value = 1
I have a macro that needs to be run for many workbooks. There is a part though where I update a segment of the connection string with unique values and apparently this can't be done in VBA so I must manually do it. Is there a way I can have the Macro do what it does then pause while I update the connection string for say 30 seconds then start running again to completion?
Thanks,
Here is a macro that will allow you 30 seconds to make changes and then resume operation:
Sub dural()
'first part
'do stuff
MsgBox "Perform updates"
t1 = Now
While Now < t1 + TimeSerial(0, 0, 30)
DoEvents
Wend
MsgBox "Resuming the second part of the macro"
'macro does more stuff
End Sub
NOTE:
the DoEvents allows focus to be shared with the user.
this one may be impossible to solve in VBA but I'd like to see what you experts have to say about it.
I have a textbox on a userform that triggers a macro within a TextBox1_Change() type of sub.
If the user types "ABC" in the textbox, the macro gets triggered 3 times: once for "A", once for "AB" and once for "ABC". This macro is actually kind of heavy, so I would like it to run only when the user is actually done typing, and not inbetween single key strokes.
I know I can make the user "press enter" or whatever and only then run the macro, but this is not what I'm looking for. I want him to type freely and see the results of his typing dynamically show up, with no other type of interaction required.
So, I came up with the idea of making the change event wait and see if another change event gets triggered within, say, 1 second from the first. If that happens, the first change event aborts.
Now this would work, and I think I would know how to code it, except that I don't know how to give the user the power to keep typing even when the first change event is running.
What I mean is that when the first macro runs, it "freezes" everything. Waiting to see if another change event triggers will therefore not work, as nothing is going to trigger until the first macro is done running.
Do you guys see my problem here? How would you go about this? Any chance I can achieve the results I'd like?
Any help is greatly appreciated, thanks guys!
I tested the following, and it works (assuming I correctly understand what you're trying to do).
In a code module, write this:
Public aRunIsScheduled As Boolean
Public nextRunTime As Variant
Sub MyMacro()
'Flag macro as having been run, no longer scheduled.
aRunIsScheduled = False
'Place your macro code here.
'I'll just use some dummy code:
MsgBox "MyMacro is running!"
End Sub
In your sheet module:
Private Sub CommandButton1_Click()
If aRunIsScheduled Then
' Cancel the previously scheduled run.
Application.OnTime EarliestTime:=nextRunTime, _
Procedure:="MyMacro", Schedule:=False
aRunIsScheduled = False
End If
' Schedule a new run 3 seconds from now:
nextRunTime = Now + TimeValue("00:00:03")
Application.OnTime EarliestTime:=nextRunTime, _
Procedure:="MyMacro", Schedule:=True
aRunIsScheduled = True
End Sub
I put a Commandbutton in my sheet and here I'm using its change event, but you can put this code in your TextBox1_Change() event instead, in exactly the same way.
reference: http://www.cpearson.com/excel/SuppressChangeInForms.htm
To suppress events in a form, you can create a variable at the form's module level called "EnableEvents" and set that to False before changing a property that will cause an event to be raised.
Public EnableEvents As Boolean
Private Sub UserForm_Initialize()
Me.EnableEvents = True
End Sub
Sub Something()
Me.EnableEvents = False
' some code that would cause an event to run
Me.EnableEvents = True
End Sub
Then, all of the controls on form should have a test if that variable as their order of business in any event code. For example,
Private Sub ListBox1_Change()
If Me.EnableEvents = False Then
Exit Sub
End If
MsgBox "List Box Change"
End Sub
You can declare the EnableEvents as Private if only procedures with that form need to suppress events. However, if you have forms that are programmatically linked together, such UserForm2 adding an item to a ListBox on UserForm1, you should declare the variable as Public and set it for another form with code like the following:
UserForm1.EnableEvents = False
'
' change something on UserForm1
'
UserForm1.EnableEvents = True
The primary difference between the EnableEvents property and code shown above and the Application.EnableEvents property is that with a UserForm EnableEvents, all control on the form must have code to exit if EnableEvents is True. In other words, all the form's controls must cooperate and respect the setting of EnableEvents.
I have an Excel workbook containing some ComboBox controls placed directly on the sheets. These are standard combo boxes from the Forms toolbar.
When the user uses "Save As" to save the workbook with a different name, this triggers the Change event on all the combo boxes, including ones on sheets that aren't active. This seems unreasonable as the selection hasn't actually changed. This causes various undesirable behaviour because of the code in the event handlers. The event isn't triggered on a simple "Save".
Google suggests this is a known problem in Excel. There are rumours that it's caused by using a named range as the ListFillRange for the combo box, which I have done, although it's not a volatile name. I'm looking for a way to prevent this happening with minimal changes to the code and the spreadsheet. Does anyone have a proven solution?
I did the following in a new workbook with only one sheet, Sheet1, and it seemed to work to diable events before save and then reenable them after. It should bypass the problem you see by mimicing an AfterSave event. This is my event code on Sheet1 (could be OLEObject code, too)
Option Explicit
Private Sub Worksheet_Change(ByVal Target As Range)
MsgBox Target.Address & ": " & Target.Value
End Sub
This is my ThisWorkbook code
Option Explicit
Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
' To see the change code work before disabling
' Should show a message box
Sheet1.Range("A1") = "Before After Events Off"
Application.EnableEvents = False
Application.OnTime Now, "ThisWorkbook.Workbook_AfterSave"
' This time it will not show a message box
' You will never see this one . . .
Sheet1.Range("A1") = "After After Events Off"
End Sub
Private Sub Workbook_AfterSave()
Application.EnableEvents = True
End Sub
The .OnTime method throws the AfterSave "event" onto the execution queue. It works!
You could set a flag in the Workbook's BeforeSave event and then check that flag before processing a change event in each of the combo boxes. There does not seem to be an AfterSave event, so you would need to clear the flag after checking it within the combo box change events. The flag would need to be more than a simple boolean since it could not be turned off until all combo box change events were processed. Here's some sample code:
Public ComboBoxChangeCounter As Integer
Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
Const NumOfComboBoxChangeEvents As Integer = 5
ComboBoxChangeCounter = NumOfComboBoxChangeEvents
End Sub
Function JustSaved() As Boolean
If ComboBoxChangeCounter > 0 Then
ComboBoxChangeCounter = ComboBoxChangeCounter - 1
JustSaved = True
End If
End Function
Private Sub Combo1_Change()
If JustSaved Then Exit Sub
'Your existing code '
' ... '
' ... '
' ... '
End Sub
I set the number of combo box change events as a constant, but there may be some way for you to determine that number programmatically. This workaround does require adding code to every combo box change event, but it should be easy as all you need to do is copy and paste the line If JustSaved Then Exit Sub at the beginning of each event.
This workaround assumes that the Workbook BeforeSave event will get called prior to the combo box change events. I don't know for a fact if that's the case.