Having a public variable available in userform / sheet objects - excel

I've spent the last hour reading pages about variable scope in various flavours of excel vba, and could not find a definite documentation reference addressing my scope problem... even though i'm convinced it is such a classic. Oh well, here goes.
I've got a workbook that contains just one sheet and one userform. I have a list of students sitting in column 1 on my sheet. I would like to :
load this list up into some global Collection variable named students_list (i do this using a Workbook-Open() procedure in the ThisWorkbook object)
use the contents of students_list to initialize a listbox in my userform
remove elements from students_list when a button on my userform is clicked on
All i need is a variable that is seen from within my userform's procedures, as well as from inside the ThisWorkbook object.
I tried declaring it as public, global, in the sheet's code, in the userform, in ThisWorkbook, in a separate module dedicated to globals... I just can't seem to find the right way to have the students_list variable visible from everywhere.
What am I missing ? My apologies for this question that should be so basic and yet beats me :-/

Place the declaration of your Public variables inside a Module (use Insert / Module from the menu to create one, if you don't already have one). The scope will then extend to your whole project.
So in a Module (e.g. Module1) have:
Public foo As Integer
And in the worksheet (e.g. Sheet1) code have:
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
foo = 4
MsgBox "foo set to 4"
End Sub
Private Sub Worksheet_BeforeRightClick(ByVal Target As Range, Cancel As Boolean)
MsgBox "foo = " & foo
End Sub
If you were to place the declaration in the code for ThisWorkbook you would need to reference it as Thisworkbook.foo because, although it is accessible from any part of the code, it is a variable specific to that ThisWorkbook object.
So, in the code for ThisWorkbook have:
Public foo As Integer
And in the worksheet (e.g. Sheet1) code have:
Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
ThisWorkbook.foo = 4
MsgBox "foo set to 4"
End Sub
Private Sub Worksheet_BeforeRightClick(ByVal Target As Range, Cancel As Boolean)
MsgBox "foo = " & ThisWorkbook.foo
End Sub

Related

how do I change the code in a sheet by applying a module

I made a module that I want to be a plugin. My issue is I want it to rerun every time I open the sheet and only the sheet I activated it on.
I found a solution on how to do it by using the
Private Sub Worksheet_Activate()
Call Func
End sub
inside the sheet, I applied the macro to. How can I make it apply this code snipped to the currently active sheet when automatically when I activate the macro.
Basically, when I use my plugin while I am on sheet x I want it to apply
Private Sub Worksheet_Activate()
Call Func
End sub
this function to that specific sheet and that specific sheet only
Just to clarify better.
I want to sit on the sheet that has NO VBA code associated,
activate my add-in
and have a predefined code-block run in the context of the activated sheet.
You need to add the following codes to your Add-In, it is important to add them to the right module (as indicated by the comments in the code).
' ThisWorkbook
Option Explicit
Private Sub Workbook_Open()
InitializeMyEventHandler ThisWorkbook.Application
End Sub
' modMyEventHandler
Option Explicit
Public g_mehHandler As clsMyEventHandler
Public Sub InitializeMyEventHandler(eapApplication As Application)
Set g_mehHandler = New clsMyEventHandler
Set g_mehHandler.eapApplication = eapApplication
End Sub
' clsMyEventHandler
Option Explicit
Public WithEvents eapApplication As Application
Private Sub eapApplication_SheetActivate(ByVal Sh As Object)
If TypeName(Sh) = "Worksheet" Then
Dim ewsSheet As Worksheet: Set ewsSheet = Sh
Debug.Print "Worksheet activated. Workbook: " & ewsSheet.Parent.Name & ", Worksheet: " & ewsSheet.Name
End If
End Sub
The first part in the Add-In's ThisWorkbook module makes sure that our event handler class will be initialized each time the Add-In is loaded (when a new Excel Application is opened).
The second part is a normal public module, which is capable of holding global public objects, in our case an object of class clsMyEventHandler. At initialization g_mehHandler will be set to a new instance of clsMyEventHandler, and its member, eapApplication will be assigned to the Application object received from the Add-In's Workbook_Open function.
The third part must be added as a class module. It has a variable that is declared with WithEvents, this means that any time an event (e.g. activation of a new sheet) happens, the appropriate functions of this class module will be called. If a SheetActivate event is fired in eapApplication, then eapApplication_SheetActivate function of this class will be called. The function is selected based on its name (object name + underscore + event name). After you declared eapApplication (Public WithEvents eapApplication As Application), you will be able to select eapApplication from the ComboBox above the VBA code, which usually contains the word (General). If you selected eapApplication there, you will be able to select events to which you want to react from the ComboBox next to it.

Execute a macro automatically when the linked cell of a combo box changes

I have a series of 2 combo boxes. I have a macro called Generate which will change the options in the second combo box based on the number the first combo box returns. However this requires the user to press a button to execute this macro. I would like this macro to execute automatically when the number in the first combo box's linked cell changes.
This is the code I have previously tried, however the change in the link cell which is B2 doesn't seem to trigger the event.
Private Sub Worksheet_Change(ByVal Target As Range)
If Not Intersect(Target, Range("B2")) Is Nothing Then Generate
End Sub
As mentioned before, a Worksheet_Change event will only be triggered by physically changing a cell, and thus your linked cell won't have any effect.
If it's a cell in another wb that needs to trigger your Generate Sub, then I'd advise using a Worksheet_Change event for said wb.
In my own project, I have a sub in a regular module:
Dim AddNew As Workbook
Set AddNew = Workbooks("") 'change this
Set oWb.Workbook = AddNew
Then, in a class module:
Public WithEvents m_wb As Workbook
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)
'In here, you could trigger the Generate Sub if a specific cell changes
End Sub
The simplest solution was to give the first combo box an input range and also assign it the Generate macro

