vba excel userform won't show - excel

Morning all!
I'm having trouble getting a userfrom to show.
Below is the code I want to use. I declare the Userfrom (oUserform) as a public variable, initialize it and make the changes, then show it.
That's the plan anyway, however I get an error with oUserform.show, saying that the object doesn't support the property or method.
Public oUserform As UserForm
Public iNumberOfRecords As Integer
Public iEndRow As Integer
Sub subIntialize()
Set oUserform = frmbusinessimpact
iEndRow = Sheet1.Cells(1, 1).CurrentRegion.Rows.Count
iNumberOfRecords = iEndRow - 1
Call subPopulateRecordClass(iEndRow)
With oUserform
.TotalRecords = iNumberOfRecords
.CurrentRecord = 1
Call subUpdateUserform
End With
oUserform.Show
End Sub
Any ideas?

You'll want to declare oUserform as an instance of frmbusinessimpact and use the New keyword.
Public oUserform As frmbusinessimpact
Public iNumberOfRecords As Integer
Public iEndRow As Integer
Sub subIntialize()
Set oUserform = New frmbusinessimpact
iEndRow = Sheet1.Cells(1, 1).CurrentRegion.Rows.Count
iNumberOfRecords = iEndRow - 1
Call subPopulateRecordClass(iEndRow)
With oUserform
.TotalRecords = iNumberOfRecords
.CurrentRecord = 1
Call subUpdateUserform
End With
oUserform.Show
End Sub
The book Professional Excel Development has a good chapter on how, and why, to code userforms this way. I've got a more specific example at http://yoursumbuddy.com/a-flexible-vba-chooser-form/.

Related

Custom class methods in Excel

I'm trying to create my own custom class module to handle custom class object.
Let's say i want to create a method witch would double the result of object property...
Class1:
Public Property Get MyObject() As Workbooks
Set MyObject = Application.Workbooks
End Property
When i create code like this:
Module1:
Function test()
Dim clstest As New Class1
Debug.Print "Result is: " & clstest.MyObject.Count
End Function
will give me
Result is: 1
since there is one worksheet in my object.
What i`m trying to achive is to make a custom function like this:
test.MyObject.DoubleResult '<- to make the result equals 2
I can`t get the idea how to pass the object to the new class function.
Class 2
Public Function DoubleResult() As Integer
DoubleResult = (The_Object_I_Created_earlier.Count) * 2
End Function
How i cant refer to the object "The_Object_I_Created_earlier"?
As far as I know you have to keep the count in an External variable.
You declare the public Long variable in a Module
Public ClassCount As Long
And on your class initialize and terminate you add and remove from it.
Private Sub Class_Initialize()
ClassCount = ClassCount + 1
End Sub
Private Sub Class_Terminate()
ClassCount = ClassCount - 1
End Sub
The design intent is not clear at all. But I can offer a simple primer with custom classes in VBA, and how one can contain the other.
Here is the code
MyClass1
Private m_value As Long
Private Sub Class_Initialize()
m_count = 0
End Sub
Public Sub InitializeWithCount(ByVal n As Long)
m_count = n
End Sub
Public Property Get Count() As Long
Count = m_count
End Property
MyClass2
Private m_obj As MyClass1
Private Sub Class_Initialize()
Set m_obj = New MyClass1
End Sub
Public Sub InitializeWithObject(ByVal x As MyClass1)
Set m_obj = x
End Sub
Public Sub InitializeWithValue(ByVal n As Long)
Set m_obj = New MyClass1
m_obj.InitializeWithCount n
End Sub
Public Property Get MyResult() As MyClass1
Set MyResult = m_obj
End Property
Module
for testing the above
Public Sub SO_Test()
Dim t1 As MyClass1
Dim t2 As MyClass2
Set t1 = New MyClass1 ' New Class1
t1.InitializeWithCount 100 ' Set value once to Long
Set t2 = New MyClass2 ' New Class2
t2.InitializeWithObject t1 ' Set value once to Class1
Debug.Print t2.MyResult.Count
' 100
t2.InitializeWithValue 200
Debug.Print t2.MyResult.Count
' 200
End Sub

How to assign a UserForm to a Class (Types)?

