VBA UserForm running twice when changing .Caption - vba

I'm running a VBA macro from SolidWorks. The form doubles as an input for two types of document. In the UserForm.Initialize subroutine I'm changing the name of the UserForm's Caption depending on which document type is open. Whenever I do this though, the program reruns UserForm.Initialize, and when it's all done, it carries on from where it left of, effectively running twice.
Does anyone know a way around this bizarre behaviour? I tried putting the FormName.Caption command into its own Sub but the result is the same.
Many thanks.

I can't replicate the problem and I don't know what SolidWorks is, so that may have something to do with it. Perhaps you can post a made-up example that shows Initialize being called twice.
My guess would be that it's related to auto-instantiating variables. When you use UserForm1, you are instantiating an object variable called UserForm1 that points to an object, also called UserForm1. It's similar to using the New keyword in a Dim statement. You never defined UserForm1 (the variable), but VBA did and the first time you use it, it instantiates automatically.
You should try to use the Me keyword when working inside the userforms class module (userforms are classes just like other objects except that they have a user interface element). In the Initialize event, say
Me.Caption = "blah"
instead of
UserForm1.Caption = "blah"
It could be (just a theory that I wasn't able to prove) that the flag that gets set to say "I'm pointing to a real instance" isn't set by the time you change the Caption property, and that by using the auto-instantiating variable UserForm1, you are forcing another instantiation.
Even better, don't use auto-instantiating variables, convenient though they are (and don't use the New keyword in a Dim statement either). You can control when your variables are created and destroyed and it's a best practice. Something like this in a standard module
Sub uftst()
Dim uf As UserForm1
Set uf = New UserForm1 'you control instantiation here
'Now you can change properties before you show it
uf.Caption = "blech"
uf.Show
Set uf = Nothing 'overkill, but you control destruction here
End Sub
Note that if the ShowModal property is set to False that the code will continue to execute, so don't destroy the variable if running modeless.

As Dick suggested, you should be able to stop the behavior by making sure to use me.caption instead of Userform1.caption.
Here's a way you can replicate the issue for those who are curious:
Create a Userform (Userform1) make sure you set ShowModal to false or you won't be able to see this.
In a module add the following:
Option Explicit
Sub ShowUserForm()
Dim uf As UserForm1
Set uf = New UserForm1
End Sub
In UserForm1 list the following code:
Option Explicit
Private Sub UserForm_Initialize()
UserForm1.Caption = "I'm UserForm1!" 'This will call the Initialize method of Userform1 not Me.
Me.Caption = "I'm Me!"
Me.Show
End Sub
Run ShowUserForm. You now have two Userforms with different captions.
Incidentally, if you have an Initialize method like I displayed adding Set uf = Nothing to the ShowUserForm sub actually fails to close either form.

Related

How to make checkbox control element reference itself [duplicate]