automatically declare object class variable when creating new sheet in EXCEL

How do I declare a - private - class variable, when a new sheet is created using the tab. It has to be done automatically. I presume it is a good idea to declare it by using the
Private Sub Workbook_NewSheet(ByVal Sh As Object) -event from the Wookbook object
Sub Workbook_NewSheet(ByVal Sh As Object)
Dim sh.privateVariableOfSheet As Integer
Declare New sh.privateVariableOfSheet2 As Integer
End Sub
Both above 'declarations' fails of course!
And for the completeness, how to refer to this variable from an ordinary module.
Another method is to use the CustomProperties collection of the Worksheet. For example:
Option Explicit
Private Sub Workbook_NewSheet(ByVal Sh As Object)
Sh.CustomProperties.Add Name:=Sh.Name, Value:=99
End Sub
You can then use it later in a sub-routine:
Option Explicit
Sub Test()
Dim var As Variant
var = ThisWorkbook.Worksheets("Sheet13").CustomProperties(1)
MsgBox var
End Sub
Let take some class - clsFoo - which has a single property of type Range with a getter and setter:
Private m_rngSomewhere As Range
Public Property Get SomeRange() As Range
Set SomeRange = m_rngSomewhere
End Property
Public Property Set SomeRange(rng As Range)
Set m_rngSomewhere = rng
End Property
Now, in the Workbook code module you have:
a Public variable which we will set as a Dictionary
a Sub to instantiate the Dictionary - could be called from Workbook_Open or something
an event handler for Workbook_NewSheet
The event handler creates a new instance of clsFoo and sets its property as a Range from the new Worksheet, and then adds that to the dictionary (and checks if it was already there for some new reason).
Code in Workbook module:
Option Explicit
Public SheetFooDic As Object
Public Sub InitialiseSheetFooDic()
Set SheetFooDic = CreateObject("Scripting.Dictionary")
End Sub
Private Sub Workbook_NewSheet(ByVal Sh As Object)
Dim rng As Range
Dim cls As clsFoo
If Not SheetFooDic.Exists(Sh) Then
Set rng = Sh.Range("A1")
Set cls = New clsFoo
Set cls.SomeRange = rng
SheetFooDic.Add Sh, cls
End If
End Sub
This leaves you needing to simply use some Worksheet object as a key into the Dictionary in order to retrieve the Range you stored when the Worksheet was created. You can refer to the public Dictionary like this:
ThisWorkbook.SheetFooDic(ThisWorkbook.Worksheets("Sheet2")).SomeRange.Address
And get:
$A$1
You can store the address of the last selected cell as a string. So, asking for an object variable might be misleading. Either way, the easiest way is to declare a public variable (for example PrevCell As String, or As Range if you prefer or need the value) in each of your worksheets' code and set that variable in each sheet's Selection_Change event procedure.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
PrevCell = Target.Cells(1) ' or .Address
End Sub
Since you may find it useful to avoid recording selections of multiple cells my above procedure just records the address of the first cell which avoid errors that often crop up when a copy/paste action was performed on a sheet.
Now, when you insert a new sheet, don't use the Add method. Instead copy an existing sheet and clean it up the way you want. In this way the new sheet will already have the variable and the code that sets it.
I can't imagine your having need of the previous cell in any circumstance other than when the sheet is activated, but if my imagination is insufficient in this case, you might declare a global array with an element for each sheet, using the sheets' CodeName property for identification. This array would be set by the Selection_Change event procedure as demonstrated above, but when a sheet which isn't known to the array tries to register its latest selection it must be a new sheet and the array is extended to include it. The code to do so is inherited from other sheets by the same method described above.

