How to declare custom event in userform using vba - vba

I have userform which collects some user input. Now what I'm trying to do, is to declare some event to throw from userform when OK button is clicked. I'm new to vba so I don't know how to do it. Any code or link to tutorial would be greatly appreciated.
Load UserForm1
UserForm1.Show
//here I want to capture UserForm1 OK button's click event and read the data

In child-form declare event and raise it at the certain moment:
Public Event clickOnChild(ByVal inputText As String)
RaiseEvent clickOnChild(Me.TextBox1.Value)
In a custom class module, worksheet class module or other user form you can catch the event. However you can't catch event in standard module because WithEvents variable are valid in object module only. To catch your event in e.g. other user form declare WithEvents variable of type childUserForm and
add event-handler where the event will be catched and handled:
Private WithEvents childForm As childUserForm
Private Sub childForm_clickOnChild(ByVal inputText As String)
Complete example:
Child user form:
Option Explicit
Public Event clickOnChild(ByVal inputText As String)
Private Sub CommandButton1_Click()
RaiseEvent clickOnChild(Me.TextBox1.Value)
End Sub
Parent user form:
Option Explicit
Private WithEvents childForm As childUserForm
Private Sub CommandButton1_Click()
childForm.Show
End Sub
Private Sub childForm_clickOnChild(ByVal inputText As String)
MsgBox "Input in child form was: " & inputText
End Sub
Private Sub UserForm_Initialize()
Set childForm = New childUserForm
End Sub

As I said in a comment, I don't think what you want to do is possible, but I thought of the following workarounds:
If your user input is very simple, like just entering a string, a messagbox could work:
Dim sUserInput As Variant
sUserInput = InputBox("Please enter something useful.", "Title", "Default")
Debug.Print "sUserInput=" & sUserInput
If you need the form to capture user input, making it modal and then exposing a value through a public method might work.
In the form:
Option Explicit
Private msFormString As String
Private Sub CommandButton1_Click()
msFormString = "Someone clicked on Button 1!"
'***** Note: if you use Unload Me, the string
'***** is unloaded with the form...
Me.Hide
End Sub
Public Function GetFormString() As String
GetFormString = msFormString
End Function
The calling code:
Load UserForm1
Call UserForm1.Show(vbModal)
Debug.Print "Value from UserForm1: " & UserForm1.GetFormString
Note: The function could return an object, class or array if you need to pass more data back.

Related

Access not firing custom event

I've been building testable MVC logic for my Access database using RubberDuck's answer to Best way to test a MS Access Application? but I'm stuck with the custom event handling. I can't figure out why the OnCreate event isn't firing.
Form_CreateStudents:
Option Compare Database
Private ctrl As ctrCreateStudent
Public Event OnCreate()
Private Sub btnCreate_Click()
Set ctrl = New ctrCreateStudent
ctrl.Run
RaiseEvent OnCreate
End Sub
Class module ctrCreateStudent:
Private WithEvents frm As [Form_Create Students]
Public Sub Run()
MsgBox "run called"
Set frm = New [Form_Create Students]
End Sub
Public Sub frm_OnCreate()
MsgBox "frm_oncreate event called"
End Sub
Run is being called, but frm_OnCreate is just ignored. I'm relatively new to VBA, what am I missing here?
Quite simple:
frm is a New [Form_Create Students], not the one calling it.
This new form doesn't raise the OnCreate event. In fact, this new form is not even visible, because you haven't set frm.Visible = True
If you want to set it to the form that just called Run, pass it:
On the form:
Private ctrl As ctrCreateStudent
Public Event OnCreate()
Private Sub btnCreate_Click()
Set ctrl = New ctrCreateStudent
ctrl.Run Me
RaiseEvent OnCreate
End Sub
On the class:
Private WithEvents frm As [Form_Create Students]
Public Sub Run(parentForm As [Form_Create Students])
MsgBox "run called"
Set frm = parentForm
End Sub
Public Sub frm_OnCreate()
MsgBox "frm_oncreate event called"
End Sub
A strong warning, though: this code contains a reference loop, and thus a memory leak.
The form has a reference to the class, and the class has a reference to the form, so neither will ever get destroyed. Every time you close and open the form, a new form and class object will get created, and none of them will ever get destroyed.
When closing the form, it turns invisible and looks gone, but it's still there and using memory.
There are many ways to work around this, but an easy one is:
In the class:
Public Sub frm_Close()
Set frm = Nothing 'Release form object, break reference loop
End Sub
And make sure the Form's On Close property is set to "[Event Procedure]" so the close event gets raised.