I'm trying to create a code that will be executed against all userforms in my workbook. So I wanted to create a Class. However, I'm stuck at this point.
The class name is clsCmdUsrFormsE
Variable declaration within the class:
Public WithEvents EndUserforms As UserForm
Objects assignation code:
Dim UserFormsAll() As New clsCmdUsrFormsE
Sub Cmd_UsrF(Optional var1 As Boolean = False)
Dim iCmdUsrFrm As Integer, UsrForm As Object
iCmdUsrFrm = 0
For Each UsrForm In WBK0001.VBProject.VBComponents
If UsrForm.Type = 3 Then
iCmdUsrFrm = iCmdUsrFrm + 1
ReDim Preserve UserFormsAll(1 To iCmdUsrFrm)
Set UserFormsAll(iCmdUsrFrm).EndUserforms = UsrForm
End If
Next UsrForm
End Sub
& thew intent is to call Cmd_UsrF using a simple call
Sub Test()
Call Cmd_UsrF
End Sub
The problem I'm having is that it seems the Userform as Object is not compatible with the Class.
I've tried as well with
Public WithEvents EndUserforms As msforms.UserForm
But all I get is
Run-time error '13': Type mismatch
Any ideas?
You are tying to store VBComponent in MsForm and hence a type mismatch
Is this what you are trying?
Dim UserFormsAll() As New clsCmdUsrFormsE
Sub Cmd_UsrF(Optional var1 As Boolean = False)
Dim iCmdUsrFrm As Integer, UsrForm As Object
Dim frmname As String
iCmdUsrFrm = 0
For Each UsrForm In ThisWorkbook.VBProject.VBComponents
If UsrForm.Type = 3 Then
iCmdUsrFrm = iCmdUsrFrm + 1
ReDim Preserve UserFormsAll(1 To iCmdUsrFrm)
'~~> Get the Name of the form
frmname = UsrForm.Name
'~~> Load the form
Set UserFormsAll(iCmdUsrFrm).EndUserforms = UserForms.Add(frmname)
End If
Next UsrForm
End Sub
In Class Module
Public WithEvents EndUserforms As MSForms.UserForm
Note:
You can also bypass the variable frmname and directly use
Set UserFormsAll(iCmdUsrFrm).EndUserforms = UserForms.Add(UsrForm.Name)

Error handling of multiple class attributes