"Include" in Excel VBA?

I have a user-form which is made up of many subs, this is assigned as a macro to a button on the worksheet. When the user is finished with this user-form they can press a button on it which causes its visibility to become false and when entered again everything appears how it was left resulting in a save like feature.
I now need to apply this to multiple buttons on the worksheet and each user form needs to have the exact same code and same buttons but be a separate form as each individual button requires it's own save like feature. The way I was planning on doing this was to copy the existing user form and paste it many times with different names however, if a modification is required it will take a long time to carry out therefore, is there a method such as "include" which could use a base module from which all the code is accessed so that if I ever need to change anything I just do it on that one module and everything else updates via the include?
EDIT:
I now have a public function called costing() and am getting an error when I used:
Private Sub material_Change()
Call costing
End Sub
You can have multiple instances of the same form. You can use this to retain multiple sets of form values
Try this:
Create your form, as usual. Let's call it MyForm
Create several buttons on your sheet. My example uses ActiveX buttons, but Form Control buttons can be used too. Let's call them CommandButton1 and CommandButton2
In your form module, include a Terminate Sub, which includes this code
Private Sub UserForm_Terminate()
' any other code you may need...
Unload Me
End Sub
The Form buton to save/Hide the form needs to be
Private Sub btnSaveAndHide_Click()
Me.Hide
End Sub
The Sheet Button code is as follows
The code is identical for each button (and calls a common Sub), and each button has its own Static form variable.)
The Error handler is needed to deal with the case a form is not properly closed. In this case the instance no longer exists, but the local Static variable is also not Nothing
Example shows form shown as Modeless, you can change this to Modal if you want.
Private Sub CommandButton1_Click()
Static frm As MyForm
ShowMyForm frm
End Sub
Private Sub CommandButton2_Click()
Static frm As MyForm
ShowMyForm frm
End Sub
Private Sub ShowMyForm(frm As MyForm)
If frm Is Nothing Then Set frm = New MyForm
On Error GoTo EH
frm.Show vbModeless
Exit Sub
EH:
If Err.Number = -2147418105 Then
On Error GoTo 0
Set frm = Nothing
Set frm = New MyForm
frm.Show
End If
On Error GoTo 0
End Sub
End result: multiple copies of the same form, each with their own values
In responce to comment How would I access the variables inside of each user form externally
In the example above the Form instances are only accessable in the Command Button Click Handler routines, or within the Form module itself. If you can write your code in the form module, then no change is needed.
To make the Form instances available elsewhere, consider moving their declaration to Module Scope of a standard Module. You could declare them as, eg individual variables, an array (either static or dynamic), a Collection, a Dictionary. Which structure is best will depend on how you want to manage and access your form instances.
For example, a Static Array: Code in a standard Module
Option Explicit
Global MyForms(1 To 2) As MyForm
Update the CommandButton code to
Private Sub CommandButton1_Click()
ShowMyForm Module1.MyForms(1)
End Sub
Private Sub CommandButton2_Click()
ShowMyForm Module1.MyForms(2)
End Sub
Private Sub ShowMyForm(frm As MyForm) no change, same as before
The code works the same as before, but you can now access the Global variable in a standard Module
Sub Demo()
Dim i As Long
For i = LBound(MyForms) To UBound(MyForms)
If Not MyForms(i) Is Nothing Then
MsgBox "Form " & i & " Value = " & MyForms(i).TextBox1.Value
End If
Next
End Sub
You don't need an "Include" (none exists in VBA); all you need to do is create a module and make the common methods public.
For example, if you create a module and have a function like this:
Public Function Add(first As Integer, second As Integer) As Integer
Add = first + second
End Function
Then you can access it like this from another module/form/class module:
Sub test()
MsgBox Add(3, 6)
End Sub