I have a bunch of TextBox-Button pairs on a form. When the button is clicked I want to insert the value of the text box into a database. The name TextBoxes and Buttons follow a naming standard, for example Value1Tb - Value1Cmd and Value2Tb - Value2Cmd.
My problem is that since I want to do the same for every button I would like the possibility to write a Sub like:
Private Sub AnyButton_Click(sender As CommandButton)
Dim tb As TextBox
Set tb = GetTBByName(s.Name)
PutValueToDatabase(s.Name,tb.Text)
End Sub
But I cannot find a way to point the Click-event of a Button to a different sub than the standard Name_Click().
Anybody know a way around this, that doesn't involve me writing 50 or so different Name_Click() subs?
If you are OK to use Form Controls rather that ActiveX, as it looks as though you may be at the moment, then Chris' solution seems good.
However if you need ActiveX CommandButtons then you are unable (as the VBA compiler will tell you, "Procedure declaration does not match...") to have parameters in the callback for the click event, and you are unable to raise the event from multiple objects, although you do of course know which button raised the event (since the relationship is 1 CommandButton = 1 Sub).
So... I would go with something like:
Private Sub Value1Cmd_Click()
Call TheMethod(Value1Cmd)
End Sub
Private Sub Value2Cmd_Click()
Call TheMethod(Value2Cmd)
End Sub
Private Sub TheRealMethod(sender As CommandButton)
' Do your thing '
Dim tb As TextBox
Set tb = GetTBByName(s.Name)
PutValueToDatabase(s.Name,tb.Text)
' Etcetera... '
End Sub
Requires a stub for each button, so some copying and pasting to begin with, but then easy to maintain etcetera as all _Click event callbacks are pointing at the same method...
Edit:
E.g.
Sub AutoWriteTheStubs()
Dim theStubs As String
Dim i As Long
For i = 1 To 10
theStubs = theStubs & "Private Sub Value" & CStr(i) & "Cmd_Click()" & vbCrLf _
& " Call TheMethod(Value" & CStr(i) & "Cmd)" & vbCrLf _
& "End Sub" & vbCrLf & vbCrLf
Next i
Debug.Print theStubs
End Sub
It seems that what you want is to get the name of the clicked button. If you are creating buttons like this:
(where 'i' increments in a loop)
Set btn = Sheet1.Buttons.Add( , , , ,)
With btn
.OnAction = "btnSub"
.Caption = "Upadate"
.Name = "btn" & CStr(i) & "Cmd"
End With
and then defining a generic "private sub btnSub()" for all the buttons, you could at least get the name of the button that was clicked using Application.Caller. Something like:
Private Sub btnSub()
Dim ButtonName As String
ButtonName = Application.Caller
MsgBox ("Hello:" & ButtonName)
End Sub
Hope it helps!
I decided to make this an answer because I am doing something similar and I confirmed that it works.
You can store the OLEobjects in a Collection, of arbitrary size, containing Custom Class Objects that include the OLEobjects and associations and the events that you need. Thus you can completely avoid any code stubs.
Create a Custom Class to bind the Button and TextBox pairs.
Declare the Button object WithEvents.
Include your call-back in the exposed button event handler in the Class Module.
Put a Public routine in a Standard Module to initialise a Collection of these Custom Class objects by spanning the Form Controls. You can also use this to Add the controls programmatically as a 'reBuild' option. The Collection can be inside another Class Module with all of the management routines, but it needs to be Instantiated and loaded in a Standard Module.
Put a public routine in a standard module to receive the call-backs with whatever context you need. This can also be in a Worksheet Module if it makes for better encapsulation. You can use late binding to reference the callback or CallByName.
You need to bear in mind that the Module of the Form will recompile every time you add a control, so you have to be careful where you put your code.
My application has the controls directly on the Worksheet Surface, so I can't put the the Collection Class in, or source any initialisation of the Collection from the Worksheet module. This would amount to self modifying code and it grinds excel to a halt.
I dreamed this idea up through bloody-minded idealism (not necessarily a good thing) but, of course, I was not the first one to think of it as you can see here. #Tim Williams explains it in his answer. You can also google VBA Control Array Events to see plenty of similar examples including an excellent article by #SiddharthRout. In line with the VB6 analogy, he uses an Array instead of a Collection to achieve the same result.
I'll try to post some code later. My application is a bit different so it will take a lot of work to trim it down, but the principle is the same.
The other thing to bear in mind is that VBE really struggles with this type of thing so don't worry if it is loading up you processors. After you re-start with VBE off, all will be fine.
I have this same situation, and I just have a click event for every button that is a wrapper to the function I want to call. This also allows you to pass sheet-specific parameters if you need to.
Example:
Public Sub StoreButton_Click()
' Store values for transaction sheet 3/27/09 ljr
Call StoreTransValues(ActiveSheet)
End Sub
I just published (Open Source) the Event Centralizer for MSForms.
Citation: "The Event Centralizer for MSForms is a VBA programming tool that allows all sorts of custom grouping when writing handlers for the events occurring in UserForms.
With the Event Centralizer for MSForms, it is easy for example to have all TextBoxes react the same way when the Enter event occurs, or all except one, or only those with a particular Tag value.
Thanks to its events logs system, the Event Centralizer for MSForms is a powerful learning and debugging help."
I can't explain here how it works. I tried to do it on the site.
Set the event to =FunctionName(parameter).
A bit late but this may help someone else:
If you have a function called OpenDocumentById(docID as integer), then each control calling the function would have in the event the following:
cmd1's On Click event:
=OpenDocumentById([DocID])
cmd2's On Click event:
=OpenDocumentById([DocID])
etc...