I have a class that holds data in separate attributes (I chose this design instead of an array or scripting.dictionary because I will be using these data for the construction of a decision tree and I want to make use of IntelliType while I construct it.).
The data are loaded from an excel spreadsheet in the form of floats, so I am storing them in a long datatype, but from time to time it happens so that a value is missing and is replaced by an "NA" string.
I would like to create an error-handling routine, that, if a non-numeric value is encountered, would replace the content with a value -1.
I know I could do this with checking with IsNumeric() or error-handling, but I don't know how to make it work for every one of the many attributes the class holds and I don't really like the solution to write a specific error-handling code for every one of them (storing each of them in a separate attribute is not elegant as well, but I find this a price I am willing to pay for the advantage of shorter syntax in the decision tree).
Is there a way to pass a value to a variable, that just encountered a type-mismatch error, by the error-handling code independent of the variable name?
A simple example with several of the attributes:
Option Explicit
Dim B_LYM As Long
Dim B_mem As Long
Dim B_CXCR3 As Long
Dim B_CXCR4_MFI As Long
Public Sub SetData(data as Range)
On Error Goto err_handler
B_LYM = data.Cells(1, 1)
B_mem = data.Cells(1, 2)
B_CXCR3 = data.Cells(1, 3)
B_CXCR4_MFI = data.Cells(1, 4)
err_handler:
'I need something like this:
'if valuebeingstored = "NA" then targetvariable = -1
End Sub
It could be that some other approach could be better and I am gladly open to options, I only want to emphasize that I would really like make use of IntelliType when constructing the decision tree. I was considering using scripting.dictionary, but the syntax will bloat the code very quickly.
You said you have a Class and therefore could include the function to check the input and return -1 inside the class and use the Get and Let properties to call the function.
Here is an example class (named clsDataStuff) demonstrating this:
Option Explicit
Private c_B_LYM As Double
Private c_B_mem As Double
Private c_B_CXCR3 As Double
Private c_B_CXCR4_MFI As Double
Public Property Let B_LYM(varValue As Variant)
c_B_LYM = ParseDouble(varValue)
End Property
Public Property Get B_LYM()
B_LYM = c_B_LYM
End Property
Public Property Let B_mem(varValue As Variant)
c_B_mem = ParseDouble(varValue)
End Property
Public Property Get B_mem()
B_mem = c_B_mem
End Property
Public Property Let B_CXCR3(varValue As Variant)
c_B_CXCR3 = ParseDouble(varValue)
End Property
Public Property Get B_CXCR3()
B_CXCR3 = c_B_CXCR3
End Property
Public Property Let B_CXCR4_MFI(varValue As Variant)
c_B_CXCR4_MFI = ParseDouble(varValue)
End Property
Public Property Get B_CXCR4_MFI()
B_CXCR4_MFI = c_B_CXCR4_MFI
End Property
Private Function ParseDouble(varValue As Variant) As Double
If IsNumeric(varValue) Then
ParseDouble = CDbl(varValue)
Else
ParseDouble = -1
End If
End Function
Noting that:
the Let property expects a Variant because you say your input could be a number, or a string
the Get property returns Double as you said your inputs are floats so Double is better than Long
the ParseDouble function simply checks for a numeric input and returns -1 otherwise
Then, in your module code:
Option Explicit
Dim B_LYM As Long
Dim B_mem As Long
Dim B_CXCR3 As Long
Dim B_CXCR4_MFI As Long
Public Sub Test()
Dim objDataStuff As clsDataStuff
Set objDataStuff = New clsDataStuff
objDataStuff.B_LYM = 1 'data.Cells(1, 1)
objDataStuff.B_mem = 2 'data.Cells(1, 2)
objDataStuff.B_CXCR3 = "a" 'data.Cells(1, 3)
objDataStuff.B_CXCR4_MFI = True 'data.Cells(1, 4)
Debug.Print objDataStuff.B_LYM
Debug.Print objDataStuff.B_mem
Debug.Print objDataStuff.B_CXCR3
Debug.Print objDataStuff.B_CXCR4_MFI
End Sub
Returns an output of:
1
2
-1
-1
Intellisense is available and you get validation of the input:
Edit - regarding the comment on dynamically setting a target variable.
Your class can be:
Option Explicit
Public B_LYM As Double
Public B_mem As Double
Public B_CXCR3 As Double
Public B_CXCR4_MFI As Double
Public Sub SetVar(ByVal strVarName As String, ByVal varValue As Variant)
Dim dblValue As Double
Dim strToEval As String
If Not MemberExists(strVarName) Then Exit Sub
dblValue = ParseDouble(varValue) ' do the parse
CallByName Me, strVarName, VbLet, dblValue ' dynamically assign the value
End Sub
Private Function ParseDouble(varValue As Variant) As Double
If IsNumeric(varValue) Then
ParseDouble = CDbl(varValue)
Else
ParseDouble = -1
End If
End Function
Private Function MemberExists(strVarName) As Boolean
Dim blnTest As Boolean
Dim varValue As Variant
On Error GoTo ErrHandler
varValue = CallByName(Me, strVarName, VbGet)
blnTest = True
GoTo ExitFunction
ErrHandler:
blnTest = False
ExitFunction:
MemberExists = blnTest
End Function
Where:
All the variables are Public and you still get Intellisense but avoid all the repetitive Let and Get code
A single SetVar method uses CallByName to dynamically set a target variable
Two problems:
You need the clunky MemberExists function to prevent SetVar trying to assign a value to a member that does not exist - otherwise this generates an error (438) but perhaps this is something you need in your logic ?
You can still assign values to the target variable with e.g. objDataStuff.B_CXR3 = "foo" which alsos produces an error for anything other than a number.
The example code shows the problem below. But sticking with SetVar method will produce the same output as above.
Option Explicit
Dim B_LYM As Long
Dim B_mem As Long
Dim B_CXCR3 As Long
Dim B_CXCR4_MFI As Long
Public Sub Test()
Dim objDataStuff As clsDataStuff
Set objDataStuff = New clsDataStuff
objDataStuff.SetVar "B_LYM", 1
objDataStuff.SetVar "B_mem", 2
objDataStuff.SetVar "B_CXCR3", -1
objDataStuff.SetVar "B_CXCR4_MFI", True
objDataStuff.SetVar "foobar", 999
' working around SetVar here generates an error
objDataStuff.B_CXCR3 = "bad"
Debug.Print objDataStuff.B_LYM
Debug.Print objDataStuff.B_mem
Debug.Print objDataStuff.B_CXCR3
Debug.Print objDataStuff.B_CXCR4_MFI
End Sub

How do you use a public variable in a Class Module?

