I'm trying to raise events from a modeless userform. My starting point is this excellent example. When I show the form modeless, the code raising the event executes, but the event handler never runs (I don't get the expected MsgBox when the Cancel button is clicked.) When I show the form modal, the events are handled as desired, but the form is no longer modeless as desired.
The userform named FormWithEvents has an OKButton and a CancelButton; here's the code behind:
Option Explicit
Public Event FormConfirmed()
Public Event FormCancelled(ByRef Cancel As Boolean)
Private Function OnCancel() As Boolean
Dim cancelCancellation As Boolean
RaiseEvent FormCancelled(cancelCancellation)
If Not cancelCancellation Then Me.Hide
OnCancel = cancelCancellation
End Function
Private Sub CancelButton_Click()
OnCancel
End Sub
Private Sub OKButton_Click()
Me.Hide
RaiseEvent FormConfirmed
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = VbQueryClose.vbFormControlMenu Then
Cancel = Not OnCancel
End If
End Sub
Here's the code for the Presenter class that shows the form:
Option Explicit
Private WithEvents myModelessForm As FormWithEvents
Public Sub Show()
Set myModelessForm = New FormWithEvents
' COMMENT OUT ONE OF THE FOLLOWING TWO LINES TO TOGGLE MODELESS OR MODAL
myModelessForm.Show vbModeless ' Modeless, but events don't get handled (no msgbox on clicking cancel button)
' myModelessForm.Show vbModal ' Events get handled, but no longer modal
End Sub
Private Sub myModelessForm_FormCancelled(Cancel As Boolean)
' Setting cancel to True will leave the form open
Cancel = MsgBox("Cancel this operation?", vbYesNo + vbExclamation) = vbNo
If Not Cancel Then
' Modeless form was cancelled and is now hidden
' ...
Set myModelessForm = Nothing
End If
End Sub
Private Sub myModelessForm_FormConfirmed()
' Form was okayed and is now hidden
Set myModelessForm = Nothing
End Sub
And here's the code in the main module:
Option Explicit
Public Sub RunForm()
With New Presenter
.Show
End With
End Sub
Any ideas on where I've gone wrong?
Maybe this instead so you can keep your Presenter instance in scope:
Dim pres as Presenter
Public Sub RunForm()
Set pres = New Presenter
pres.Show
End Sub
Avoid using RaiseEvent. Declare a public instance of the Presenter class in the standard module.
Option Explicit
Public Preter As New Presenter
Public Sub RunForm()
With New Presenter
.Show
End With
End Sub
Convert the event procedure to a sub with public scope.
Sub FormCancelled(Cancel As Boolean)
' Setting cancel to True will leave the form open
Cancel = MsgBox("Cancel this operation?", vbYesNo + vbExclamation) = vbNo
If Not Cancel Then
' Modeless form was cancelled and is now hidden
' ...
Set myModelessForm = Nothing
End If
End Sub
The call the sub within the class.
Private Function OnCancel() As Boolean
Dim cancelCancellation As Boolean
Preter.FormCancelled cancelCancellation
If Not cancelCancellation Then Me.Hide
OnCancel = cancelCancellation
End Function
I'm not sure but your code line Set myModelessForm = Nothing in the class module bother me. I wonder whether the form should be hidden instead so that the code can continue to the Show procedure where the form can then be put to rest.
Related
I have an Outlook macro that pops up a form when someone sends an email and asks them to select a classification. Under ThisOutlookSession I have:
Private Sub Application_ItemSend(ByVal Item As Object, Cancel As Boolean)
Dim frm As classificationForm
Dim chosenvalue As String
Set frm = New classificationForm
frm.Show vbModal
Select Case True
Case frm.restrictedButton.Value
chosenvalue = "Restricted"
Case frm.internalButton.Value
chosenvalue = "Internal"
Case frm.publicButton.Value
chosenvalue = "Public"
Case Else
MsgBox "No classification chosen"
Cancel = True
Exit Sub
End Select
If TypeName(Item) = "MailItem" Then
Item.Subject = "[" & chosenvalue & "] " & Item.Subject
End If
End Sub
and under classificationForm I have:
Private Sub okCommand_Click()
Unload Me
End Sub
Private Sub cancelCommand_Click()
Unload Me
End Sub
The form is 3 option buttons (the classifications) and 2 command buttons (OK and Cancel).
The [Internal] option button is selected by default. However, if someone clicks the form's "X" button in the top-right corner or the "Cancel" command button, then the Macro sends the email with the selected classification button when I want it to kick me back to the draft email like it would if there were no options selected.
How would I do this? Thanks.
Unload is a lie. It seems to only work with the form's default instance, and working with forms' default instance is a poisonous place you don't want to go.
Don't unload. Hide instead.
Another problem is that since the calling code is querying the form's controls' state, it doesn't matter how the form was closed: Ok, Cancel, "X" - everything single one of these buttons gets the form unloaded and whatever was selected is acted upon: you have no mechanism in place to determine whether the user okayed the dialog, or wishes to cancel everything.
Add a Cancelled property:
Private IsCancelled As Boolean
Public Property Get Cancelled() As Boolean
Cancelled = IsCancelled
End Property
Handle the QueryClose event, set the Cancel parameter to False when the CloseMode is vbFormControlMenu, and Hide the form - actually, what you want is for that "X" button to do exactly the same thing as your Cancel button, so just do that:
Private Sub OkButton_Click()
Me.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()
IsCancelled = True
Me.Hide
End Sub
Now your calling code can determine whether the form was cancelled, and act accordingly.
If frm.Cancelled Then
Cancel = True
Else
'user okayed the dialog: determine the selection...
End If
See this article for more in-depth information about user forms: the calling code doesn't need to care about the controls on the form.
use UserForm_QueryClose() event handler to intercept any userform red "X" clicking
then change your UserForm code pane as follows:
Private Sub okCommand_Click()
Me.Hide ' don't unload the Form, just hide it
End Sub
Private Sub cancelCommand_Click()
Me.Tag = "NO" ' signal to the calling sub that it must not send the email
Me.Hide ' don't unload the Form, just hide it
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = 0 Then Me.Tag = "NO" ' if user clicked the red X button signal to the calling sub that it must not send the email
End Sub
and adjust your Application_ItemSend event handler code accordingly:
Set frm = New classificationForm
With frm
.Show vbModal
If .Tag <> "NO" Then ' if userform didn't signal not to send the email
Select Case True
Case frm.restrictedButton.Value
chosenvalue = "Restricted"
Case frm.internalButton.Value
chosenvalue = "Internal"
Case frm.publicButton.Value
chosenvalue = "Public"
Case Else
MsgBox "No classification chosen"
Cancel = True
Exit Sub
End Select
Else ' the userform signaled not to send the email
MsgBox "No classification chosen"
Cancel = True
End If
End With
Unload frm
I have a userForm (mappingGuide) that allows user to pick a smartyTag from a list of more user-friendly names.
I have a second user-form (conditionalBuilder) that I would like to call this userForm upon double-clicking a text field so that a user can lookup which smartyTag to apply (in case they don't know).
So logic, is:
open conditionalBuilder
double-click Field text box
mappingGuide opens
pick a smartytag from listbox
fill smartytag value into field text-box in conditionalBuilder
unload mappingGuide
The issue I think I having with completing the requirement is that when I load the forms themselves I cannot find a way to set the text of the fieldName textbox of the loaded instance of conditionalBuilder (see last code block below). I've been searching around, but cannot figure it out.
Here is relevant code:
conditionalBuilder loads from Custom UI ribbon
Sub RunCode(ByVal Control As IRibbonControl)
Select Case Control.ID
Case Is = "mapper": LoadMappingGuide
Case Is = "conditional": LoadConditionalBuilder
End Select
End Sub
Sub LoadConditionalBuilder()
Dim conditionalForm As New conditionalBuilder
conditionalForm.Show False
End Sub
double-click event of fieldName then loads mappingGuide
Private Sub fieldName_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
Me.hide
Dim pickField As New mappingGuide
pickField.Show False
End Sub
smartTag listbox click event then attempts to place selection into fieldName (or selection if form not loaded)
Private Sub smartTagList_Click()
If smartTagList.ListIndex > -1 And smartTagList.Selected(smartTagList.ListIndex) Then
Dim smartyTag As String
smartyTag = smartTagList.List(smartTagList.ListIndex, 2)
If isUserFormLoaded(conditionalBuilder.Name) Then
'*** ---> below is my issue how to reference instance of form
conditionalBuilder.fieldName.Text = smartyTag
conditionalBuilder.Show
Else
Selection.Range.Text = smartyTag
End If
End If
Unload Me
End Sub
If there is a better set-up that would be great to know too. I have the forms separate because there's a couple of levels a user can create tags with.
This is how I would do it, a bit of overkill but in case of multiple forms it will be beneficial.
Module 1:
Option Explicit
Sub test()
frmMaster.Show False
End Sub
Form 1 : frmMaster:
Option Explicit
'/ Declare with events
Dim WithEvents frmCh As frmChild
Private Sub TextBox1_DblClick(ByVal cancel As MSForms.ReturnBoolean)
handleDoubleClick
End Sub
Sub handleDoubleClick()
If frmCh Is Nothing Then
Set frmCh = New frmChild
End If
frmCh.Show False
End Sub
'/ Handle the event
Private Sub frmCh_cClicked(cancel As Boolean)
Me.TextBox1.Text = frmCh.bChecked
End Sub
Form 2: frmChild:
Option Explicit
Event cClicked(cancel As Boolean)
Private m_bbChecked As Boolean
Public Property Get bChecked() As Boolean
bChecked = m_bbChecked
End Property
Public Property Let bChecked(ByVal bNewValue As Boolean)
m_bbChecked = bNewValue
End Property
Private Sub CheckBox1_Click()
Me.bChecked = Me.CheckBox1.Value
'/ Raise an event when something happens.
'/ Caller will handle it.
RaiseEvent cClicked(False)
End Sub
You can do this with a presenter class which controls userform instances and pass values between them. I mocked up something similar to give you an idea.
Presenter. This is a class module which creates the userforms, controls their scope, and catches the event thrown by the
ConditionalBuilder. It makes it super easy to pass values between
userforms.
Private WithEvents CB As ConditionalBuilder
Private MG As MappingGuide
Public Sub ShowCB()
Set CB = New ConditionalBuilder
CB.Show vbModal
End Sub
Private Sub CB_ShowMappingGuide()
Set MG = New MappingGuide
MG.Show vbModal
CB.UpdateTB1 Value:=MG.SmartTag
End Sub
ConditionalBuilder.
This has a simple function to update your textbox and also an event which raises actions in the presenter.
Public Event ShowMappingGuide()
Public Function UpdateTB1(Value As String)
TextBox1.Value = Value
End Function
Private Sub TextBox1_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
RaiseEvent ShowMappingGuide
End Sub
MappingGuide.
The Type and Property could be overkill since we just want one value from the mapping guide but it's still good practice.
Private Type TView
Tag As String
End Type
Private this As TView
Public Property Get SmartTag() As String
SmartTag = this.Tag
End Property
Private Sub UserForm_Initialize()
Tags.List = Array("a", "b", "c")
End Sub
Private Sub Tags_Click()
this.Tag = Tags.List(Tags.ListIndex, 0)
Me.Hide
End Sub
I have one final Standard Module which creates the Presenter. This is what you'd hook up to your ribbon.
Public Sub ShowProject()
With New Presenter
.ShowCB
End With
End Sub
Step 1 (double click text field)
Step 2 (selecting "b")
Step 3 (result)
I actually solved it by placing the below block inside the IF where I check for the form being loaded and I will leave open for better answers, if there are any.
Dim uForm As Object
For Each uForm In VBA.UserForms
If uForm.Name = conditionalBuilder.Name Then
uForm.fieldName.Text = smartyTag
uForm.Show
End If
Next
I am trying to close a UserForm if a person clicks the red x in the upper right hand corner. Here is my code so far.
Public Sub Worksheet_BeforeDoubleClick(ByVal target As Range, Cancel As Boolean)
If target.Column = 10 Then
UserForm2.Show
etc...
Now, the Form opens and I run this code...
Public Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = vbFormControlMenu Then
If Not ExitAsk = vbYes Then Cancel = True
End If
End Sub
Public Function ExitAsk() As VbMsgBoxResult
Dim Smsg As String
Smsg = "Are you really want to exit? Click Yes to terminate or No to Continue."
ExitAsk = MsgBox(Smsg, vbYesNo + vbDefaultButton2 + vbQuestion, "Exit!")
End Function
Then, focus goes back to the Sub, and the code continues to run through everything, which causes some problems for me. I want to click the red x and close the UserForm and exit the Sub. It seem like the Sub and UserForm don't communicate, even though both are declared a Public. I must be missing something simple, but I'm not sure what. Any ideas, anyone?
Thanks!
It seem like the Sub and UserForm don't communicate, even though both are declared a Public
Accessibility has nothing to do with whether a procedure communicates with a form. A form is an object, not very different from a Range or a Collection - except it has a designer and a default instance: it won't "communicate" with your procedure without you telling it how to do that.
First, stop using the default instance and treat the form as you would any other object: New it up!
With New UserForm2 'object instance starts existing here...
.Show 'vbModal is implicit
End With '...and dies here
Now if you want the calling code to know how the form was closed, you need to expose something that the calling code can access to know that.
That's best done with a property. You could also expose a public field, but then the calling code would be able to tamper with it and you don't want that - that's what encapsulation does:
Private isCancelled As Boolean
Public Property Get Cancelled() As Boolean
Cancelled = isCancelled
End Property
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
If CloseMode = vbFormControlMenu Then
isCancelled = True
End If
Cancel = True
Me.Hide
End Sub
Notice Cancel = True and Me.Hide: without cancelling the close, the object gets destroyed immediately and you lose its state. So you want to Hide the form instead of unloading/destroying it.
Only the form's code-behind can access isCancelled, but the calling code can read the Cancelled property (but not write to it).
With New UserForm2 'object instance starts existing here...
.Show vbModal 'execution in this procedure will resume after form is closed
If .Cancelled Then
'form was X'd out
End If
End With '...and dies here
So... not sure what you're trying to achieve exactly, but you'll want something along these lines.
In UserForm you can define your own public Get-property e.g. CloseModeInfo which will return value of private member which can be set in UserForm_QueryClose. Value of this public property can be then tested later. According to value in this property the calling code will decide what to do. HTH
UserForm
Private m_closeModeInfo As Integer
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
m_closeModeInfo = CloseMode
If CloseMode = vbFormControlMenu Then
If Not ExitAsk = vbYes Then Cancel = True
End If
End Sub
Private Function ExitAsk() As VbMsgBoxResult
Dim Smsg As String
Smsg = "Are you really want to exit? Click Yes to terminate or No to Continue."
ExitAsk = MsgBox(Smsg, vbYesNo + vbDefaultButton2 + vbQuestion, "Exit!")
End Function
Public Property Get CloseModeInfo() As Integer
CloseModeInfo = m_closeModeInfo
End Property
Worksheet Code
Private Sub Worksheet_BeforeDoubleClick(ByVal target As Range, Cancel As Boolean)
If target.Column = 10 Then
Dim frm As UserForm2
Set frm = New UserForm2
UserForm2.Show
If frm.CloseModeInfo = vbFormControlMenu Then
Unload frm
' I want to click the red x and close the UserForm and exit the Sub:
Exit Sub
End If
End If
End Sub
Prehistory
I have read the best practises for creating a form, concerning the fact that one should always refer to an object of the form and not the form itself. Thus, I have decided to build a boiler-plate form for myself.
The problem
Everything ran smoothly, until the moment I have decided to close the form with the top right red X. It closes ok. But then, when I try to open the form again, I get this runtime error:
The error is on objPresenter.Show (see the code below). Obviously, it does not enter in the if above. But the problem is that the closing from the X does not work fine. When I close the form from the End button, anything works. And even, if I copy the code for the closing from the btnEnd to UserForm_QueryClose it still does not work the same.
The form
Thus, I have a modMain, frmMain and clsSummaryPresenter, which all take care of the form. I start the code from modMain
My form looks like this:
It has btnRun, btnExit, lblInfo. The name of the class is frmMain.
The code
In frmMain:
Option Explicit
Public Event OnRunReport()
Public Event OnExit()
Public Property Get InformationText() As String
InformationText = lblInfo.Caption
End Property
Public Property Let InformationText(ByVal value As String)
lblInfo.Caption = value
End Property
Public Property Get InformationCaption() As String
InformationCaption = Caption
End Property
Public Property Let InformationCaption(ByVal value As String)
Caption = value
End Property
Private Sub btnRun_Click()
RaiseEvent OnRunReport
End Sub
Private Sub btnExit_Click()
RaiseEvent OnExit
End Sub
Private Sub UserForm_QueryClose(CloseMode As Integer, Cancel As Integer)
If CloseMode = vbFormControlMenu Then
Cancel = True
Hide
'Even if I change the two lines above with this the error happens:
'RaiseEvent OnExit
'However, if I simply write END in stead of those two lines
'anything works quite ok...
'but that is a bit brutal.
End If
End Sub
In clsSummaryPresenter
Option Explicit
Private WithEvents objSummaryForm As frmMain
Private Sub Class_Initialize()
Set objSummaryForm = New frmMain
End Sub
Private Sub Class_Terminate()
Set objSummaryForm = Nothing
End Sub
Public Sub Show()
If Not objSummaryForm.Visible Then
objSummaryForm.Show vbModeless
Call ChangeLabelAndCaption("Press Run to Start", "Starting")
End If
With objSummaryForm
.Top = CLng((Application.Height / 2 + Application.Top) - .Height / 2)
.Left = CLng((Application.Width / 2 + Application.Left) - .Width / 2)
End With
End Sub
Public Sub Hide()
If objSummaryForm.Visible Then objSummaryForm.Hide
End Sub
Public Sub ChangeLabelAndCaption(strLabelInfo As String, strCaption As String)
objSummaryForm.InformationText = strLabelInfo
objSummaryForm.InformationCaption = strCaption
objSummaryForm.Repaint
End Sub
Private Sub objSummaryForm_OnRunReport()
MainGenerateReport
Refresh
End Sub
Private Sub objSummaryForm_OnExit()
Hide
End Sub
Public Sub Refresh()
With objSummaryForm
.lblInfo = "Ready"
.Caption = "Task performed"
End With
End Sub
In modMain
Option Explicit
Private objPresenter As clsSummaryPresenter
Public Sub MainGenerateReport()
objPresenter.ChangeLabelAndCaption "Starting and running...", "Running..."
GenerateNumbers
End Sub
Public Sub GenerateNumbers()
Dim lngLong As Long
Dim lngLong2 As Long
tblMain.Cells.Clear
For lngLong = 1 To 4
For lngLong2 = 1 To 1
tblMain.Cells(lngLong, lngLong2) = lngLong * lngLong2
Next lngLong2
Next lngLong
End Sub
Public Sub ShowMainForm()
If (objPresenter Is Nothing) Then
Set objPresenter = New clsSummaryPresenter
End If
objPresenter.Show
End Sub
The question
Once again, why I cannot close the form with the red X? I can substitute the code in UserForm_QueryClose with End but that is a bit brutal. Any ideas?
Changing the form's mode from vbModeless to vbModal gives you an earlier and more informative failure:
The problem seems to be because the Cancel = True assignment in the QueryClose handler, isn't working for some reason.
The signature for the QueryClose handler is as follows:
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
Yours is:
Private Sub UserForm_QueryClose(CloseMode As Integer, Cancel As Integer)
You should never type these handler signatures manually yourself - instead, use the drop-down in the codepane's top-right corner, and have the VBE generate the handler stubs for you:
That way your handler signatures will always match the interface they're for.
VBA doesn't really care about parameter names in handlers: the way the runtime matches a handler signature is by matching the parameter indices and their types, against the expected ones. Since both QueryClose parameters are Integer values, inverting them compiles just fine - except when you set Cancel = True, what the runtime sees is that you've assigned CloseMode = -1 and left the Cancel parameter alone.
Which means your form doesn't cancel its close, and thus the object gets destroyed every time.
Invert the parameters in your QueryClose handler, and everything works perfectly fine and exactly as intended.
Calling the form like so works just fine for me:
Option Explicit
dim mfrmMain as ufMain
Sub ShowMainForm2()
If ufMain Is Nothing Then
Set ufMain = New mfrmMain
End If
mfrmMain.Show vbModeless
End Sub
I have two command buttons (cmd1 and cmd2) on userform1, when clicked each show the same userform (userform2). Within the initialize or load sub is it possible to determine which command button was clicked on userform1 and therefore show the form differently? I imagine the code in either the initialize or load sub on userform2 to have the following skeleton:
if (cmd1 was clicked)
' do stuff relating to 1
elseif (cmd2 was clicked)
' do stuff relating to 2
else
' error handling
End if
The respective "stuff" could be moved into the event handler for cmd1 and cmd2 however, if the method described above can be used it will be a lot simpler and cleaner.
Use a Public Variable in UserForm1 and then test it in UserForm2_Initialize Event.
Something like this in UserForm1:
Public whatsclicked As String
Private Sub CommandButton1_Click()
whatsclicked = "CommandButton1"
UserForm2.Show
End Sub
Private Sub CommandButton2_Click()
whatsclicked = "CommandButton2"
UserForm2.Show
End Sub
And then in UserForm2:
Private Sub UserForm_Initialize()
Select Case UserForm1.whatsclicked
Case "CommandButton1": MsgBox "CommandButton1 loaded form."
Case "CommandButton2": MsgBox "CommandButton2 loaded form."
Case Else 'Do something else
End Select
End Sub
you can do it also without public variable.
i won't show an example where you can simply write something in a cell or hidden sheet, instead i just pass the wanted info directly .
this time whatsclicked is the name of a label in userform2,
in userform1, before calling userform2:
Private Sub CommandButton1_Click()
load UserForm2
with UserForm2
.whatsclicked.caption= "CommandButton1"
.Show
end with
End Sub
Private Sub CommandButton2_Click()
load UserForm2
with UserForm2
.whatsclicked.caption= "CommandButton2"
.Show
end with
End Sub
Of course, you will have to hide the text of whatsclicked to the user (same color as font or background...)
You can use ActiveControl for this as when you click a control it becomes active/focussed:
Private Sub CommandButton1_Click()
Debug.Print Me.ActiveControl.Name
End Sub
Private Sub CommandButton2_Click()
Debug.Print Me.ActiveControl.Name
End Sub
So more in line with your example:
Private Sub CommandButton1_Click()
Call DoStuff
End Sub
Private Sub CommandButton2_Click()
Call DoStuff
End Sub
Private Sub DoStuff()
select case Me.ActiveControl.Name
case "CommandButton1"
' do stuff relating to 1
case "CommandButton2"
' do stuff relating to 2
case else
'error etc.
end select
End Sub