Where should I put Load and Unload frm1 (Userform name is frm1) and where should I put Me.Show and Me.Hide?
The (x) button within the UserForm doesn't work.
My Load and Unload is within the Active-X command button code on Sheet1:
Private Sub cmdb1_Click()
Load frm1
Unload frm1
End Sub
This way my UserForm is initialized and I can run the code
Private Sub Userform_Initialize()
'Some other code that Works...
frm1.Show
End Sub
that shows my Userform. Now I have a command button in my Userform that has the code
Private Sub cmdbClose_Click()
Me.Hide
End Sub
which I use to hide the sub, upon which the last line within cmdb1_Click() is executed and UserForm is unloaded. This Works.
However when I press the (x) button in my UserForm, the following error appears
Debugger says error lies within cmdb1_Click(). I've tried adding a sub called UserForm_QueryClose(), but the error persists. If I'd have to guess, I'd say the error is caused by the way I handle Load and Unload, thus by cmdb1_Click().
EDIT:
My problem is solved. ShowUserform and cmdbClose_Click contain the code CallumDA suggests. My command button now has:
Private Sub cmdb1_Click()
Load frm1
Call ShowUserform
End Sub
I recommend that you create an instance of your userform:
Dim MyDialog As frm1
Set MyDialog = New frm1 'This fires Userform_Initialize
You can then easily check whether the form is loaded before attempting to unload it:
If Not MyDialog Is Nothing Then
Unload MyDialog
End If
I also recommend that you don't call the Show method from the form's Initialize event. Think of your userform as just another object in Excel and manage it from your main code body.
Also, I wouldn't unload it in the cmdbClose_Click event as suggested by CallumDA (though that works fine to solve the current issue). Often you will need to be able to refer to values on your form from your main code body, and if you unload it they won't be available. Instead, keep it like you had it in the first place:
Private Sub cmdbClose_Click()
Me.Hide
End Sub
So your main code body (in your activeX button) will look something like this:
Dim MyDialog as frm1
Set MyDialog = New frm1 'This fires Userform_Initialize
'Place any code you want to execute between the Initialize and Activate events of the form here
MyDialog.Show 'This fires Userform_Activate
'When the close button is clicked, execution will resume on the next line:
SomeVariable = MyDialog.SomeControl.Value
'etc.
If Not MyDialog Is Nothing Then
Unload MyDialog
End If
You can also catch the event that fires when a user clicks the "X" on the form, and prevent the form from being unloaded:
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
Cancel = True
Me.Hide
End If
End Sub
Lastly, often you need a Cancel button on the form. The way I handle this is to create a "Cancelled" property in the form's code-behind:
Public Cancelled as Boolean
'(Note You can create additional properties to store other values from the form.)
In the Cancel button's click event:
Private Sub cmdbCancel_Click()
Me.Cancelled = True
Me.Hide
End Sub
And in the main code body:
Dim MyDialog as frm1
Set MyDialog = New frm1
MyDialog.Show
If Not MyDialog.Cancelled Then
SomeVariable = MyDialog.SomeControl.Value
SomeOtherVariable = MyDialog.SomeOtherProperty
'etc.
End If
If Not MyDialog Is Nothing Then
Unload MyDialog
End If
(I know the above is not strictly the correct way to declare a property, but this will work fine. You can make it read-only in the usual way if you wish.)
Put this in a standard module and link it up to the button you use to show the userform
Sub ShowUserform
frm1.Show
End Sub
And then in the userform
Private Sub cmdbClose_Click()
Unload Me
End Sub
I struggled for years with forms in Excel. I could never understand how an object (a form) could be destroyed but still have it's code running.
I now use this code on every form that has [OK] and [Cancel] buttons, as it allows you to control the [OK], [Cancel] and [X] being clicked by the user, in the main code:
Option Explicit
Private cancelled As Boolean
Public Property Get IsCancelled() As Boolean
IsCancelled = cancelled
End Property
Private Sub OkButton_Click()
Hide
End Sub
Private Sub CancelButton_Click()
OnCancel
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
Cancel = True
OnCancel
End If
End Sub
Private Sub OnCancel()
cancelled = True
Hide
End Sub
You can then use the form as an instance as follows:
With New frm1
.Show
If Not .IsCancelled Then
' do your stuff ...
End If
End With
or you can use it as an object (variable?) as noted above such as:
Dim MyDialog As frm1
Set MyDialog = New frm1 'This fires Userform_Initialize
Then similar instance noted above.
This is all based on an excellent article by RubberDuck which goes into more detail and explains the code given above.
Related
I've been moving some of my code to utilize UserForms utilizing Modeless so the user can copy data from the worksheet. I finally "generalized" this answer and got it working. But, I want to avoid having to create a class for each UserForm and truth is I don't 100% understand what is going on in the code. Is there a way I can easily migrate all my UserForms to utilize the below functions of "Event Driven Modeless UserForms". Basically, I'd like to pass the UserForm as a variable and have a bunch of subs in the class, mostly general and a specific or perhaps just using If/Then to call the correct exit module after the _Closed event.
Hope that makes sense, let me know if you require further clarification.
Generalized Code:
Module Name doesn't Matter
UserForm Name = UserForm1
Class Name = Class1
Module Code:
Private UserFormNameStr As Class1
Public Sub DoStuff()
Set UserFormNameStr = New Class1
UserFormNameStr.ClassSubNameStrSubName
End Sub
Public Sub CallMeWhenUserFormClosed()
Debug.Print "Module Code Run"
End Sub
Class Code:
Private WithEvents UserFormNameStr As UserForm1
Private Sub Class_Initialize()
Set UserFormNameStr = New UserForm1
End Sub
Public Sub ClassSubNameStrSubName()
UserFormNameStr.Show vbModeless
End Sub
Private Sub UserFormNameStr_Closed()
'_Closed is required syntax
Debug.Print "Closed Event"
Call CallMeWhenUserFormClosed
End Sub
UserForm Code:
Public Event Closed()
Private Sub UserForm_Initialize()
'
End Sub
Sub OkButton_Click()
Debug.Print "Raising Events from OK Button!"
RaiseEvent Closed
Unload Me
End Sub
Private Sub CancelButton_Click()
Unload Me
End
End Sub
Update:
Perhaps I'm looking for something on Workbook_Open with a "hook"?
What's making it all work is the WithEvents declaration. Instance variables declared with the WithEvents modifier will appear in the editor's left-side dropdown.
To create an event handler procedure for an event provider, select the variable from the left dropdown, then pick an event to handle in the right-side dropdown.
Ultimately the module would look something like this, i.e. with a WithEvents declaration for each modeless form you want to handle events for:
Private WithEvents UserFormNameStr As UserForm1
Private WithEvents SomeOtherUserForm As UserForm2
Private WithEvents AnotherUserForm As UserForm3
Private Sub Class_Initialize()
Set UserFormNameStr = New UserForm1
Set SomeOtherUserForm = New UserForm2
Set AnotherUserForm = New UserForm3
End Sub
Public Sub ClassSubNameStrSubName() 'weird name, consider methods that begin with a verb
UserFormNameStr.Show vbModeless
End Sub
Public Sub ShowSomeOtherForm()
SomeOtherUserForm.Show vbModeless
End Sub
Public Sub ShowAnotherForm()
AnotherUserForm.Show vbModeless
End Sub
Private Sub UserFormNameStr_Closed() 'select "UserFormNameStr" from the left-side dropdown: NEVER hand-write event handler signatures.
Debug.Print "Closed Event (UserFormNameStr)"
CallMeWhenUserFormClosed
End Sub
Private Sub SomeOtherUserForm_Closed() 'select "SomeOtherUserForm" from the left-side dropdown: NEVER hand-write event handler signatures.
Debug.Print "Closed Event (SomeOtherUserForm)"
CallMeWhenSomeOtherUserFormClosed
End Sub
Private Sub AnotherUserForm_Closed() 'select "AnotherUserForm" from the left-side dropdown: NEVER hand-write event handler signatures.
Debug.Print "Closed Event (AnotherUserForm)"
CallMeWhenAnotherUserFormClosed
End Sub
One Handler, Many Forms?
If all _Closed handlers need to do exactly the same thing, then we can get interfaces and polymorphism involved and have one class exist in 3 instances that each do their own thing for their respective form - but VBA does not expose Public Event declarations on a class' default interface, so the paradigm is a little bit different here, and because it doesn't involve Event and WithEvents, it's arguably simpler that way, too.
Define an IHandleClosingForm interface: add a new class module to your project, but give no attention to the implementation - just a very high-level abstraction of the functionality you want (here with Rubberduck annotations):
'#ModuleDescription "An object that handles a form being closed."
'#Interface
'#Exposed
Option Explicit
'#Description "A callback invoked when a form is closed."
Public Sub Closing(ByVal Form As UserForm)
End Sub
In each form module, hold a reference to that interface, and invoke its Closing method in the form's QueryClose handler:
Option Explicit
Public CloseHandler As IHandleClosingForm
`...
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
'user clicked the [X] button, form instance is going to be destroyed!
Cancel = True 'prevents a self-destructing form instance.
Me.Hide
End If
'don't assume the caller set the CloseHandler:
If Not CloseHandler Is Nothing Then CloseHandler.Closing(Me)
'...
End Sub
Now implement that interface in the presenter class:
Option Explicit
Implements IHandleClosingForm
'...rest of the module...
Private Sub IHandleClosingForm_Closing(ByVal Form As UserForm)
'NOTE: procedure exits to the still-closing form's QueryClose handler
If TypeOf Form Is UserForm1 Then
CallMeWhenForm1Closes
Else
CallMeWhenAnyOtherFormCloses
End If
End Sub
The final step is to introduce a circular reference between the form and the presenter, by setting the public CloseHandler property before showing the form:
Set theForm.CloseHandler = Me
theForm.Show vbModeless
This will work, but then there's a memory leak and neither the form nor the presenter instance would terminate (handle Class_Terminate to find out!), and you will want to strive to avoid that (Excel will/should clean it all up when it shuts down the VBA environment though).
The solution is to untie the knot at the first opportunity, so make sure your forms' QueryClose handler sets the IHandleClosingForm reference to Nothing as soon as it is no longer useful:
'don't assume the caller set the CloseHandler:
If Not CloseHandler Is Nothing Then CloseHandler.Closing(Me)
Set CloseHandler = Nothing 'release the handler reference
The next time the form is shown and the handler is set, it's going to be on another instance of the form.
If you need the state of the form to persist between it being shown and closed, then you must separate the state from the form (and keep the state around but still properly destroy the form object), ...but that's another topic for another day :)
1st, what i want to do:
I have a main UserForm, on that form i have a button to show secondary UserForm. When i click on that button, i want main form to be hidden. When i'm done with with work on the secondary form, i want to close it and show the main form again.
2nd, what i have so far (relevant code):
Main form code:
Private Sub createFastButton_Click()
Me.Hide
formSec.Show
End Sub
Secondary form code:
Private Sub cancelButton_Click()
Me.Hide
formMain.Show
End Sub
Private Sub UserForm_Terminate()
formMain.Show
End Sub
3rd, the problem:
If i use ControlButtons to navigate between forms, all works as intended. But if i use the "X" close window button on the secondary form and try to open it again from main form, it loses all functionality (main form works fine). It just shows the secondary form as i see it, when i use "View Object" in VBA editor. None of the buttons work and none of the intended boxes and labels are filled. Even the "X" button doesn't work. For me it seems obvious, that the problem is with unloading of secondary form. I tried to replace Me.Hide in secondary form with Unload Me and exactly same thing happens, as if i press the "X" button. So i need to do something with Sub UserForm_Terminate(), i tried to add Me.Hide there and as expected, it did nothing.
I there a solution to my problem?
Thx in advance.
main routine, calling Main Form:
With New formMain
.Show
End With
main form, calling secondary form
Private Sub createFastButton_Click()
Me.Hide
With New formSec
.Show
End With
Me.Show
End Sub
secondary form
Private Sub cancelButton_Click()
Me.Hide
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then Cancel = True
End Sub
no need for UserForm_Terminate() in secondary form, unless for unsaid needs
Clicking the "X" raises a QueryClose event that you need to handle, e.g.:
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
Cancel = True
Me.Hide
' Add more code here to respond to form close event
End If
End Sub
See here for a very helpful explanation.
I have a sub that calls a userform to show and would only like to proceed if the user didn't click my Cancel button. I don't want to put all my other sub calls within the userform.
Is it possible to have a userform return a value or a way to check if the user clicked a particular button?
I suppose I can use a global variable, but was wondering if I could pass things to and from a userform.
I prefer to use properties.
Inside your userForm
Private m_bCancel As Boolean
Public Property Get Cancel() As Boolean
Cancel = m_bCancel
End Property
Public Property Let Cancel(ByVal bCancel As Boolean)
m_bCancel = bCancel
End Property
Code for the cancel button
Private Sub cmdCancel_Click()
Me.Cancel=True
Me.Hide
End Sub
Call the userForm from outside like this
sub loadForm()
dim frm
set frm= new UserForm1
frm.show
if frm.Cancel then
Msgbox "Cancelled"
end if
End Sub
I have four modules, three buttons and one userform, a progressbar, in my Excel workbook. I would like to show a progressbar during the runtime of all four modules.
Example:
I click on a button which executes the following code and makes my progressbar visible:
Private Sub GWPCClearDataButton_Click()
ProgressBar.Show
End Sub
In my userform i have the following code:
Private Sub UserForm_Activate()
GWPCClearData
End Sub
This calls one of my four modules which works fine so far.
Now here's where I'm stuck.
Of course, I would like to use the same progressbar for all modules but how can I determine in the userform code block which button was clicked and then depending on that call another module?
Example:
Private Sub GWPCClearDataButton_Click()
ProgressBar.Show
End Sub
Private Sub GSEPClearDataButton_Click()
ProgressBar.Show
End Sub
UserForm:
Private Sub UserForm_Activate()
If "BUTTON_NAME" = "GWPCClearDataButton" Then
GWPCClearData
ElseIf "BUTTON_NAME" = "GSEPClearDataButton" Then
GSEPClearData
End if
End Sub
How could I do that? I have yet to find a possibility to pass a parameter to my userform which would contain the button name.
Thanks for any help.
In your form put
Sub Start_Form(Called_From as string)
'Process Called_From as needed
End Sub
When you call your form just call the sub first like below
<form_name>.Start_Form "Button Called From"
<form_name>.Show
Basically you have to pass the variable via another sub(can be called anything) in the userform and then show the form after
User Form Code
Dim test
Private Sub UserForm_Activate()
MsgBox test
End Sub
Sub start(tt)
test = tt
End Sub
Module code
Sub t()
UserForm1.start "Hello World!"
UserForm1.Show
End Sub
The attached VBA procedures are for a progress bar userform. Everything works as expected, except that the cancel button is intermittently unresponsive.
I say intermittently because, 95% of the time I have to click the cancel button numerous times before the procedure stops. I can see the button click event being animated, but the procedure isn't being interrupted. It looks as though something is stealing focus from the cancel button before the button down event can occur.
The escape and window close buttons respond as expected with one click.
What do I need to do to make the cancel button respond correctly? Thanks!
Update: I noticed that when I click and hold down on the cancel button, instead of the button staying "down" it gets kicked back up. So apparently something is resetting the button state to up, fast enough that the procedure is not catching the down state to fire the click event.
Here is the code in the userform module (named UserForm1):
Private mbooUserCancel As Boolean
Public Property Get UserCancel() As Boolean
UserCancel = mbooUserCancel
End Property
Private Property Let UserCancel(ByVal booUserCancel As Boolean)
mbooUserCancel = booUserCancel
End Property
Public Sub UpdateProgress(CountTotal As Long, CountProgress As Long)
On Error GoTo Error_Handler
ProgressBar1.Value = CountProgress / CountTotal * 100
DoEvents
Error_Handler:
If Err.Number = 18 Then CommandButton1_Click
End Sub
Private Sub CommandButton1_Click()
Hide
UserCancel = True
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
Cancel = True
CommandButton1_Click
End Sub
Private Sub UserForm_Activate()
With Application
.Interactive = False
.EnableCancelKey = xlErrorHandler
End With
End Sub
Private Sub UserForm_Terminate()
Application.Interactive = True
End Sub
Here is the code for the module (named Module1) that calls the UserForm1:
Sub TestProgress()
On Error GoTo Error_Handler
Dim objUserForm As New UserForm1
Dim lngCounter As Long
Dim lngSubCounter As Long
With objUserForm
.Show vbModeless
DoEvents
For lngCounter = 1 To 5
If .UserCancel Then GoTo Exit_Sub
For lngSubCounter = 1 To 100000000
Next lngSubCounter
.UpdateProgress 5, lngCounter
Next lngCounter
Application.Wait Now + TimeValue("0:00:02")
.Hide
End With
Exit_Sub:
If objUserForm.UserCancel Then
MsgBox "User Cancelled from UserForm1"
End If
Exit Sub
Error_Handler:
If Err.Number = 18 Then
Unload objUserForm
MsgBox "User Cancelled from Module1"
End If
End Sub
Works for me on the first click every time. Try unloading any add-ins or any other code that could be running inside the container app and see if that helps.
If people are still in need of the answer, well I researched & cracked it!
For the click on cancel button, code should be;
Private Sub CommandButton1_Click()
Unload Me
'this bit ends all macros
End
End Sub
The answer is to use modal userforms for progress bars so that the button click event can fire without getting obscured by processing in the calling procedure.