How to set a timer in a UserForm Textbox? - excel

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.

Related

Tying to Display running stopwatch on a label on an user form in VBA?

I am trying to find some codes on Youtube to help me learn to display timer/stopwatch on a target label.
What I am trying to do is that the timer should start from zero upon clicking the start button, and pause upon clicking submit button, and reset to zero upon clicking the reset button.
The code I got so far is
Private Sub UserForm_Activate() //Doesn't work on renaming the function to StartTime()
Do
If CM = True Then Exit Sub
Label6.Caption = Format(Now, "hh:mm:ss")
DoEvents
Loop
End Sub
"Now" function starts from current time , but I want it to start from zero. I tried something like this:
Tick = Time + TimeValue("00:00:01")
label6.Caption = Format(Tick - t - TimeValue("00:00:01"), "hh:mm:ss")
But it doesn't seem to work this is also starting my timer automatically when I run the program. Any suggestions on how I may be able to achieve my objective on my target label?

Is there a way to cancel Userform_Terminate if conditions are not met?

I have an Excel utility that reads/writes tag values to and from a PLC. Some of the tag values are Boolean, therefore range checking is very important. I created a Userform_Termiate() event that first calls a range checking sub, and then updates the sheet that houses the actual data for the form. My question is, is there anyway I can cancel the termination of the user form if the input is not in range? I will post what I have now, which just tells the user the input is not in range, cancels the update for the sheet, but then still closes the form.
Private Sub Userform_Terminate()
Call CheckNumericRange
If Check = True Then
UpdateC1Sheet
Else
Exit Sub
End If
End Sub
UserForm_Terminate event occurs on closure of the userform. For example, if you close (Manually)/ unload(With procedure) the userform, the following procedure will give the message box.
Private Sub UserForm_Terminate()
MsgBox "You closed the useform"
End Sub
So, if you want to keep the userform open while UpdateC1Sheet is running, then first confirm the public boolean variable Check variable is set to True in some other macro. .. OR .. If it is set to True in CheckNumericRange procedure, you can also add in the same procedure after it is set to True -:-
If Check = True Then
UpdateC1Sheet
End If
Rather than the Terminate event, from my understanding of your intentions, you'd be better off using the QueryClose event.
Straight from the MS documentation online;
QueryClose event:
Occurs before a UserForm closes.
Terminate event:
Occurs when all references to an instance of an object are removed from memory by setting all variables that refer to the object to Nothing or when the last reference to the object goes out of scope.
So in layman's terms, say you click the red X close button or have some close my userform CommandButton on your form, the QueryClose event fires which has a Cancel part - allowing you to catch the close operation and decide if you want to do cancel the close event or not. If you don't cancel the close opearation in the QueryClose event, the form is unloaded from memory and thus the Terminate event is fired.
You could change your code to the following, to evaluate Check and either cancel closing the form or not.
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
Call CheckNumericRange
If Check = True Then
UpdateC1Sheet
Else
Cancel = 1 '1 = True
End If
End Sub

Stop previous loops and only run the last one

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.

Drawing shapes to screen falling behind other code

I am trying to create a small tutorial for one of my Excel Applications and I am running into the issue where I'm trying to draw a text shape to the screen to give advice on what to enter into an InputBox but the InputBox gets displayed before the text shape, however, when running in debug mode and stepping through the code it all works fine.
There is one userform ufNext which only contains one button, ufNext. The click event code for this button contains a Select Case clause to determine what to do each time it is clicked. The value the clause is checking is a Public variable, tutSectionsRun
Option Explicit
Private Sub btnNext_Click()
Select Case tutSectionsRun
Case 1
Call Section2
Case 2
Call Section3
Call MPFilterString
' Case N
' ...
End Select
End Sub
The code starts in Section1 which just sets the position of ufNext and shows the form then sets the global variable tutSectionsRun to 1.
The user clicks the "Next" button on the ufNext form and it calls Section2 which re-positions the form (there would normally be other code in these "Section" procedures), and sets the global variable to 2.
Again, the user clicks the "Next" button but this time there is the issue where before the shapes are drawn to the screen, I get the InputBox popping up first and only after it closes the text shape tutText is drawn to the screen.
Option Explicit
Public tutSectionsRun As Long
Sub Section1()
ufNext.Left = 550
ufNext.Top = 450
ufNext.Show
tutSectionsRun = 1
End Sub
Sub Section2()
ufNext.Left = 910
ufNext.Top = 350
tutSectionsRun = 2
End Sub
Sub Section3()
Dim tutText As Shape
Set tutText = ActiveSheet.Shapes.AddLabel(msoTextOrientationHorizontal, 600, 300, 200, 100)
tutText.TextFrame2.TextRange.Text = "Enter the string ""gr"" into the input box."
tutText.Locked = False
ufNext.Hide
tutSectionsRun = 3
End Sub
Sub MPFilterString()
Dim s As Variant
Application.ScreenUpdating = False
s = Application.InputBox("Enter string to filter out.", "Filter String.")
If s = False Then Exit Sub
End Sub
**Edit : I forgot to mention that the userform is non-modal. Otherwise execution would pause on the call to ufNext.Show and clicking next would call the event handler before the tutSectionsRun variable had been set to 1
Thanks to #BrakNicku who confirmed my suspicions in the comments, saying that the InputBox was preventing the screen from being refreshed for the text shape from ActiveSheet.Shapes.AddLabel to be displayed.
Their link to this answer offered some suggestions.
What I found was that adding either of these before the InputBox was called would force the screen to refresh but only if Application.ScreenUpdating = False was removed, or at least just moved further down in the code.
- ActiveSheet.Calculate
- ActiveWindow.SmallScroll
- Application.WindowState = Application.WindowState
I always like to call Application.ScreenUpdating = False at the top of my procedures, so I went with a different approach, thinking that the problem was that the time to refresh the screen with shapes drawn was longer than the time between the instruction to do so and the instruction to draw the InputBox to screen. So, I thought a slight delay before calling the InputBox might be a better choice for me, probably not for everyone but I felt better about doing it this way. (Application.ScreenUpdating = False is a good friend of mine and I didn't want to see her go, or even be relocated) So I just made a small wait procedure.
Sub Wait(secs As Single)
Dim finishTime As Single
finishTime = Timer + secs
Do While Timer < finishTime
DoEvents
Loop
End Sub
And called it at the top of the MPFilterString procedure. It only takes about 50ms for the shapes to show on screen but I give it 100ms to be safe.
Sub MPFilterString()
Dim s As Variant
WaitFor (0.1)
Application.ScreenUpdating = False
s = Application.InputBox("Enter string to filter out.", "Filter String.")
If s = False Then Exit Sub
' more code ...
End Sub

VBA to prevent a change event from triggering if another change event triggers

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.

Resources