Obtaining a value from a userform - vba

I'm trying to get a value from this userform to determine the next step to selecting a material.
Private Sub UserForm_Initialize()
Dim Material(1, 0)
Dim MaterialC As Variant
Material(0, 0) = "Carbon Steel"
Material(1, 0) = "Stainless Steel"
ComboBox1.List = Material
ComboBox1.Value = Material
Worksheets("Sheet2").Range("A1").Value = MaterialC
End Sub
Also would really appreciate it if someone could briefly clarify the difference in a sub and a private sub?
I want to use the choice of the 2 materials to present the user with a set of sizes unique to either choice, so I need to know what they chose.
Also, is there a more efficient way of pasting code than adding 4 spaces before each line of code?

The difference is the Private keyword. Private is defining the scope of your subroutine, which is that you can only use it inside your Module. Public means that you can call it from outside the Module as well. Since Public is the default scope modifier, the difference between a Sub and a Private Sub is that the Sub will be Publicly usable, while a Private Sub will be kept Private.
For more information read this.

Related

Reducing Decoupling in VBA Object Event Wrapper

tl;dr Is there a way to enable events for built-in objects without coupling the event to the original object's parent, assuming the event interacts with the parent?
Disclaimer 1: I don't have access to MS Office on my home machine and therefore type all code from memory. I'm sorry if something's incorrect.
Disclaimer 2: This post is incredibly lengthy because I've been trying to figure out how to do this process for several years but never quite hit the correct Google terms to figure it out. I do a lot of explaining in the hopes that it might help someone with the same issues.
The Original Problem
I've had this longstanding issue of having Userforms with near-identical event handling but no way to compact the code into a generic solution. For example, let's say I have a Userform with a bunch of Command Buttons that all do the same thing when clicked. Traditionally, you would have to include something like the following in Userform1
Private Sub CommandButton1_Click()
Me.DoSomething CommandButton1.Name
End Sub
Private Sub CommandButton2_Click()
Me.DoSomething CommandButton2.Name
End Sub
'...a bunch more of these...'
Private Sub CommandButtonN_Click()
Me.DoSomething CommandButtonN.Name
End Sub
This is annoying to setup and hurts readability for a large number of buttons.
The Naive Solution
I recently discovered that wrapper classes can be utilized to make a generic WithEvents handler for built-in objects. Applying this to our previous example, we create an EventCommandButton.cls Class with the following code
Private WithEvents mCommandButton as MSForms.CommandButton
Private Sub mCommandButton_Click()
mCommandButton.Parent.DoSomething(mCommandButton.Name)
End Sub
Property Get CommandButton() as MSForms.CommandButton
Set CommandButton = mCommandButton
End Property
Property Set CommandButton(cmdBtn as MSForms.CommandButton)
Set mCommandButton = cmdBtn
End Property
And Userform1 turns into
Private EventCommandButtons() as New EventCommandButton
Private Sub Userform1_Initialize()
For Each ctl in Me.Controls
If TypeName(ctl) = "CommandButton" Then
i = i + 1
ReDim Preserve EventCommandButtons(1 to i)
Set EventCommandButtons(i).CommandButton = ctl
End If
Next
End Sub
This approach saves space and looks comparatively nice, but it presents (at least) 2 major issues:
All of Userform1's control events are no longer housed in its own code
Our EventCommandButton requires a specific procedure (DoSomething(str)) to exist in its parent or else we'll get an error.
A Slight Refinement
The solution I'm currently implementing is to take a more intuitive approach that returns control of the event handling back to where you'd expect it to be. In EventCommandButton.cls we add a new property to specify where we expect to find the return code:
Private mCommandButton as MSForms.CommandButton
Private mCallback as Object
Private Sub mCommandButton_Click()
'Some error handling should be here to check that mCallback is set
mCallback.EventCommandButton_Click(mCommandButton)
End Sub
Property Get Callback() as Object
Set Callback = mCallback
End Property
Property Set Callback(ParentObject as Object)
'Let's not assume it's always the .Parent
Set mCallback = ParentObject
End Property
Property Get CommandButton() as MSForms.CommandButton
Set CommandButton = mCommandButton
End Property
Property Set CommandButton(cmdBtn as MSForms.CommandButton)
Set mCommandButton = cmdBtn
End Property
And in Userform1
Private EventCommandButtons() as New EventCommandButton
Public Sub EventCommandButton_Click(cmdBtn as MSForms.CommandButton)
Me.DoSomething cmdBtn.name
End Sub
Private Sub Userform1_Initialize()
For Each ctl in Me.Controls
If TypeName(ctl) = "CommandButton" Then
i = i + 1
ReDim Preserve EventCommandButtons(1 to i)
Set EventCommandButtons(i).CommandButton = ctl
Set EventCommandButtons(i).Callback = Me 'Set new property
End If
Next
End Sub
This approach feels close to the intuitive solution of the original problem (with some extra steps involved) and resolves issue #1 from the previous, but we still have issues:
There's still coupling between the Class and Userform, now requiring that each parent object must have corresponding pseudo-event procedures of the form Public Sub [ClassName]_[EventName]([OriginalObject], Optional [EventParams]), which isn't intuitive and looks weird amongst the sea of Private Event Subs.
The coupling now depends on the class name, which may not always be ideal. Renaming the class will require editing the events to reflect that.
For the wrapper to be "complete", it must include all events and error handling to ignore the ones that aren't setup on the Parent side. At some point I'd think having all these On Error GoTo EoF statements in each class instance will have a performance impact.
The Question
Is there a way that this process can be further improved to reduce the coupling between (in this case) the Class and Form code? With VBIDE we could detect the classname and generate the pseudo-events, but without VBIDE access it seems like it requires some upkeeping and instruction to properly utilize the class.
In Python (and I'm sure other languages), you could just pass a reference to a function to direct the event returns; however, VBA doesn't seem to support this.
If you can pass the method name from the parent as a string you could use something like CallByName mCallback, vbMethod, mProcName, mCommandButton from within the class instance, to call the method mProcName on the parent, passing the clicked-on button.
For example:
Event class (properties changed to public fields for brevity)
Option Explicit
Public WithEvents mCommandButton As MSForms.CommandButton
Public mCallback As Object '<< object on which the callback method is to be called
Public mProcName As String '<< name of the callback method
Private Sub mCommandButton_Click()
CallByName mCallback, mProcName, VbMethod, mCommandButton
End Sub
Form code:
Private EventCommandButtons As Collection
Public Sub ButtonClick(cmdBtn As MSForms.CommandButton)
MsgBox "clicked on button " & cmdBtn.Caption
End Sub
Private Sub Userform_Initialize()
Dim ctl As Object
Set EventCommandButtons = New Collection
For Each ctl In Me.Controls
If TypeName(ctl) = "CommandButton" Then
EventCommandButtons.Add NewClickHandler(ctl)
End If
Next
End Sub
Function NewClickHandler(btn As Object) As EventCommandButton
Set NewClickHandler = New EventCommandButton
Set NewClickHandler.mCommandButton = btn
Set NewClickHandler.mCallback = Me
NewClickHandler.mProcName = "ButtonClick"
End Function

How do I get a reference to the Application object of a UserForm?

If I want to write a Sub that positions a UserForm relative to the Application object that displays it, How would I go about doing it?
I want to write a sub that goes like this:
Sub PositionForm(WhichForm as Object)
WhichForm.Left = <WhichForm Application Object>.Left
End Sub
I understand that there are many workarounds to this. I am interested in knowing whether there is a way to getting that reference.
in Excel the following works:
Sub PositionForm(WhichForm As Object)
WhichForm.Left = Application.Left
End Sub
to be called from any UserForm code as:
Private Sub CommandButton2_Click()
... any code
PositionForm Me
... any code
End Sub

Calling a userform from a specific sheet sub

Another newbie question but I cannot find my answer anywhere so far...
I have a workbook with several sheets, lets call them S1, S2 etc., I have a userform that does an operation that can be activated from any of the sheet.
My problem here is that I have parameters passed to the userform from the sub
Public c As Integer, lf As Integer, ld As Integer
Sub Tri()
ld = 8
lf = 128
Application.ScreenUpdating = False
UsForm.Show
End Sub
Now my workbook is growing in size and differences appear from S1 to S2 etc requiring me to change parameters depending on the sheet it is launched from.
So i removed my code from "module" and put it in the "Microsoft excel object" part. But it now seems it does not have access to my public variables and as soon as I request ld or lf, it is shown as empty (even if it was implemented in the previous userform).
Please can someone tell me what I'm missing ? How can I do otherwise (I do not want to put the data in the sheets themselves)?
You need to take advantage of the fact that a userform is a class. So as an example add the following code to the "form". Let's assume you have a button with the name CommandButton1
Option Explicit
Dim mVar1 As Long
Dim mVar2 As String
Property Let Var1(nVal As Long)
mVar1 = nVal
End Property
Property Let Var2(nVal As String)
mVar2 = nVal
End Property
Private Sub CommandButton1_Click()
MsgBox mVar1 & " - " & mVar2
Me.Hide
End Sub
Then you can add in a normal Module
Sub TestForm()
Dim frm As UserForm1
Set frm = New UserForm1
Load frm
frm.Var1 = 42
frm.Var2 = "Test"
frm.Show
Unload frm
End Sub
In such a way you can pass variables to a form without using global variables.
Here is a widely accepted answer about Variable Scopes. https://stackoverflow.com/a/3815797/3961708
If you have decalred your variable inside thisworkbook, you need to access it by fully qualifying it. Like ThisWorkbook.VariableName
But with UserForms I recommend to use Properties for data flow. Thats the clean and robust way to do it. Get in the habit of using properties and you will find it highly beneficial for UserForms.
Example:
Add this code in the ThisWorkbook
Option Explicit
'/ As this variable is defined in ThisWorkBook, you need to qualify it to access anywher else.
'/ Example ThisWorkbook.x
Public x As Integer
Sub test()
Dim uf As New UserForm1
x = 10
'/ Set the propertyvalue
uf.TestSquare = 5
'/Show the form
uf.Show
'/ Get the property value
MsgBox "Square is (by property) : " & uf.TestSquare
'/Get Variable
MsgBox "Square is (by variable) : " & x
Unload uf
End Sub
Now add a UserForm, called UserForm1 and add this code
Option Explicit
Private m_lTestSquare As Long
Public Property Get TestSquare() As Long
TestSquare = m_lTestSquare
End Property
Public Property Let TestSquare(ByVal lNewValue As Long)
m_lTestSquare = lNewValue
End Property
Private Sub UserForm_Click()
'/ Accessing the Variable Defined inside ThisWorkkbook
ThisWorkbook.x = ThisWorkbook.x * ThisWorkbook.x
'/ Changing Property Value
Me.TestSquare = Me.TestSquare * Me.TestSquare
Me.Hide
End Sub
Now when you run the Test sub from ThisWorkbook you will see how you can access variables and properties across the code.

Update a userform listbox after userform initialization

Is there any way to update a ListBox on a UserForm outside of the Userform_Initialize sub?
Why?
I am building a blackjack game and using listboxes to tell the user what cards they have/ the dealer has. I was hoping to use a simple sub (ShowCards) to add items to the listboxes but I've run into problems:
The Play button calls the PlayBlackjack sub which sits in a normal module
Option Explicit
Dim cards As New Collection
Sub ShowGame()
UFDisplay.Show
End Sub
Sub PlayBlackjack()
'fill the cards collection with 5 shuffled decks
PrepareCards
Dim i As Integer
Dim userHand As New Collection
Dim dealerHand As New Collection
'deal cards (removing the dealt cards from the cards collection)
For i = 1 To 2
DealCard cards, userHand
DealCard cards, dealerHand
Next i
ShowCards userHand, UFDisplay.UserHandList <-- ERROR HERE (Type mismatch)
'more code to follow
End Sub
Private Sub ShowCards(hand As Collection, list As ListBox)
Dim i As Integer
For i = 1 To hand.Count
list.AddItem hand(i).CardName
Next i
End Sub
Let me know if you think you need any more of the code. hand is a collection of card classes where .CardName returns something like 3 of Hearts
Everything I read seems to tell me that the userform is static after being initialized so I would need to refresh it in some way after adding the new items. I tried Userform.Repaint with no luck.
So if there really is no other way, should I declare userHand and dealerHand as global variables, update them and call Useform_Initialize to take the updated values and show them to the user? Given the nature of the game being that multiple more cards could be dealt for both players it doesn't seem sensible to be re-initializing the userform multiple times each game.
All suggestions welcome. If you think I should have done it completely differently I'd still be keen to hear (but not so interested in worksheet solutions)
Update #1
For clarity, ShowGame is called by a button on the workshet, then PlayBlackjack is called from the Play button on the Userform (nothing else in the userform code)
Ah, I see. You cannot declare the listBox parameter as ListBox. The latter is reserved for Activex controls, not VBA controls. Change the signature of your ShowCardssub into this:
Private Sub ShowCards(hand As Collection, list As Control) '<~~ or MSForms.ListBox, or simply as Object...
You could use a class for the hand also, and set the class listbox to be the form listbox, for example, the class clsHand
Public colHand As collection
Public lstToUpdate As MSForms.ListBox
Private Sub Class_Initialize()
Set colHand = New collection
End Sub
Friend Function AddCard(card As clsCard)
colHand.Add card, CStr(colHand.Count)
If Not lstToUpdate Is Nothing Then
lstToUpdate.AddItem card.strCardName
End If
End Function
It's use in a form
Private clsPlayerHand As clsHand
Private Sub UserForm_Initialize()
Set clsPlayerHand = New clsHand
Set clsPlayerHand.lstToUpdate = Me.ListBox1
End Sub
Private Sub CommandButton1_Click()
Dim clsC As New clsCard
clsC.strCardName = "one"
clsPlayerHand.AddCard clsC
End Sub
EDIT: Recommendation,
Use the numeric and the suit name for your cards, then you can do the following, say enabling the split button, incidentally, you'd use an array of hands then arrHands(x) ...
Public colHand As collection
Public lstToUpdate As MSForms.ListBox
Public cmdSplitButton As MSForms.CommandButton
Private Sub Class_Initialize()
Set colHand = New collection
End Sub
Friend Function AddCard(card As clsCard)
colHand.Add card, CStr(colHand.Count)
If Not lstToUpdate Is Nothing Then
lstToUpdate.AddItem card.CardName
End If
If Not cmdSplitButton Is Nothing Then
If colHand.Count = 2 Then _
cmdSplitButton.Enabled = colHand(1).NumericPart = colHand(2).NumericPart
End If
End Function
Look at using classes to their full potential and look at events also, to react to certain things.

Create global variables

Bit of a newbie to VBA, sorry
I need to create some variables that are available throughout my workbook, but I can't seem to figure it out. I've read in previous questions where some people have suggested create a separate dim for this?
When the workbook opens I need to set some variables equal to certain cells in a worksheet, these variables need to be called from dims in others worksheets.
So far I have tried to use
Workbook_Open()
In the 'ThisWorkbook' code area but to no avail.
Any tips?
Reagards
EDIT ----
I have tried with the following:
In 'ThisWorkbook'
Public wsDrawings As String
Public Sub Workbook_Open()
wsDrawings = "Hello"
End Sub
And in Sheet1
Private Sub CommandButton1_Click()
MsgBox wsDrawings
End Sub
I do not get an error, but the message box is empty.
Just declare the variables you need wherever they are first used (ThisWorkbook is a fine place to do it) and replace the typical Dim with Public. It will then be accessable in all your code
You can create global variable with code like this
Public testVar As String
you need to place it outside function or sub and then this variable has value till you close workbook. But i think it have scope only in current module.
So you can have something like this
Public testVar As String
Private Sub Workbook_Open()
testVar = "test"
End Sub
Sub testEcho()
MsgBox testVar
End Sub
for shared variable between multiple modules look here
edit:
So now i found, that you can use public variable from ThisWorkbook using this
Sub testSub()
MsgBox ThisWorkbook.wsDrawings
End Sub
you can use module for creating global variable.