I have a VBA script that uses a Private Sub Open_Workbook() to initialize some arrays from one of the excel sheets to be used as lookup tables later. The Private Sub Open_Worbook() is placed in the ThisWorkbook module and it appears to do it's job when the excel workbook is open.
The arrays in the Open_Workbook() sub are Dim as Variants but I have not been able to "pass" them along to Sheet1 for example to be used in the Sub for that sheet. I have tried using an accessor similar to what was suggested at the following link:
Create and assign variables on Workbook_open, pass it to Worksheet_change
Here is my code following that suggestion.
Following code is in the ThisWorkbook module of the excel workbook:
Option Explicit
Private Test_Array() As Variant
Private Sub Workbook_Open()
' Code here to redim preserve Test_array and set elements of the array and other code that
' Added snippits of code to
Dim Test_Array() As Variant
For i = 1 To UBound(TestRange,2)
ReDim Preserve Test_Array(i - 1)
Test_Array(i-1) = TestRange(1,i)
Next i
End Sub
Public Property Get TestArray() As Variant
TestArray = Test_Array()
End Property
I was hoping that I would be able to use Thisworkbook.TestArray in Sheet1 to do some calculations on it but when I have a Msgbox ThisWorkbook.TestArray(0), I am getting a Subscript out of range error. I debugged the code and it appears that the Public Property Get TestArray(), Test_Array() is empty. What am I doing wrong? Can I not use Variant with Public Get Property?
I did confirm that the Test_Array in the Workbook_Open() Sub is indeed populated with the expected elements.
Edited: Added code for populating Test_Array
You have a Dim Test_Array() As Variant on module level and in the Private Sub Workbook_Open(). So you never populate Test_Array on the module level.
The following code is working for me
Workbook class module:
Option Explicit
Private Test_Array() As Variant
Private Sub Workbook_Open()
' Code here to redim preserve Test_array and set elements of the array and other code that
' Added snippits of code to
'Dim Test_Array() As Variant
Dim testrange
Dim i As Long
testrange = Sheet1.Range("A1:A4").Value
For i = 1 To UBound(testrange, 2)
ReDim Preserve Test_Array(i - 1)
Test_Array(i - 1) = testrange(1, i)
Next i
End Sub
Public Property Get TestArray() As Variant
TestArray = Test_Array()
End Property
Normal module
Option Explicit
Sub testit()
Debug.Print ThisWorkbook.TestArray(0)
End Sub
Reading on Lifetime of variables
Related
I have a class module which hosts a worksheet_change sub, and in that sub a Userform has to pop up. I want to use a number of variables from the class module in the Userform's code. Whatever I do, however, I can't get it to work.
I have tried to apply the method from this very lenghty guide, but to no avail. Other threads on SO weren't able to help me.
Private cell As Range
Public WithEvents m_wb As Workbook
Property Get cellr() As Range
Set cellr = cell
End Property
Property Set cellr(cellrange As Range)
Set cell = cellrange
End Property
Public Property Set Workbook(wb As Workbook)
Set m_wb = wb
End Property
Public Property Get Workbook() As Workbook
Set Workbook = m_wb
End Property
Public Sub m_wb_SheetChange(ByVal Sh As Object, ByVal Target As Range) 'simplified, but accurate
Application.EnableEvents = False
For each cell in Target
ReplaceTask.Show
Next cell
Application.EnableEvents = True
End Sub
In the userform_initialize macro, I need to be able to get the name of the m_wb workbook, as well as the cell (preferably as a range variable, otherwise just the address) in the For each cell in Target loop. For each variable in the code below I get
Error '424' object required
which shows the variables are not public..
Private Sub UserForm_Initialize()
Debug.Print cellrange.Address
Debug.Print cell.Address
Debug.Print cellr.Address
Debug.Print m_wb.Name
'....
I am positive it's my inability to understand how these properties work that's holding me back.. If someone could shine some light on what I am doing wrong, please!
To make this work it needs at least a public object variable of type of your class. And this object variable must be set to be a new instance of your class. This object variable then, and only this object variable, is the public accessable instance of your class.
Example:
Let your class be named clsWorkbook and having following code:
Option Explicit
Private m_cell As Range
Private WithEvents m_wb As Workbook
Property Let cell(cellrange As Range)
Set m_cell = cellrange
End Property
Property Get cell() As Range
Set cell = m_cell
End Property
Public Property Let Workbook(wb As Workbook)
Set m_wb = wb
End Property
Public Property Get Workbook() As Workbook
Set Workbook = m_wb
End Property
Private Sub m_wb_SheetChange(ByVal Sh As Object, ByVal Target As Range) 'simplified, but accurate
Application.EnableEvents = False
For Each m_cell In Target
ReplaceTask.Show
Next m_cell
Application.EnableEvents = True
End Sub
Let your UserForm named ReplaceTask having following code:
Option Explicit
Private Sub UserForm_Initialize()
Debug.Print oWB.Workbook.Name
Debug.Print oWB.cell.Address
End Sub
And in a default Module have following code:
Option Explicit
Public oWB As clsWorkbook
Public Sub test()
Set oWB = New clsWorkbook
oWB.Workbook = ThisWorkbook
End Sub
Now, after Sub test() was run, do changig something in a worksheet in the workbook the code is in. This should trigger the Sub m_wb_SheetChange(ByVal Sh As Object, ByVal Target As Range) of your class object oWB then, which shows the user form which also can access oWB.Workbook.Name and oWB.cell.Address.
Because of the discussion about the need of a global instance of clsWorkbook lets have a complete example which one can reconstruct and which shows how the clsWorkbook can be a private class member:
Let your class be named clsWorkbook and having following code:
Option Explicit
Private m_cell As Range
Private WithEvents m_wb As Workbook
Property Let Cell(cellrange As Range)
Set m_cell = cellrange
End Property
Property Get Cell() As Range
Set Cell = m_cell
End Property
Property Let Workbook(wb As Workbook)
Set m_wb = wb
End Property
Property Get Workbook() As Workbook
Set Workbook = m_wb
End Property
Private Sub m_wb_SheetChange(ByVal Sh As Object, ByVal Target As Range) 'simplified, but accurate
Application.EnableEvents = False
Dim frm As ReplaceTask
For Each m_cell In Target
Set frm = New ReplaceTask
frm.Init Me
frm.Show
Next m_cell
Application.EnableEvents = True
End Sub
Let your UserForm named ReplaceTask having following code:
Option Explicit
Private m_ParentClass As clsWorkbook
Friend Sub Init(ByVal p As clsWorkbook)
Set m_ParentClass = p
Me.Caption = p.Workbook.Name & " : " & p.Cell.Address
End Sub
And in default class module ThisWorkbook have following code:
Option Explicit
Private oWB As clsWorkbook
Private Sub Workbook_Open()
Set oWB = New clsWorkbook
oWB.Workbook = Workbooks.Open("P:/Mappe1.xlsx")
End Sub
Now the clsWorkbook gets instantiated while workbook open and is a private member of ThisWorkbook and it's workbook member is the workbook which was opened addditional. There the SheetChange are listened by the clsWorkbook oWB instance.
And because the ReplaceTask user form gets instantiated in clsWorkbook and was given the class instance as parameter, this user form knows the class members too.
The user form and the class with the event handler are two different scopes. You cannot expect to be able to refer to members of a different scope without qualifying that scope. The code in your UserForm_Initialize interprets cellrange and cellr as local variables declared in the user form itself. You don't have such variables declared in the user form, and you are not using Option Explicit, so instead of a compile time error you are getting a runtime error 424 when the code implicitly assumes it's Dim cellrange As Variant which was never initialized and is therefore Empty.
To fix the problem, you need to tell the instance of the user for which instance of the event-handling class it should get the properties from. For that it would be enough to put this in the UserForm:
Private m_ParentClass As ThatClassThatCreatesForms
Friend Sub Init(ByVal p As ThatClassThatCreatesForms)
Set m_ParentClass = p
End Sub
and change the For Each loop in the parent class as:
For each cell in Target
ReplaceTask.Init Me
ReplaceTask.Show
Next cell
You have to have a separate "Init" method because VBA classes cannot have constructors with parameters.
Then the code in ReplaceTask can use m_ParentClass.cell, m_ParentClass.Workbook etc. But you cannot do that from UserForm_Initialize because Init has not been called yet. It is not a problem though, simply move the code from UserForm_Initialize into Init.
To take it one step further, I would advice that you stop are using the implicit form instance. It is a good practice to create the instances manually:
For each cell in Target
Dim f As ReplaceTask
Set f = New ReplaceTask
f.Init Me
f.Show
Next cell
Is it possible to nest procedure definitions in VBA so that the inner Sub has access to the outer Subs variables?
Something like...
Sub Outer()
Dim dataSheet As Worksheet
Dim sheetName As Variant
Dim sel As Range
Dim destCol As Variant
Dim defCol As String
Dim outerLoop As Boolean
Sub Inner()
' Can I do this and get access to the outer variables in here?
' So that I can pass them to the Sub call in here and have that
' Sub call access the outer variables?
dataSheet.Activate
Set sel = dataSheet.ListObjects("ReqMarkerTable").ListColumns("Quantity").DataBodyRange
destCol = GetInputBoxVal("Enter destination column:", "Column?", defCol)
If destCol <> False Then
Call DuplicateValsByQty(sheetName, destCol, sel)
outerLoop = False
End If
End Sub
End Sub
Sub DuplicateValsByQty(sheetName As Variant, destCol As Variant, sel As Range)
' I want this Sub's call within the Inner Sub to have access to the outer variables.
End Sub
I want to avoid using global variables if possible.
Ultimately, I want to have several of these "Inner" Subs that process the same data in different ways.
Or is there some way that data in one Sub can be accessed from another? Something like dot notation in JS?
Perhaps I am looking at this all wrong?
Any help or direction provided would be greatly appreciated.
I have a quite simple macro to hide the row the checkbox is in when clicked. It works but the problem is there are A LOT of rows.
Private Sub CheckBox3_Click()
[3:3].EntireRow.Hidden = CheckBox3.Value
Range("AB3").Value = True
End Sub
I of course can make a separate macro for every single checkbox i have (all 250 of them), but i hope i can avoid a macropage that is 6 pages long.
My question is: is there a way to combine it into 1? the only thing that is different for all of them is the number (in the example its 3).
You should create those CheckBoxes programatically and create Class Module to handle events. Here is example how to achieve it:
ClassModule i.e. MyCheckBox:
Option Explicit
Dim WithEvents m_CheckBox As MSForms.CheckBox
Dim m_Row As Long
Public Sub CreateCheckBox(ByVal sh As Worksheet, ByVal rowNumber As Long)
m_Row = rowNumber
Set m_CheckBox = sh.OLEObjects.Add(ClassType:="Forms.CheckBox.1", Left:=sh.Cells(rowNumber, 1).Left, Top:=sh.Cells(rowNumber, 1).Top, Width:=108, Height:=19.5).Object
End Sub
Private Sub m_CheckBox_Change()
MsgBox "I'm in row " & m_Row & " MyValue is " & m_CheckBox.Value
End Sub
Module using this code:
Option Explicit
Dim chkBoxes As New Collection
Public Sub test()
Dim sh As Worksheet
Dim chk As MyCheckBox
Set sh = ActiveSheet
Set chk = New MyCheckBox
chk.CreateCheckBox sh, 1
chkBoxes.Add chk
Set chk = New MyCheckBox
chk.CreateCheckBox sh, 2
chkBoxes.Add chk
End Sub
Of course method CreateCheckBox has to be adjusted to your needs (my one is creating checkbox in first column). Global collection in module is required otherwise classes are destroyed automatically when sub is finished and events are never fired. If workbook is opened and closed then this code must be upgraded to either:
1) Recreate classes on Workbook event i.e. open
2) Recreate CheckBoxes every time workbook is opened
I have a public Variant variables declared in a UserForm called MainForm
Public increaseArray As Variant
Public countryArray As Variant
Then in sub on button click for the MainForm:
Sub testButton_Click()
Dim country As Variant
Set countryArray = Module1.callSomeFunctionThatReturnsVariant(1)
Set increaseArray = Module1.callSomeFunctionThatReturnsVariant(2)
For Each country In countryArray
Call Module1.createPage(country)
Next country
End Sub
In Module1 I have:
Function callSomeFunctionThatReturnsVariant(ByVal testInt As Integer) As Variant
.... do something when testInt = 1
.... do something when testInt = 2
callSomeFunctionThatReturnsVariant = someVariant
End Function
Public Sub createPage(ByVal country As String)
Dim testInt As Integer
... do something
testInt=insertSection(country, MainForm.increaseArray)
End Sub
Function insertSection(ByVal country As String, arr as Variant) As Integer
Dim arrCountry As Variant
For Each arrCountry In arr
If country = "France" Then
...do something
insertSection = 1
Exit Function
End If
Next arrCountry
insertSection = 2
End Function
I get ByRef argument type mismatch error when passing MainForm.increaseArray to insertSection() function. I've tried using Function insertSection(ByVal country As String, ByVal arr as Variant) As Integer but I get same error.
If I try to define a Variant variable in createPage sub Dim testArray As Variant and get the increaseArray from its getter function Set testArray = MainForm.getterForIncreaseArray I get type mismatch error...
If I pass getter function directly to caller of insertSection function I get ByRef argument type mismatch...
Please help :)
this simple code works fine.
declaring public array in userform not allowed (so using variant as disguise was good idea).
But then, Functions dont want to accept passing it as argument as a legit array, so i used a temporary 'legit' array.
on UserForm1 :
Option Explicit
Public a As Variant 'i would usually declare it like this : Public a() as variant, but public arrays not allowed in userforms (throws error)
'Private a() as variant , would not throw error (inside userform)
Private Sub UserForm_Initialize()
Dim i&
ReDim a(1 To 2) 'absolutely needed, it shows a is actually an array type
a(1) = 1
a(2) = 2
End Sub
Private Sub UserForm_Terminate()
Erase a
End Sub
in a module :
Option Explicit
Sub test()
Load UserForm1
Dim b&
Call get_value(1, UserForm1.a, b)
Unload UserForm1
MsgBox b
End Sub
Sub get_value(ByVal i&, ByRef arr As Variant, ByRef answer As Long) ' function won't let it through, i used a sub with aditionnal variable as Byref.
answer = arr(i)
End Sub
Launch it by calling TEST.
Note : i didn't succeed in passing argument in a Function, so did it in a SUB by adding an argument called Answer, wich is Byref.
Note2 : i looked back at my older code, and it would seem that you can pass a byref Array (disguised as variant) in a function , but maybe because this one is declared not with () or whatever, it don't want to work through a function.
Note 3 : after thurther digging into it, i found a solution using function, and as i thought, the array-declaring was the troublemaker :
'in a module (use the same userform as before)
Sub test()
Load UserForm1
Dim b&
Dim i& 'counter
Dim Temp_Array() As Long 'as variant works too, but i filled it with numbers so as long is ok too
ReDim Temp_Array(LBound(UserForm1.a) To UBound(UserForm1.a))
'Temp_Array = UserForm1.a 'damn, i first thought this would work, in the same way you can fill a listbox in one simple line (wich would be a 3rd solution passing an array from the userform to a module)
For i = LBound(UserForm1.a) To UBound(UserForm1.a)
Temp_Array(i) = UserForm1.a(i)
Next i
b = get_value(1, Temp_Array)
Erase Temp_Array
Unload UserForm1
MsgBox b
End Sub
Function get_value(ByVal i&, ByRef arr As Variant) As Long
get_value = arr(i)
End Function
As per findwindow's comment. You can't make use of things in module 1 from within the form as it's beyond its scope. Instead, try putting all the code from module1 into a new class module. Instantiate an instance of that from within your form and it should work ok.
This is a continuation of posting #12772920 pertaining to error 91. Okay, here's the situation.
(1) If this line is commented out, then (2) produces error 91. If not commented out, the code runs fine.
(3) This should point the public obj shCompanies to the "Companies" sheet, shouldn't it? I'm confused why this line doesn't seem to set the shCompanies object the way that (1) does.
Option Explicit
Public wbCode As Workbook
Public Sub Main()
Dim tables As New CTables
Set wbCode = ThisWorkbook
Call SetExcelObjects
(2)Call tables.Indexing
End Sub
Public Sub SetExcelObjects()
Dim tables As New CTables
(3)Set tables.shCompanies = wbCode.Worksheets("Companies")
End Sub
---Class Module CTables
Option Explicit
Public shCompanies As Worksheet
Public Sub Indexing()
Dim rng As Long
Dim tables As New CTables
(1)Set tables.shCompanies = wbCode.Worksheets("Companies")
rng = tables.shCompanies.UsedRange.Rows.Count
End Sub
You need to declare tables as a public instance of CTables. Then you can act on it from different modules. Re-declaring it as you've been doing "wipes the slate clean" and causes the errors you are seeing. You certainly don't want to declare an instance of tables inside of CTables.
Note that best practice is to separate the instantiating tables from its declaration, therefor the line Set tables = New CTables in Main.
The below compiles and runs for me. I think it does what you expect:
Public wbCode As Workbook
Public tables As New CTables
Public Sub Main()
Set tables = New CTables
Set wbCode = ThisWorkbook
Call SetExcelObjects
Call tables.Indexing
End Sub
Public Sub SetExcelObjects()
Set tables.shCompanies = wbCode.Worksheets("Companies")
End Sub
'Class CTables
Public shCompanies As Worksheet
Public Sub Indexing()
Dim rng As Long
rng = shCompanies.UsedRange.Rows.Count
End Sub