VBA Calling a private function inside of a userform

I have a userform defined as "SingleListForm".
I have a private sub named "loadSingleListForm(ID As Double)"
I am attempting to pass a variable into the form and I was implementing it in the fashion that loadSingleListForm would set the form up based on ID, basically re-using one form to show variable data from variable sources in the listbox.
But calling a standard Intersect from outside the form (Worksheet_SelectionChange) these two options compile but do not work.
Application.Run "SingleListForm.loadSingleListForm", ID 'ID already declared and assigned
This doesn't work either
Call ActiveWorkbook.UserForm("SingleListForm").loadSingleListForm(ID)
Where it says UserForm I have also tried SingleListForm.
Here the runtime error is:
I am trying hard not to use a Global Variable here to pass to the form.
Perhaps I should go to Initialize and try something there.
I am trying to pass the variable to the form and then of course set up the form based on this case and then show the form. you can't pass with show so you have to find another way to set up.
I just realized I have not called a userform private function from outside of the form before, but I do it with modules all the time. The first case works in that instance.
Cheers,
-WWC
The better way to do this is to declare a property to the form. In the form's module enter
Option Explicit
Private myID as double
Property Set ID(i as double)
myID = i
End Property
Then your function
Private Sub loadSingleListForm()
can refer to myID with in it's code
To Use this from outside modules you use
Load SingleListForm
SingleListForm.ID = ID 'ID variable already declared
Declare your sub in the form as Public Public Sub loadSingleListForm(ID As Double) and then call it like this SingleListForm.loadSingleListForm ID
Just to cover this. Empty workbook, one button.
The button calls to a private function in the form that does nothing but open a message box. Testing concept here.
This is all there is:
Doesn't work:
UserForm1.you_made_it
Error at compile, method or data member not found
Same if this:
With ThisWorkbook
UserForm1.you_made_it
End With
Then try this:
Application.Run "UserForm1.you_made_it"
Error: Cannot run macro . . . .
Try this from first comment:
ActiveWorkbook.UserForm("UserForm1").you_made_it
Error: Object doesn't support this property or method
So this is the winner from above. Not sure I wanted to go Public but it works.
Doesn't solve how to use a private member in the form but it gets the coding going forward.
Public Sub you_made_it()
MsgBox ("you made it")
End Sub
So far:
1) Move the private to a module and then call it
2) make the function Public and it works
Thank you,
-WWC

VBA- UserForm.Lable.Caption gets Error 91 Object Not Set

