Excel allows to start with a Modeless form and then display a Modal (but not the other way around)
I have an app with 4 Userforms : UF1 - Select a partner, UF2 - List existing transactions, UF21 - Display an existing transaction, UF22 - Make a new transaction. UF21 and UF22 both stem from UF2.
UF21 needs to be Modeless in order to display more than one transactions and compare side by side, therefore UF1, UF2 and UF21 are all Modeless. But I want UF22 to be Modal in order to issue one new transaction at a time.
My problem is that after I close UF22, even just ESCaping from the form right off the bat, all previous forms close. I should be able to return to UF2. If I make UF22 Modeless all is ok.
I have written a function to traverse the UserForms Collection and I am able to get a reference to the object of the Form I want to activate. So, I am able to return (in debug mode) to UF2 which is a listbox, activate the list box, but after the last pending statement both UF2 and UF1 close.
Is what I am trying to do impossible due to the nature of the Modal and Modeless forms or should I keep pushing for the correct code?
Since my original question is still open and my tested implementation of the proposed solution by #PeterT is not working properly, I include the code I have for the moment, based on #PeterT 's suggestion.
'===============
' Form UF1
'===============
Private Sub UserForm_Activate()
If ActivateUF22(FormID) = True Then Exit Sub
'.... more commands
End Sub
'============
' Form UF2
'============
Private Sub UserForm_Activate()
If ActivateUF22(FormID) = True Then Exit Sub
'.... more commands
End Sub
'----------------
Private Sub Cbn_OpenUF22_Click()
If ActivateUF22() = True Then
Exit Sub
Else
With New UF22
.Show vbModeless
End With
End If
End Sub
'================
' In a Module...
'================
Public Function ActivateUF22() As Boolean
Dim frm As Object
Set frm = GetFormFromID("UF22*") ' Custom function to get a form Object based on
' some criterion (FormID in a hidden TextBox)
If Not frm Is Nothing Then
' the only way I know to *Activate* an already .Show(n) form and compensate
' for the fact that the Close CommandButton may already have Focus
frm.TBx_UF22_CODE.SetFocus
frm.CBn_UF22_CLOSE.SetFocus
ActivateUF22 = True
Else
ActivateUF22 = False
End If
End Function
Well I finally managed to get the workaround to behave.
The remaining problem was the fact that clicking twice in a row on the same userform, besides the "Modal" one, would succeed and allow the user to break out.
I even tried the "AppActivate Application.caption" approach found in another SO thread but that didn't work either.
The only solution that works and does not bother me is to insert a MsgBox with a warning to the user, as such:
Public Function ActivateUF22() As Boolean
Dim frm As Object
Set frm = GetFormFromID("UF22*") ' Custom function to get a form Object based on
' some criterion (FormID in a hidden TextBox)
If Not frm Is Nothing Then
' the only way I know to *Activate* an already .Show(n) form and compensate
' for the fact that the Close CommandButton may already have Focus
frm.TBx_UF22_CODE.SetFocus
frm.CBn_UF22_CLOSE.SetFocus
ActivateUF22 = True
MsgBox("You cannot move away from this form until it is either completed or cancelled")
Else
ActivateUF22 = False
End If
End Function
Displaying the MsgBox does the trick internally, switches the focus to a different form from the one clicked and, upon return, the UserForm.Activate event fires normally and the ActivateUF22 function prevents the user from escaping the Pseudo-Modal form.
Thanks #PeterT for pointing me to a workaround. I managed to do what I set out to do, albeit in a different manner.
PS I still believe that there is a way to switch from a Modeless form to a Modal one. After all the MsgBox I use is obviously a Modal form and works just as I would like ;-)
Related
I have researched several questions and answers, but was unable to solve my problem.
I have a workbook that when it opens then a UserForm is shown, let's call it the main form.
On the main form there are several controls, one specific checkbox when clicked and it's value is changed to TRUE then it opens another UserForm, let's call this one the secondary form.
This secondary form is a simple password prompt form, and it's code name is pswdPrompt.
The main form code name is usf_TDM.
Now the code inside the pwsdPrompt is the following:
Option Explicit
Private Sub btn_input_Click()
Call passwordSubmit
Me.Hide
End Sub
Private Sub UserForm_Initialize()
With usf_TDM
Me.Top = .Top + .Height / 2
Me.Left = .Left + .Width / 2
End With
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = vbFormControlMenu Then
Call closePswdForm
End If
End Sub
The called subs passwordSubmit and closePswdForm are inside a module:
Public Sub passwordSubmit()
Application.enableEvents = False
With pswdPrompt
If .txt_password.Value = unblockPassword Then
usf_TDM.c_unblockForced.Value = True
Else
usf_TDM.c_unblockForced.Value = False
End If
.txt_password.Value = ""
End With
Application.enableEvents = True
End Sub
Public Sub closePswdForm()
pswdPrompt.Hide
End Sub
When I get to the pswdPrompt.Hide command, the secondary form dissapears BUT after inserting a breakpoint on the QueryClose event of the main form, I discovered that after the secondary form dissapears somehow the code continues to the QueryClose event of the main form even if no code called for the closure of the main form.
Furthermore, upon getting there the CloseMode variable of the event equals 1, which according to the documentation it means that "The Unload statement is invoked from code."
However at no point no Unload command was used.
So I'm unable to understand what is going on, and also this disrupt the purpose of the main form also.
Can anyone decipher what is going on here please?
UPDATE
After reading through the information, I have changed the code.
But before changing the code, I noticed that the problem I had dissapeared even though the default instances were being used, so I'm thinking it was some other context error.
I have however changed the code to use explicit instances of each user form.
I didn't use encapsulation nor composition as suggested on the rubberduck article because the software I wrote isn't that big and doesn't need to be scalable.
I find myself now with another problem, I have been searching for a way to identify each instance of each user form.
So far, and this is the wrong way to do it, I have been using the vba.Userforms with the index.
Is there another way to identify each instance?
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
Problem: I am building a Userform that has a 'Submit' and 'Cancel' button. I want the entire form to clear any entered data and close the form if the user hits the 'Cancel' button but I also am trying to build in the same functionality if the user hits the red 'X' in the top right corner. I'm unclear where I need to unload the form. I currently have it placed within the btnCancel_Click() method and I'm able to launch the form, enter some data and hit Cancel and it will close the form down.
But when I try to re-launch the form a 2nd time I get an error (I attached a picture of that message) that says
"Run-Time error '-2177418105 (80010007): Automation Error - The Callee (server [not server application]) is not available and disappeared; all connections are invalid. The call may have executed.
If I remove Unload Me from btnCancel_Click() then the form can close and re-open just fine, but any data I entered the first time will still be on the form and isn't cleared properly. I'm wondering if this is an Unload Me error or do I need to reset all form controls when I initialize the form?
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
' how was the form closed?
' vbFormControlMenu = X in corner of title bar
If CloseMode = vbFormControlMenu Then
' cancel normal X button behavior
Cancel = True
' run code for click of Cancel button
btnCancel_Click
End If
End Sub
'******************************************************************
Private Sub btnCancel_Click()
mbCancel = True
Me.Hide
Unload Me
End Sub
'*********************************************************************
Private Sub UserForm_Initialize()
'Populate values for 2 combo boxes
lastEmp = Sheets("Form_Ref").Cells(Rows.Count, 1).End(xlUp).Row
Me.cmbBoxEmpName.List = Sheets("Form_Ref").Range("A2:A" & lastEmp).Value
lastBld = Sheets("Form_Ref").Cells(Rows.Count, 2).End(xlUp).Row
Me.cmbBoxBuildingName.List = Sheets("Form_Ref").Range("B2:B" & lastBld).Value
End Sub
'******************************************************************
Public form As New CheckOutForm
Sub testFormOptions()
'Button pressed within Excel will start program and show the userform
form.Show
End Sub
This is the easiest quick and dirty solution:
Delete Public form As New CheckOutForm from the code. Then add it in the testFormOptions():
Sub testFormOptions()
Dim form As New CheckOutForm
form.Show
End Sub
Some not-that-good VBA books/tutorials would even go a bit like this, but this is brutal:
Sub testFormOptions()
CheckOutForm.Show
End Sub
Anyway, now the problem with the predefined values in the form is solved.
For the clean and not-so-easy solution, consider writing a MVC framework around the form:
https://codereview.stackexchange.com/questions/154401/handling-dialog-closure-in-a-vba-user-form
this blogpost (disclaimer - mine!), which pretty much says what the above link proposes, but it does not have the errors from the question.
the old StackOverflow tutorial for UserForms
If you execute Unload, you destroy the form object. With other words, your (global) variable form gets invalid and if you issue a new form.show, you get the runtime error.
When, on the other hand, you just unhide the form, the form-object stays valid (it's just currently not visible) and all controls keep their value.
Either you do some housekeeping by resetting all controls when a form is displayed (use the UserForm_Activate-event), or you have to create a new form-object every time you want to display it (I would strongly advice not to use the name form as variable name to avoid confusion).
Sub testFormOptions()
dim myForm as CheckOutForm
myForm.Show
End Sub
Dear Stackoverflow users,
For a small project, i have a few modules and a User form.
In the module, i have a loop statement that calls the User form.
The Userform gives the user four options to choose from.
When selection an option a public variable (string) will be defined.
The loop will fill the string in the column.
But now the loop will not stop before everything is answered.
I would like to create an exit button because if the list is large this is quite annoying).
So I thought of creating a command button "exit" which can be clicked (in the user form) to cancel the loop.
But because this button is in the user form, this will not stop the loop, it will just shut down the current user from, but the next will be displayed after.
Do you know the best way to stop the loop from-out the user form?
I know the techniques like Goto, but should only be used for error handling and won't work because the Goto needs to be in the user form sub and I want to cancel the total loop in another module.
I'm quite stuck at this, I hope you guys can help me out.
I think it would be something simple.
Regards,
Dubblej
I suggest not to use global variables as well as go to statement. In your form add a function to show the form and return the user choice whether they want to continue or not. Something like this:
Option Explicit
Private continue As Boolean
Public Function ShowDialog() As Boolean
Me.Show 1
ShowDialog = continue
End Function
Private Sub btnCancel_Click()
continue = False
Me.Hide
End Sub
Private Sub btnOK_Click()
continue = True
Me.Hide
End Sub
And in your module call ShowDialog function and if its return value is false quit the loop:
Option Explicit
Public Sub Sub1()
Dim form As New UserForm1
Do
If Not form.ShowDialog Then
Exit Do
End If
Loop
End Sub
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.