Access VBA put a form element name as an argument

Let's see if anyone knows how to solve this problem:
I have a form with several elements: Some of them are textboxes called A1, A2, A3, A4...
Now, their AfterUpdate SubProcedure is extremely long but barely similar for each of them: A1_AfterUpdate, A2_AfterUpdate, A3_AfterUpdate...etc... are very similar but for the names of the textboxes they change.
My idea was to gather all that was equal in a subprocedure defined this way:
Private Sub Update(Box As String, Menu As Boolean)
If Menu=True{
Me!Box.Text = "This is the text that is going to change"
}
End Sub
So, the only thing I must do is to call it this way, for instance:
Update(A1, True)
But it doesn't seems to work. Any idea on how to reach this objective?
Add a class module - I've called it clsTextBoxEvents.
Add this code to the class:
Public WithEvents txt As Access.TextBox
Private Sub txt_AfterUpdate()
MsgBox txt.Name & " has been updated."
End Sub
In your form module add this code:
Public MyTextBoxes As New Collection
Private Sub Form_Open(Cancel As Integer)
Dim ctl As Control
Dim txtBoxEvent As clsTextBoxEvents
For Each ctl In Me.Controls
If TypeName(ctl) = "TextBox" Then
Set txtBoxEvent = New clsTextBoxEvents
Set txtBoxEvent.txt = ctl
txtBoxEvent.txt.AfterUpdate = "[Event Procedure]"
MyTextBoxes.Add txtBoxEvent
End If
Next ctl
End Sub
The MyTextBoxes declaration must be at the very top of the module.
This just adds the AfterUpdate event to all textboxes on the form. You'll probably want to refine that a bit to textboxes with specific text in the name, or controls that are in a specific frame on the form.
If you use a function instead of a sub:
Private Function UpdateCtl(Menu As Boolean)
If Menu Then
activecontrol = "This is the text that is going to change"
End If
End Sub
then you can call it directly from the control's AfterUpdate property: =UpdateCtl(True).
Simple and fast

VBA Input Value From Another UserFormB into TextBox From UserFormA

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

Monitor class events from userform