System Description: I have a userform that takes input on an item that is being returned. A user clicks the row of the item that needs to be returned and then clicks a "Check-In button"
My Attempt: I created a button checkin_cmdbutton on the spreadsheet that measures which item is selected by which cell is selected Application.ActiveCell.Row, writes the info into a userform Checkin_Form, the user finishes the rest of the check-in info, and clicks submit.
This code is the event for the button checkin_cmdbutton on the spreadsheet:
Private Sub checkin_cmdbutton_Click()
Set ItemID = Cells(Application.ActiveCell.Row, 1)
Set ItemDescription = Cells(Application.ActiveCell.Row, 2)
If ItemID Is Nothing Then
MsgBox ("ID is null, ending...")
Exit Sub
End If
Checkin_Form.UserForm_Initialize
Checkin_Form.itemid_dynamiclabel.Caption = ItemID.Value
Checkin_Form.description_dynamiclabel.Caption = ItemDescription.Value
Checkin_Form.checkin_datepicker.Value = Date
Checkin_Form.Show
End Sub
Problem: The code throws an error 91 "Object variable or with block variable not set" at Checkin_Form.itemid_dynamiclabel.caption and the following 2 lines. Why is an object on a form throwing this error? I can't declare these, can I?
You shouldn't be explicitly calling UserForm_Initialize - that's an event handler, and there's a reason handlers are Private by default: they're invoked by the event provider, when the event provider deems it necessary - in this case, when the object instance is getting initialized.
The best way to ensure the form gets initialized properly, is to treat it like the object it is, instead of storing global state on its default instance.
A UserForm class is little more than a class module with a designer and a VB_PredeclaredId module attribute. This attribute makes VBA create a global-scope object variable named after the class, and that is how this code is legal:
UserForm1.Show
Except, it shouldn't be.
You DON'T want to store global state in the default instance: that's the very last thing you want, especially if your form involves dynamic controls.
New it up instead.
With New UserForm1
.Show
'what follows only executes when the form is closed:
'...
End With
For this to work, you must handle the form's QueryClose event, to prevent the object instance from self-destructing itself when the user clicks the [X] button.
For this to work, you must also avoid destroying the form yourself, e.g. with Unload Me (or worse, Unload UserForm1) calls - say, when the user clicks the [Ok] button. Instead, you Hide (or Me.Hide) the form, so that the caller (the code that New'd it up) can still access the object's state.
From the look of your code - i.e. with the .Show call being the very last thing your macro does, I can tell that you're having the form run the show: this is an anti-pattern that will keep creating problems every time you do that.
Forms don't implement application logic: forms present and collect data. Nothing more, nothing less. It's not the form's job to write to any spreadsheet, or even to know anything about worksheets.
Read this recent article of mine if you want more information about doing forms right.
Now, the actual problem.
Checkin_Form.itemid_dynamiclabel.Caption = ItemID.Value
If that label is dynamic (i.e. created at run-time), then I'm surprised accessing it like this even compiles. First, remove the underscore in the form's name: underscores have a special meaning in VBA - I'm sure you've noticed the pattern by now, of how VBA generates event handlers for a given object:
Private Sub ObjectName_EventName()
End Sub
If ObjectName or EventName has an underscore, you're asking for compile errors at one point or another - one day you'll want to use an Implements statement and discover that your code can't be compiled anymore, if you kept that underscore habit: better lose it now.
If the control is dynamic, you can't do what you're trying to do the way you're doing it.
Dynamic controls need to be accessed through the form's Controls collection:
Dim myLabel As MSForms.Label
Set myLabel = Me.Controls("NameOfTheLabelControl")
Otherwise, you need to keep a reference to the dynamic contols at module-level, in the form's code-behind - you could expose it via a property:
Option Explicit
Dim myLabel As MSForms.Label
Private Sub UserForm_Initialize()
Set myLabel = Me.Controls.Add(...)
End Sub
Public Property Get ThatLabel() As MSForms.Label
Set ThatLabel = myLabel
End Property
Or better, use an actual model class, and let the calling code not be bothered with controls at all - see the previously linked article for details.
TL;DR:
You're getting that error because your label object instance isn't initialized, i.e. it's Nothing. Since you aren't showing your form's code-behind, we can't really point out why that is the case, but my money is on the form's default instance making you yet another victim of the "hey look how easy it is!" VBA tutorials that teach things wrong.
Implement the worksheet-handling code outside the form, make the form collect data, make the calling code read this data after the form is hidden, and then make the calling code create and destroy the form instance.
Now, with all that said, I've no idea why you think you need a dynamic control for this.
Just shooting in the dark, as far as I really do not know the names of your variables and what they are (a few screenshots will be helpful). Try like this, if your code is in a form (as far as you have _Click I assume it is):
Private Sub checkin_cmdbutton_Click()
Set ItemID = Cells(Application.ActiveCell.Row, 1)
Set ItemDescription = Cells(Application.ActiveCell.Row, 2)
Me.itemid_dynamiclabel.Caption = ItemID.Value
Me.description_dynamiclabel.Caption = ItemDescription.Value
Me.checkin_datepicker.Value = Date
Me.Show
End Sub
And try at least declaring the variables (e.g. ItemID etc) and using Option Explicit on top.

How do I effectively create controls dynamically in Excel's VBA or How do I use Application.OnTime()?

I am working on a very large VBA project in Excel at my job. We are about 1500 lines of code for just one feature and have about a dozen more features to add. Because of this, I've been trying to break everything down so that I can keep code for each feature in separate places. OOP sucks in VBA... The problem being that these controls MUST have events fired. Of course, some events (like the TextBox_AfterUpdate event) are not available when you dynamically create controls. It's a bit convoluted because of everything that is going on, so I'll break it down the best I can:
I have a class module that represents a tab for a multipage control. When a user clicks on a tab, the Userform calls this class module and THERE I have the controls created dynamically. This way I can keep the code in that class module. I have a sub that I deemed as the "AfterUpdate" sub and put code that I needed to run there. Now the problem is to get that sub to be called at the appropriate time.
So what I did is to set up a Timer of sorts to check and see if the "ActiveControl" is said textbox. If it is not, we can assume that focus has left and we can raise that event. Here's the code I'm using:
An abbreviated version of the tab creation...
Private WithEvents cmbMarketplace As MSForms.ComboBox
Public Sub LoadTab(ByVal oPageTab As Object)
If TabLoaded Then Exit Sub
Set PageTab = oPageTab
Dim tmp As Object
Set tmp = PageTab.Add("Forms.Label.1")
tmp.Top = 6: tmp.Left = 6: tmp.Width = 48
tmp.Caption = "Marketplace:"
Set cmbMarketplace = PageTab.Add("Forms.ComboBox.1", "cmbMarketplace")
' LOAD OTHER CONTROLS '
TabLoaded = True
Start_Timer
End Sub
Then Start_Timer:
Public Sub Start_Timer()
TimerActive = True
Application.OnTime Now() + TimeValue("00:00:01"), "Timer"
End Sub
And the sub that is to be fired:
Public Sub Timer()
If TimerActive Then
' DO SOME RANDOM THINGS '
Application.OnTime Now() + TimeValue("00:00:01"), "Timer"
End If
End Sub
Does this seem like a reasonable approach to solving the problem I'm facing? I'm open to suggestions...
That's the first problem. This seems like a lot of work to accomplish this. (I'm working on getting visual studio, but I don't know if that's going to happen)
The above code will work but the "Timer" sub will not get raised at all. I get no errors if I just run the code. Everything is created, everything works as I would hope. However, if I step through the code, I eventually will get the following error:
Cannot run the macro "...xlsm!Timer". The macro may not be available in this workbook or all macros may be disabled.
Obviously neither of those suggestions are valid. Macros ARE enabled and the sub is in the same darn class module. I tried making it public, same problem. Tried "ClassModule1!Timer" to no avail. I'm at my wits end trying to figure this out. Thinking of having people write ALL this in the Userform or just giving up.
Does anybody have any suggestions on how to effectively break up large chunks of code? And does anybody have a clue why this sub will not run and seemingly cannot be found?
I understand that this is a confusing situation, so if you need more info or code examples or want to know why I have something set up the way I do, let me know.
Thanks!
Obviously neither of those suggestions are valid. Macros ARE enabled and the sub is in the same darn class module.
There's the problem: a macro cannot be in a class module. The message is entirely correct: VBA cannot see the Timer procedure, because it's not accessible.
A class module is a blueprint for an object, VBA (or any OOP language for that matter) can't do anything with a class module, without an instance of that class - i.e. an object.
Your timer callback needs to be a Public Sub in a standard module, so that it can be called directly as a macro. Public procedures of a class modules are methods, not macros.
Depending on what ' DO SOME RANDOM THINGS ' actually stands for, this may or may not require some restructuring.
1500-liner spaghetti code can be written in any language BTW.