I'm using a Class Module to make a collection of save buttons all do the same thing. But when I try to get them to run a sub that requires a variable I can't get the variable passed to them.
Edited using #Teasel's advice about properties. The problem seems to be the Let Property is not allowing me to set the variable from Module1.
Class1
Public WithEvents SaveBtn As MSForms.CommandButton
Dim currentrow As Long
Private Sub SaveBtn_Click()
SendMessage
`Even if I just have it Msgbox currentrow it returns 0
End Sub
Property Let GetRow(myrow As Long)
currentrow = myrow
End Property
Property Get GetRow() As Long
GetRow = currentrow
End Property
Module1
`Trying to send the value into the Class using Let
Private Sub SendRow_Click()
Module1.GetRow = 22
End Sub
`Trying to Get the value back from the Class
Public Sub SendMessage()
Dim therow As Long
therow = Module1.GetRow
`I get the "Method or Data Member not found" error in the line above
MsgBox therow
End Sub
UserForm1
`This part works fine
Dim colSaveButtons As New Collection
Private Sub UserForm_Initialize()
Dim i As Long
Dim ctl As MSForms.Control
Dim obEvents As Class1
For Each ctl In Me.Controls
If TypeOf ctl Is MSForms.CommandButton Then
For i = 0 To 5
If ctl.Name = "btnSavePage" & i Then
Set obEvents = New Class1
Set obEvents.SaveBtn = ctl
colSaveButtons.Add obEvents
End If
Next
End If
Next ctl
End Sub
Add a "CurrentRow" field to your class module:
Public WithEvents SaveBtn As MSForms.CommandButton
Public CurrentRow As Long '<< add this
Private Sub SaveBtn_Click()
SendMessage CurrentRow
End Sub
In your loop:
...
If ctl.Name = "btnSavePage" & i Then
Set obEvents = New Class1
obEvents.CurrentRow = 10 'or whatever...
Set obEvents.SaveBtn = ctl
colSaveButtons.Add obEvents
End If
...
And your SendMessage method:
Public Sub SendMessage(CurrentRow As Long)
MsgBox "This works"
End Sub
You can use two differents ways to achieve that.
1. Public Property
To simple access your variable's value you need a Get property and to set its value you need a Let property.
In your Module:
'Your module private variable
Dim nameOfYourModuleVariable As String
...
'Set property to assign a value to your variable
Public Property Let nameOfYourProperty(value As String)
nameOfYourModuleVariable = value
End Property
'Get property to return the value of your variable
Public Property Get nameOfYourProperty() As String
nameOfYourProperty = nameOfYourModuleVariable
End Property
You can then use it like this:
'Set the value
MyModule.nameOfYourProperty = "foo"
'Get the value
MyModule.nameOfYourProperty
I highly recommend to use properties to do such things however you can also simply set your variable as public as shown in point 2.
2. Public Variable
Define your variable to be public so you can access it from everywhere.
In your Module:
Public nameOfYourVariable As String
Get or set the value from another module:
'Set the value
MyModule.nameOfYourVariable = "foo"
'Get the value
MyModule.nameOfYourVariable

Variables and constants across multiple vba macros in same module & workbook

After a lot of small sub() writing in the same Excel workbook, I realised that I often used same part code, variables and constants. Thus I decided to write funcions() for the code, and declare variables & constant/static as Public outside functions and sub. I am very new to vba declarations and this is not so easy. Let me give you one summary of what i want to achieve. I have writen all funcions and sub in one module under the module directory of the workbook.
Option Explicit
Public ToDate As String ' variable I use in many sub and functions
Public MyPath As String ' variable I use in many sub and functions
Public NameOfWorker As Variant ' constant I use in many sub and functions
Public Salary As Double ' constant I use in many sub and functions
NameOfWorker = Cells(14, 19) ' !!! PB : 14 is highlighed with error : incorrect instruction outside a procedure
Salary = Cells(20, 7).Value '!!! same as above
How and where shall I declare such constants/statics ? Shall I write a "special" procedure to declare all these variables and constants ? I tried many way to declare them with no success.
Public Static NameOfWorker = Cells(14, 19) As String ' not working
''''''
Public Static nameOfWorker As String
NameOfWorker = Cells(14, 19) ' not working
''' etc etc
Thank you for help.
EDIT : after more reading, I found one solution this way:
Public Const MY_PATH = "Y:\path\to\directory\"
Public Const WORKERNAME = "14, 19"
Not so bad :-)
You could create a new module called something like DataHelper which looks like this:
Private NameOfWorker As String
Private AgeOfWorker As Long
Private SetupComplete As Boolean
Public Function GetNameOfWorker()
If NameOfWorker = "" Then
NameOfWorker = Sheets("SomeSheet").Cells(14, 19)
End If
GetNameOfWorker = NameOfWorker
End Function
Public Function GetAgeOfWorker()
...
End Function
Now in any other code you can retreive the value:
Sub SomeMethod()
Cells(1, 1).Value = DataHelper.GetNameOfWorker()
End Sub
...and you never have to worry if it's been set.
Good question!
I would Dim the globals above all the subs in the module, but initialize the globals at a convenient spot within some sub. For example:
Public NameOfWorker As String
Public AgeOfWorker As Long
Public SetupComplete As Boolean
Sub MAIN()
If SetupComplete Then
Else
NameOfWorker = Sheets("Sheet1").Range("B9")
AgeOfWorker = Sheets("Sheet1").Range("B10")
SetupComplete = True
MsgBox "Global variable set up complete!"
End If
End Sub

Resources