Get Selected value of a Combobox

I have a thousands of cells in an Excel worksheet which are ComboBoxes. The user will select one at random and populate it.
How do I get the selected ComboBox value? Is there a way to trigger a function (i.e. an event handler) when the ComboxBoxes has been selected?
You can use the below change event to which will trigger when the combobox value will change.
Private Sub ComboBox1_Change()
'your code here
End Sub
Also you can get the selected value using below
ComboBox1.Value
If you're dealing with Data Validation lists, you can use the Worksheet_Change event. Right click on the sheet with the data validation and choose View Code. Then type in this:
Private Sub Worksheet_Change(ByVal Target As Range)
MsgBox Target.Value
End Sub
If you're dealing with ActiveX comboboxes, it's a little more complicated. You need to create a custom class module to hook up the events. First, create a class module named CComboEvent and put this code in it.
Public WithEvents Cbx As MSForms.ComboBox
Private Sub Cbx_Change()
MsgBox Cbx.Value
End Sub
Next, create another class module named CComboEvents. This will hold all of our CComboEvent instances and keep them in scope. Put this code in CComboEvents.
Private mcolComboEvents As Collection
Private Sub Class_Initialize()
Set mcolComboEvents = New Collection
End Sub
Private Sub Class_Terminate()
Set mcolComboEvents = Nothing
End Sub
Public Sub Add(clsComboEvent As CComboEvent)
mcolComboEvents.Add clsComboEvent, clsComboEvent.Cbx.Name
End Sub
Finally, create a standard module (not a class module). You'll need code to put all of your comboboxes into the class modules. You might put this in an Auto_Open procedure so it happens whenever the workbook is opened, but that's up to you.
You'll need a Public variable to hold an instance of CComboEvents. Making it Public will kepp it, and all of its children, in scope. You need them in scope so that the events are triggered. In the procedure, loop through all of the comboboxes, creating a new CComboEvent instance for each one, and adding that to CComboEvents.
Public gclsComboEvents As CComboEvents
Public Sub AddCombox()
Dim oleo As OLEObject
Dim clsComboEvent As CComboEvent
Set gclsComboEvents = New CComboEvents
For Each oleo In Sheet1.OLEObjects
If TypeName(oleo.Object) = "ComboBox" Then
Set clsComboEvent = New CComboEvent
Set clsComboEvent.Cbx = oleo.Object
gclsComboEvents.Add clsComboEvent
End If
Next oleo
End Sub
Now, whenever a combobox is changed, the event will fire and, in this example, a message box will show.
You can see an example at https://www.dropbox.com/s/sfj4kyzolfy03qe/ComboboxEvents.xlsm
A simpler way to get the selected value from a ComboBox control is:
Private Sub myComboBox_Change()
msgbox "You selected: " + myComboBox.SelText
End Sub
Maybe you'll be able to set the event handlers programmatically, using something like (pseudocode)
sub myhandler(eventsource)
process(eventsource.value)
end sub
for each cell
cell.setEventHandler(myHandler)
But i dont know the syntax for achieving this in VB/VBA, or if is even possible.

Resources