Catch a value from form1 to form2 in vb [closed]

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question appears to be off-topic because it lacks sufficient information to diagnose the problem. Describe your problem in more detail or include a minimal example in the question itself.
Closed 8 years ago.
Improve this question
In VB, I create 2 forms in 1 project. In form1, I put 3 buttons with different value. In form2, I only put 1 textbox with no value.
My question is, how if I press one of the button in form1, the form2 is automatically opened and the value from the button that I press automatically added to the form2 textbox?
Add following code into your button handler. You can double click button and add the code into event handler which is automatically created:
'Here we are creating actual object and passing string into it constructor method
Dim instanceOfForm2 = new Form2("String value from Form1!")
instanceOfForm2.Show() ' Showing form
In Form2 we need to tweak our constructor to accept one parameter:
Public Sub New(someValue as String)
InitializeComponents() 'This is always first row in form constructor
TextBox1.Text = someValue 'And put that text into textbox...
End Sub
In VB6 you can do all the stuff that lardymonkey has in place, but you don't have to. The most concise way to do what you want is this. First, make your three command buttons in Form1 into a control array. To do this, give them all the same name (I'll use "cmdMyButtons" in my example), and set their index properties to 0, 1, and 2. Then do this in Form1's code window:
Option Explicit
Dim ButtonText(2) As String
Public Sub Form_Load()
ButtonText(0) = "First Button Text"
ButtonText(1) = "Second Button Text"
ButtonText(2) = "Third Button Text"
End Sub
Public Sub cmdMyButtons_Click(Index As Integer)
With Form2
.txtMyTextBox.Text = ButtonText(Index)
.Show vbModal
End With
End Sub
Now, I like lardymonkey's idea of showing modally, so I put it in here as well. However, several things in his code sample aren't intrinsically necessary to do what you want, and create overhead, so I pulled them out:
You don't need to make a property; you can just set the text
directly as I have here.
You don't need to create a form variable; you can just reference the form directly as I have here.
You don't have to load the form explicitly; the form gets
automatically loaded as soon as you set the variable (by the way,
the Show method also automatically loads the form, too--you only use
Load when you want to have the form loaded into memory before you do
anything to it).
If you close the modal form it will be
automatically unloaded. However, unloading a form doesn't set any
object variables referencing it to nothing. Therefore, frmDetail
will not be Nothing when you check it, you will unload a form that
isn't loaded. While this doesn't throw an error (the statement is ignored), I wouldn't do it anyway. So, you don't
need any of the "make sure the form is destroyed" code.
And now, for a short lecture on the whole business of always explicitly destroying object variables:
There is a longstanding argument about whether you need to explicitly set all your local object variables to Nothing before exiting a subroutine in VB6. I don't agree with this at all; VB takes care of this automatically when the variables go out of scope. As far as I can see, the reason that people believe that they have to do this is that the scope finalizer doesn't collect garbage in any particular order, and sometimes two interacting COM objects need to be destroyed in a particular order due to poor coupling architecture. In such a case, you do indeed need to clear the objects in the correct order to work around intermittent bugs, so the myth developed that VB's garbage collection is buggy and needs to be circumvented by always manually destroying object variables.
Frankly, the idea that a programmer is going to always do this and never forget is naive. So I persist in disagreeing with it; the developers of VB6 put a lot more thought and effort into developing the scope finalizer than any programmer is going to put into circumventing it.
Without knowing the specific version of the software you are using we cant answer, we can answer it if you provide the correct version
In .net it's a simple as creating the form then passing the value over.
Friend objForm2 as New Form2
Private Sub button1_Click(ByVal sender As System.Object, ByVal e as System.EventArgs) Handles button1.Click
objForm2 = new Form2()
TextBox1.Text = value
End Sub
This would be a way of doing it in VB6. I've left the error handling up to you.
I've made the assumption that the name of the text box is txtOutput on form2.
In Form2 add the following:
Option Explicit
Public Property Let OutputText(ByVal strOut As String)
txtOutput.Text = strOut
End Property
In Form1 add the following:
Option Explicit
Private Sub Command1_Click()
DisplayForm "1"
End Sub
Private Sub Command2_Click()
DisplayForm "2"
End Sub
Private Sub Command3_Click()
DisplayForm "3"
End Sub
Private Sub DisplayForm(ByVal strValue As String)
'Declare the variables
Dim frmDetail As Form2
'Create the form
Set frmDetail = New Form2
'Load the form and display it modal
Load frmDetail
frmDetail.OutputText = strValue
frmDetail.Show vbModal, Me
'Make sure the form is destoryed
If Not frmDetail Is Nothing Then
Unload frmDetail
Set frmDetail = Nothing
End If
End Sub
Make sure you comment the code and if you need some error handling then add it. If you need help with the VB6 functions you can find it here MSDN VB6 Reference