Can I call variables from outside the Public Sub which was defined in in Visual Basic?
This is rather basic-level stuff, but if you don't know what to search for, it can be hard to know how to find the information. This should get you started.
You don't "call" variables, you "call" procedures. A variable that's declared with the Dim keyword inside a procedure scope, is local to that scope it's declared in, so no.
This is best observed with Option Explicit specified.
Option Explicit
Public Sub Test1()
Dim foo As Long
End Sub
Public Sub Test2()
foo = 42 ' illegal: variable is not declared / not accessible in this scope
End Sub
The concept to understand here, is scoping. Use Dim to declare local variables. As the name says, such variables only exist in the scope they're declared in.
Next you have module scope. You can use the Dim keyword outside of a procedure scope, at the top of the module for this, but for consistency it's probably a better idea to use the Private keyword.
Option Explicit
Private foo As Long
Public Sub Test1()
foo = 42
End Sub
Public Sub Test2()
MsgBox foo
End Sub
This code will compile, and if Test1 is invoked before Test2, the Test2 call will pop 42 in a message box.
Then you have public scope, aka "global". You can use the obsolete Global keyword for those, but for consistency it's better to use the Public keyword.
Option Explicit
Public foo As Long
Public Sub Test1()
foo = 42
End Sub
Option Explicit
Public Sub Test2()
MsgBox foo
End Sub
The above will do exactly the same as the previous snippet, except now we have the two procedures in separate modules (standard modules - it's important). And the code will compile and run.
Rule of thumb, you don't need to declare global variables.
Variables should always be scoped as tightly as possible, and passed around as parameters. Parameters can be passed by value (ByVal) or, by reference (ByRef). If unspecified, ByRef is the [unfortunate] default.
Option Explicit
Public Sub Test()
Dim foo As Long
Assign foo
MsgBox foo
End Sub
Private Sub Assign(ByVal bar As Long)
bar = 42
End Sub
Running Test will pop a message box saying 0, because ByVal passes a copy of the value (or a copy of the pointer to the object reference, when we're talking about objects).
Contrast with:
Option Explicit
Public Sub Test()
Dim foo As Long
Assign foo
MsgBox foo
End Sub
Private Sub Assign(ByRef bar As Long) ' or implicit: (bar As Long)
bar = 42
End Sub
This will pop a message box saying 42, because ByRef passes a pointer to the value (or in the case of object references, the pointer itself). Note that this is usually not behavior you want to allow, hence most parameters should be passed by value.
Related
I am a little confused about the usage of ByVal keyword in some event-handlers in Excel.
The NewSheet event example
(Copied from Excel 2019 Power Programming with VBA, Wiley)
Private Sub Workbook_NewSheet(ByVal Sh As Object)
If TypeName(Sh) = "Worksheet" Then
Sh.Cells.ColumnWidth = 35
Sh.Range("A1") = "Sheet added " & Now()
End If
End Sub
This code works as expected, as seems trivial. But after some thought I am getting more and more confused.
Based on my understanding ByVal means Sh is just copy of the the original worksheet object, and procedures using ByVal arguments won't result in change to the original object. In other words, what the code does should have no effect.
Only when references are passed to procedures will they be able to modify objects referred to by those references.
Am I missing something there? Thanks
P.S. Most of my understanding of passing by reference/value comes from other programming languages such as C#. There might be some peculiarity in VBA I am not aware of.
Since you're passing an Object, ByVal passes a copy (the value) of the reference. The copy (value) of the reference still points to what the original reference is pointing to. Hence your method works and changes what the original reference is pointing to.
If you know Java, this is similar in nature to Java, where everything can be said to 'pass by value' but when you pass an Object as a parameter, what you are actually doing is passing a copy of the reference, and not a copy of what the reference is pointing to.
ByVal in those cases prevents the change of the value in the original reference itself.
Please see next:
Module Module1
Sub Main()
' Declare an instance of the class and assign a value to its field.
Dim c1 As New Class1()
c1.Field = 5
Console.WriteLine(c1.Field)
' Output: 5
' ByVal does not prevent changing the value of a field or property.
ChangeFieldValue(c1)
Console.WriteLine(c1.Field)
' Output: 500
' ByVal does prevent changing the value of c1 itself.
ChangeClassReference(c1)
Console.WriteLine(c1.Field)
' Output: 500
Console.ReadKey()
End Sub
Public Sub ChangeFieldValue(ByVal cls As Class1)
cls.Field = 500
End Sub
Public Sub ChangeClassReference(ByVal cls As Class1)
cls = New Class1()
cls.Field = 1000
End Sub
Public Class Class1
Public Field As Integer
End Class
End Module
So I have a Workbook_open sub that creates Long variables when workbook is opened:
Private Sub Workbook_open()
MsgBox ("Workbook opened")
Dim i As Long
i = 68
End Sub
How would I pass the i value to a Worksheet_change sub for a particular worksheet?
Dim i inside a procedure scope makes i a local variable; it's only accessible from within the procedure it's declared in.
The idea of passing i to another procedure is very sound: it means you intend to keep variable scopes as tight as possible, and that's a very very good thing.
But in this case it's too tight, because the parameters of an event handler are provided by the event source: you need to "promote" that local variable up one scope level.
And the next tightest scope level is module scope.
You can use the Dim keyword at module level, but for consistency's sake I'd recommend using the keyword Private instead. So in the same module, declare your module-level variable:
Option Explicit
Private i As Long
Private Sub Workbook_open()
MsgBox "Workbook opened"
i = 68
End Sub
If you want to expose that variable's value beyond this module, you can expose an accessor for it:
Option Explicit
Private i As Long
Private Sub Workbook_open()
MsgBox "Workbook opened"
i = 68
End Sub
Public Property Get MyValue() As Long
'invoked when MyValue is on the right-hand side expression of an assignment,
'e.g. foo = ThisWorkbook.MyValue
MyValue = i
End Property
Now the Sheet1 module's Worksheet_Change handler can read it:
Private Sub Worksheet_Change(ByVal Target As Range)
MsgBox ThisWorkbook.MyValue
End sub
But it can't write to it, because the property is "get-only". If everyone everywhere needs to be able to read/write to it, then you might as well make it a global variable, or expose a mutator for it:
Option Explicit
Private i As Long
Private Sub Workbook_open()
MsgBox "Workbook opened"
i = 68
End Sub
Public Property Get MyValue() As Long
'invoked when MyValue is on the right-hand side expression of an assignment,
'e.g. foo = ThisWorkbook.MyValue
MyValue = i
End Property
Public Property Let MyValue(ByVal value As Long)
'invoked when MyValue is on the left-hand side of an assignment,
'e.g. ThisWorkbook.MyValue = 42; the 'value' parameter is the result of the RHS expression
i = value
End Property
For that, declare i as a Public Variable on a Standard Module like Module1 and then you can access the value of i in Sheet Change Event if it is initialized during Workbook Open Event.
On Standard Module:
Public i As Long
On ThisWorkbook Module:
Private Sub Workbook_open()
MsgBox ("Workbook opened")
i = 68
End Sub
I broke out some code from a larger block and need to pass a worksheet to it...
I'm not assigning any new value to the worksheet, but I am making changes to the Page Break settings for that sheet. Do I need to pass it as ByRef, or is ByVal good enough?
Private Sub SetPageBreaks(ByRef wsReport As Worksheet)
Dim ZoomNum As Integer
wsReport.Activate
ActiveWindow.View = xlPageBreakPreview
ActiveSheet.ResetAllPageBreaks
ZoomNum = 85
With ActiveSheet
Select Case wsReport.Name
Case "Compare"
Set .VPageBreaks(1).Location = Range("AI1")
ZoomNum = 70
Case "GM"
.VPageBreaks.Add before:=Range("X1")
Case "Drift"
.VPageBreaks.Add before:=Range("T1")
Case Else
.VPageBreaks.Add before:=Range("U1")
End Select
End With
ActiveWindow.View = xlNormalView
ActiveWindow.Zoom = ZoomNum
End Sub
Either will work, but for semantically correct code, prefer passing it by value (ByVal).
When you pass an object variable by value, you're passing a copy of the pointer to the object.
So what the procedure is working with is the same object (i.e. changed property values will be seen by the caller), except it's not allowed to Set the pointer to something else - well it can, but it'll do that on its own copy and so the caller won't be affected.
Public Sub DoSomething()
Dim target As Worksheet
Set target = ActiveSheet
Debug.Print ObjPtr(target)
DoSomethingElse target
Debug.Print ObjPtr(target)
End Sub
Private Sub DoSomethingElse(ByVal target As Worksheet)
Debug.Print ObjPtr(target)
Set target = Worksheets("Sheet12")
Debug.Print ObjPtr(target)
'in DoSomething, target still refers to the ActiveSheet
End Sub
On the other hand...
Public Sub DoSomething()
Dim target As Worksheet
Set target = ActiveSheet
Debug.Print ObjPtr(target)
DoSomethingElse target
Debug.Print ObjPtr(target)
End Sub
Private Sub DoSomethingElse(ByRef target As Worksheet)
Debug.Print ObjPtr(target)
Set target = Worksheets("Sheet12")
Debug.Print ObjPtr(target)
'in DoSomething, target now refers to Worksheets("Sheet12")
End Sub
In general, parameters should be passed by value. It's just an unfortunate language quirk that ByRef is the default (VB.NET fixed that).
The same is true for non-object variables:
Public Sub DoSomething()
Dim foo As Long
foo = 42
DoSomethingElse foo
End Sub
Private Sub DoSomethingElse(ByVal foo As Long)
foo = 12
'in DoSomething, foo is still 42
End Sub
And...
Public Sub DoSomething()
Dim foo As Long
foo = 42
DoSomethingElse foo
End Sub
Private Sub DoSomethingElse(ByRef foo As Long)
foo = 12
'in DoSomething, foo is now 12
End Sub
If a variable is passed by reference, but is never reassigned in the body of a procedure, then it can be passed by value.
If a variable is passed by reference, and reassigns it in the body of a procedure, then that procedure could likely be written as a Function, and actually return the modified value instead.
If a variable is passed by value, and is reassigned in the body of a procedure, then the caller isn't going to see the changes - which makes code suspicious; if a procedure needs to reassign a ByVal parameter value, the intent of the code becomes clearer if it defines its own local variable and assigns that instead of the ByVal parameter:
Public Sub DoSomething()
Dim foo As Long
foo = 42
DoSomethingElse foo
End Sub
Private Sub DoSomethingElse(ByVal foo As Long)
Dim bar As Long
bar = foo
'...
bar = 12
'...
End Sub
These are all actual code inspections in Rubberduck, as VBE add-in I'm heavily involved with, that can analyze your code and see these things:
Parameter is passed by value, but is assigned a new value/reference. Consider making a local copy instead if the caller isn't supposed to know the new value. If the caller should see the new value, the parameter should be passed ByRef instead, and you have a bug.
http://rubberduckvba.com/Inspections/Details/AssignedByValParameterInspection
A procedure that only has one parameter passed by reference that is assigned a new value/reference before the procedure exits, is using a ByRef parameter as a return value: consider making it a function instead.
http://rubberduckvba.com/Inspections/Details/ProcedureCanBeWrittenAsFunctionInspection
A parameter that is passed by reference and isn't assigned a new value/reference, could be passed by value instead.
http://rubberduckvba.com/Inspections/Details/ParameterCanBeByValInspection
That's the case of wsReport here:
I have a problem with global variable
I'm working with modules in vba
-- In module5 is define the global variable
Public tes As Integer
-- In module6 I define the "tes"
Function a()
tes = 1
End Function
-- In module I try to call "tes" variable
Sub test()
MsgBox tes
End Sub
I suppose the result should show 1, but it show 0
I wonder what's wrong
Thanks
Randy
If a never runs, tes is never assigned. Make sure a runs before you read tes and the msgbox will show 1:
Sub test()
a
MsgBox tes
End Sub
That said, it's probably better to pass tes as a parameter instead of sprinkling global variables that can be modified from anywhere.
Sub DoSomething()
Dim foo As Integer
foo = 1
Test foo
End Sub
Sub Test(ByVal value As Integer)
MsgBox value
End Sub
Variables should usually be as tightly scoped and short-lived as possible; use globals only when you need them (that should be rarely), not everytime a function/procedure needs to access some data.
I try to understand the VBA scope type, it's impossible to make this such of thing in VBA, but it's possible in other language (java,scala,etc):
public sub try()
dim myVar as String
myvar = "hello world"
Call displayVar()
end sub
public sub displayVar()
msgbox (myvar)
end sub
Can you give me some information about this type of limited scoping ? It's dynamical or lexical, i don't really understand the difference :/
Franck Leveque has provided a clear and simple demonstratation of the difference between local and global declarations.
However, like most languages, VBA allows you to pass parameters to subroutines. Sometimes a global variable is the only choice or the only sensible choice. But normally it it is better to declare myVar within try and pass it as a parameter to displayVar. This prevents displayVar from accidentally changing myVar because, by default, parameters are passed as values. If you want a subroutine to change the value of a parameter you must explicitly pass the parameter as a reference. This is true of most modern programming languages.
Note also that Public means these subroutines are visible to subroutines in other modules. If Public was omitted or replaced by Private, try and displayVar would only be visible within their module.
In the code below I have passed the value of myVar as a parameter to displayVar.
Public Sub try()
Dim myVar As String
myvar = "hello world"
Call displayVar(myVar)
End Sub
Public Sub displayVar(Stg As String)
Call Msgbox(Stg, VBOKOnly)
End Sub
Variable myVar is declared in the function try()
As such, you can only use it in the try() function scope.
By try() function scope, I mean all the instruction written between public sub try() and end sub.
You try to call your variable from another function (displayVar). Which define it's own scope and is not inside the try function scope.
If you want this, you have to declare your variable in the global scope (outside any function)
For example :
dim myVar as String
public sub try()
myvar = "hello world"
Call displayVar()
end sub
public sub displayVar()
msgbox (myvar)
end sub