I have a userform which assembles itself at runtime, by looking in a folder and extracting all the pictures from it into image-controls on my form. What makes the process a little more complex is that I'm also using the image-controls' events to run some code.
As a simplified example - I have a form which creates a picture at runtime, the picture has an on-click event to clear its contents. To do this I have a custom class to represent the image object
In a blank userform called "imgForm"
Dim oneImg As New clsImg 'our custom class
Private Sub UserForm_Initialize()
Set oneImg.myPic = Me.Controls.Add("Forms.Image.1") 'set some property of the class
oneImg.Init 'run some setup macro of the class
End Sub
In a class module called "clsImg"
Public WithEvents myPic As MSForms.Image
Public Sub Init() 'can't put in Class_Initialise as it is called before the set statement - so myPic is still empty at that point
myPic.Picture = LoadPicture(path/image)
End Sub
Public Sub myPic_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
onePic.Picture = Nothing
End Sub
The problem is, this doesn't display the changes, and I realised I needed a imgForm.Repaint in there somewhere - the question is, where?
Attempts
First option is to put it in the Init() sub of clsImg. (ie. have a line imgForm.Repaint at the end of the click event) That works, but not ideal as the class can then only be used with the userform of the correct name.
A 2nd idea was to pass the userform as an argument to Init()
Public Sub Init(uf As UserForm) 'can't put in Class_Initialise as it is called before the set statement - so myPic is still empty at that point
myPic.Picture = LoadPicture(path/image)
uf.Repaint
End Sub
And called with
oneImg.Init Me
That works too, but would mean that wherever I require a repaint, I would have to pass the parameter which is also not ideal - the code is in reality a lot more complex than is shown here, so I don't want to have to add in this extra parameter unless necessary
The third option which I'm currently using is to pass the userform object to the class and save it there.
So with a Public myForm As UserForm at the top of my class module, I can pass the userform with the Init(uf As UserForm) and have a
Set myForm = uf 'Works with a private "myForm"/ class Property
Or I can set it directly from the userform code with a
Set clsImg.myForm = Me 'only if "myForm" is Public
But what does this do for memory - does saving the userform as a variable in my class take up a lot of memory? Bear in mind that in my real code I declare an array of clsImgs that can be of the order of >100 instances so I don't really want to be making copies of the UF in each class if that's what this method does. Also, it's ugly
What I really want...
... is a way of telling the userform that it needs to repaint, rather than directly repainting from within the class. To me this says I need an event to occur in my class, which the userform hears with some custom event handler. Exactly how Worksheet_Change works, the sheet object raises a change event, the sheet class code handles it.
Is such a thing possible (I suppose I would have to declare clsImg WithEvents - can you do that for an array?), or is there a better alternative. I'm looking for a method which does not impede performance with a large number of classes declared, as well as one which is portable and easily readable. This is my first use of Classes so I may be missing something really obvious!
Since good practice is that classes are self-contained (as you obviously know) the clsImg should indeed not have to be aware of the UserForm and thus shouldn't tell the UserForm to repaint.
What this calls for, is indeed that the clsImg raises an event that the UserForm hooks into, so it repaints based on that event, or, in your own words: "a way of telling the userform that it needs to repaint."
I replicated your Custom Class (clsImg) as follows (wanted to use a proper Setter / Getter, functionality doesn't really change)
clsImg Code:
Private WithEvents myPic As MSForms.Image 'Because we need the click event.
Public Event NeedToRepaint() 'Because we need to raise an event that the UserForm can hook into.
Public Property Let picture(value As MSForms.Image)
Set myPic = value
End Property
Public Property Get picture() As MSForms.Image
Set picture = myPic
End Property
Public Sub myPic_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
myPic.picture = Nothing
RaiseEvent NeedToRepaint
End Sub
Next, in the UserForm we hook into this NeedToRepaint Event that's raised during the Event Handler of the MouseDown of the picture.
UserForm1 Code:
Private WithEvents oneImg As clsImg 'Our custom class
Private Sub oneImg_NeedToRepaint() 'Handling the event of our custom class
Me.Repaint
End Sub
Private Sub UserForm_Initialize()
Dim tmpCtrl As MSForms.Image
Set oneImg = New clsImg
Set tmpCtrl = Me.Controls.Add("Forms.Image.1")
tmpCtrl.picture = LoadPicture("C:\Path\image.jpg")
oneImg.picture = tmpCtrl
End Sub
The second part of your question is whether you can use this in an array.
The short answer is "no" - Each object would have to have it's own Event Handler. However, there are ways to work around this limitation by using a Collection or some similar approach. Still, this wrapper will have to be "UserForm aware" since that's where you'll be repainting. The approach would be something like in this article
EDIT: A solution / workaround for not being able to use an Array:
Since I really liked this question - Here's another approach.
We can apply somewhat of a PubSub pattern as follows:
I did a quick build for CommandButtons, but no reason that it can not be made for other classes of course.
Publisher class:
Public Event ButtonClicked(value As cButton)
Public Sub RegisterButtonClickEvent(value As cButton)
RaiseEvent ButtonClicked(value)
End Sub
'Add any other events + RegisterSubs.
In a regular class, I setup a factory routine to keep this specific Publisher a singleton (as in: It will always be the very same in memory object that you're pointing at):
Private pub As Publisher
Public Function GetPublisher() As Publisher
If pub Is Nothing Then
Set pub = New Publisher
End If
Set GetPublisher = pub
End Function
Next, we have the UserForm (I just made one with 4 buttons) and the button class to utilize this Publisher. The Userform will just subscribe to the event it raises:
Userform code:
Private WithEvents pPub As Publisher 'Use the Publishers events.
Private button() As cButton 'Custom button array
Private Sub pPub_ButtonClicked(value As cButton) 'Hook into Published event.
MsgBox value.button.Caption
End Sub
Private Sub UserForm_Initialize()
Set pPub = GetPublisher 'Private publisher for getting it's event. Will be always the same object as long as you use "GetPublisher"
Dim i As Integer
Dim btn As MSForms.CommandButton
'Create an array of the buttons:
i = -1
For Each btn In Me.Controls
i = i + 1
ReDim Preserve button(0 To i)
Set button(i) = New cButton
button(i).button = btn
Next btn
End Sub
Last we have the cButton class, that centralizes the button events (through the array). Instead of handling each event individually, we just tell the publisher that an Event has been raised.:
Private WithEvents btn As MSForms.CommandButton
Private pPub As Publisher
Public Event btnClicked()
Private Sub btn_Click()
pPub.RegisterButtonClickEvent Me 'Pass the events to the publisher.
End Sub
Public Property Let button(value As MSForms.CommandButton)
Set btn = value
End Property
Public Property Get button() As MSForms.CommandButton
Set button = btn
End Property
Private Sub Class_Initialize()
Set pPub = GetPublisher
End Sub
With this approach we have one "Publisher" that can handle any event from specific classes that register the right event with it. You could also add image events, workbook events, etc.
The publisher itself raises the events we need based on what gets passed to it.
This way the UserForm can be agnostic of the button class and vice versa.
Based on what is supported in VBA, I'm quite confident this is the cleanest approach for your scenario. If anyone has a better idea, I'd love to see another answer.
I did the following, If you pass the control as a control, you can use the parent.
In my form
Public c As Collection
Private Sub UserForm_Initialize()
Dim ctl As Control
Dim cls As clsCustomImage
Set c = New Collection
For Each ctl In Me.Controls
If TypeName(ctl) = "Image" Then
Set cls = New clsCustomImage
cls.init ctl
c.Add cls, CStr(c.Count)
End If
Next ctl
End Sub
and in my class, clsCustomImage
Private WithEvents i As MSForms.Image
Private frm As UserForm
public event evtRepaint
Public Sub init(c As control)
Set frm = c.parent
Set i = c
End Sub
Private Sub Class_Initialize()
End Sub
Private Sub Class_Terminate()
Set frm = Nothing
Set i = Nothing
End Sub
'
Private Sub i_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
i.Picture = Nothing
frm.Repaint
raiseevent evtRepaint
End Sub
EDIT
To have a single handler, you'd need to look at something along these lines, in a class called say clsHoldAndHandle
Private c As Collection
Private f As UserForm
Private WithEvents cls As clsCustomImage
Public Sub AddControl(ctl As Control)
Set cls = new clsCustomImage
If f Is Nothing Then Set f = ctl.Parent
cls.init ctl
c.Add cls, CStr(c.Count)
End Sub
Private Sub Class_Initialize()
Set c = New Collection
End Sub
Private Sub cls_evtRepaint()
f.Repaint
End Sub

VBA Excel: dynamically created button in a class module runs immediately and only once

I am trying to make class that would create a button. The button should have a macro assigned to it. The macro is a function of the class.
The code of the class module is the following:
'Class Module: btnClass
Option Explicit
Dim btn As Button
Function addButton()
'Adding a button
Set btn = ActiveSheet.Buttons.Add( _
Range("A1").Left, _
Range("A1").Top, _
Range("A1").Width, _
Range("A1").Height)
With btn
'Assigning a function
.OnAction = Me.onClickAction
.Caption = "Button"
End With
End Function
Function onClickAction()
MsgBox ("Click")
End Function
The code of the main macro is the following:
'Module
Option Explicit
Sub main()
Dim btnInstance As btnClass
Set btnInstance = New btnClass
'Calling a function of the instance that creates a button
Call btnInstance.addButton
End Sub
The code above creates a button successfully. However, the function assigned to the button is run immediately (right after the button is created, not when I click on it), and only once (when you click on the button later, nothing happens).
Is there a way in VBA to implement the required functionality using class modules (want to create a class that does not rely on the outside functions)?
To expand on Rory's comment, your class needs something like this:
Public WithEvents Button As CommandButton
Private Sub Class_Initialize()
Set Me.Button = Sheet1.OLEObjects("Thebutton").Object
End Sub
Private Sub Button_Click()
MsgBox "Foo"
End Sub
Then in a normal module create a public instance of the class so it stays in memmory:
Public myButt As ButtonClass
Public Sub AddEvent()
Set myButt = New ButtonClass
End Sub
Note that the click event will only be handled as long as the instance of the class remains in memory. If you close the workbook and open it again the event will no longer be handled.
Edit: I forgot to mention, you need to set a reference to Microsoft Forms in order to declare a variable of type CommandButton.