Simple as below:
If 1 = 2 Then
Dim i As Integer
Else
Dim i As Integer
End If
This will give an error as "duplicated declaration in current scope". Why?
Variables are local to the sub/function (procedure). In your case, the "current scope". VB, VBA, and VBScript do not have the concept of code blocks within a procedure. If I remember correctly, the only code blocks are modules, classes, forms (which are classes), and procedures.
You can declare a variable anywhere within the procedure as a convenience. Yet, the "current scope" is the procedure.
It was a design decision to make the language easier to use for B-eginners.
Duplicate declaration in current scope
The specified name is already used at this level of scope. For
example, two variables can have the same name if they are defined in
different procedures, but not if they are defined within the same
procedure.
Inserted from http://msdn.microsoft.com/en-us/library/gg251613.aspx
In VBA, declarations of two variables with the same name cannot be made in the same procedure. See this article for more information: Duplicate declaration in current scope.
In your case, the "current scope" is the current procedure. The if and else blocks share the same scope.
For example, the following will give the same error even though the second declaration is unreachable:
Sub ErrorSub()
Dim i As Integer
If False Then
Dim i As Integer
End If
End Sub
But the following is perfectly valid and will work fine:
Sub MySub()
Dim i As Integer
i = 4
MsgBox i
End Sub
Sub MyOtherSub()
Dim i As Integer
i = 3
MsgBox i
End Sub
Sub CallSubs()
MySub
MyOtherSub
End Sub
As mentioned, you cannot have more than one Dim statement for a variable with the same scope. Aside from that, however, why would you want to? Take your code for example.
If 1 = 2 Then
Dim i As Integer
Else
Dim i As Integer
End If
The purpose of a conditional block is to execute two different pieces of code under two different circumstances. If you are declaring the same variable either way, there is no need for the conditional block at all. In all likelihood, it should probably be moved before the conditional.
Related
I want to Get List of Variables used in the Script i.e. VariableName13, strDLink, strZone.
A single file contains about 150+ events and each project contains about 700-900 files.
In VBA environment, I want to parse through each file, loop through each event and extract the Variable names declared or referenced by the Events.
I did find some material like Roslyn or TypeLib but unable to understand how to use them?
Can someone please share a proper approach to extract the variables?
Environment: VBA 6, SCADA HMI
Private Sub Rect13_Click()
Dim lResult As Long
Dim strDLink As String
Dim strZone As String
On Error GoTo ErrorHandler
lResult = OpenFuncUpdate
If lResult = SomeValue Then
'DoThis
ElseIf lResult = SomeOtherValue Then
strDLink = "FullPathLink"
FuncDisassemblePath strDLink, , , , , , , , , , , , strZone
If Len(strZone) > 0 And (InStr(VariableName13.CurrentValue, "%") = 0) Then
SubLoadIA strZone & "%" & VariableName13.CurrentValue, Me
Else
SubLoadIA VariableName13.CurrentValue, Me
End If
End If
Exit Sub
ErrorHandler:
SubHandleError
End Sub
Depending on how you define what a "variable" is, you can try to parse VBA code with VBA code and regular expressions.
If all your declarations are consistently made, and consistently declare a single variable, and variables are consistently declared (Option Explicit is in every module), then capturing Dim|Private|Public|Friend|Global {identifier} should be good-enough... but that makes a lot of "ifs"
Real-life projects have Dim statements that can declare a list of local or private variables. Or there's a ReDim statement somewhere that's actually declaring an array on-the-spot. Or they don't always specify Option Explicit and variables aren't always even declared at all. Or there's a line continuation in the middle of the statement that breaks the regular expression. Or, or, or... so many things can go wrong, parsing VBA code is a rabbit hole.
For example suppose you need to pick up and list undeclared variables. A regular expression can't tell its usage from a procedure or function call, because they're syntactically identical. Regular expressions are missing the context of the grammar - and it's by tokenizing (aka "lexing") the source code and then parsing the tokens using parser rules that we can be 100% certain of what we're looking at.
Fortunately this is a solved problem, and there's free, open-source VBIDE tooling available for this, and get you 100% correct results every time without writing a single line of code or worrying about what legal declarations might be left unaccounted for.
Rubberduck (I manage this OSS project and own its website) will correctly parse any legal VB6/vBA code (and if it doesn't, we're extremely interested in a repro!), and then you can simply click a "copy" button to instantly have every single declaration in the clipboard:
Ctrl+V /paste onto a worksheet (or a Word document, or in Notepad!) and then you can easily turn it into a filter-enabled table; in your case you'd want to filter the [Declaration Type] for "Variable":
Above, the exported declarations for a VBA project that has a Sheet1 module with a test procedure that uses (but doesn't declare) a variable named undeclared:
Sub test()
undeclared = 42
Debug.Print undeclared
End Sub
Here's the same table for the code you've provided:
Note that SubHandleError and other Sub and Function calls would parse as and resolve to a procedure/function in your project. Here they're being picked up as undeclared variables because I didn't parse anything other than the code you supplied, so these identifiers are undefined.
I am attempting to reduce the amount of clutter on my code by creating "shortcuts" if you will
For instance, I always have to type
ThisWorkBook.ActiveSheet.Range
Is there a way for me to define the above to create a less wordy macro? I have tried convert to range and string and the former returns an error (but I could still get intellisense recognize and attempt to autofill) while the string version doesnt work.
Just like in any programming language, you can use variables to store data
For example:
Dim myrange As Range: Set myrange = Sheets("Sheet1").Range("B5")
Alternatively, if you will be working with the same object multiple times, you can use the With keyword
For example. instead of writing you want to work with table every time on every new line you can do
With Sheets("Sheet1").ListObjects("Table1")
.ListRows.Add
.ListColumns(2).Range(3) = "Hello World!"
' ... and so on
End With
Also, please on a sidenote: Avoid using Select/ActiveSheet/ActiveWorkbook and so on!
More info on how to here
You can create functions or customized properties, which are always evaluated when called
Property Get pARng As Range
Set pARng = ThisWorkBook.ActiveSheet.Range
End Property
Function fARng As Range
Set fARng = ThisWorkBook.ActiveSheet.Range
End Function
'Usage
Sub xxx
'...
pARng.Rows(1).Activate
'Same as ThisWorkBook.ActiveSheet.Range.Rows(1).Activate
fARng.Rows(1).Activate
'using function instead achieves same result
End Sub
This question already has an answer here:
Array argument must be ByRef
(1 answer)
Closed 6 years ago.
Given the following code:
I can not seem to successfully pass a Range Object Variable from one sub-function to another. I spent an entire day researching, and experimenting before I swallowed pride and came here.
Please read the comments below, and reply with any ideas you have regarding why the LAST two lines will not behave.
Public Sub doSomethingToRows(ROI As Range)
*'do Something with the cell values within the supplied range*
End Sub
'
Public Sub testDoAltRows()
Dim RegionOfInterest As Range 'is this an object or not?
'*The following yields: Class doesn't support Automation (Error 430)*
'*Set RegionOfInterest = New Worksheet 'this just gives an error*
Set RegionOfInterest = Worksheets("Sheet1").Range("A1")
RegionOfInterest.Value = 1234.56 '*okay, updates cell A1*
Set RegionOfInterest = Worksheets("Sheet1").Range("B5:D15")
RegionOfInterest.Columns(2).Value = "~~~~~~" '*okay*
'doSomethingToRows (RegionOfInterest) 'why do I get "OBJECT IS REQUIRED" error?
doSomethingToRows (Worksheets("Sheet1").Range("B5:C15")) 'but this executes okay
End Sub
From the msdn documentation of the Call keyword statement,
Remarks
You are not required to use the Call keyword when calling a procedure.
However, if you use the Call keyword to call a procedure that requires
arguments, argumentlist must be enclosed in parentheses. If you omit
the Call keyword, you also must omit the parentheses around
argumentlist. If you use either Call syntax to call any intrinsic or
user-defined function, the function's return value is discarded.
To pass a whole array to a procedure, use the array name followed by
empty parentheses.
From a practical standpoint, even though Subs can be called with or without the "Call" keyword, it makes sense to pick one way and stick with it as part of your coding style. I agree with Comintern - it is my opinion, based on observation of modern VBA code, that using the "Call" keyword should be considered deprecated. Instead, invoke Subs without parenthesis around the argument list.
And now the answer to the important question:
Why does your code throw an error?
Take for example the following Subroutine:
Public Sub ShowSum(arg1 As Long, arg2 As Long)
MsgBox arg1 + arg2
End Sub
We have established that, if not using the Call keyword, Subs must be invoked like so:
ShowSum 45, 37
What happens if it were instead called like ShowSum(45, 37)? Well, you wouldn't even be able to compile as VBA immediately complains "Expected =". This is because the VBA parser sees the parenthesis and decides that this must be a Function call, and it therefore expects you to be handling the return value with an "=" assignment statement.
What about a Sub with only one argument? For example:
Public Sub ShowNum(arg1 As Long)
MsgBox arg1
End Sub
The correct way to call this Sub is ShowNum 45. But what if you typed this into the VBA IDE: ShowNum(45)? As soon as you move the cursor off of the line, you'll notice that VBA adds a space between the Sub name and the opening parenthesis, giving you a crucial clue as to how the line of code is actually being interpreted:
ShowNum (45)
VBA is not treating those parenthesis as if they surrounded the argument list - it is instead treating them as grouping parenthesis. MOST of the time, this wouldn't matter, but it does in the case of Objects which have a default member.
To see the problem this causes, try running the following:
Dim v As Variant
Set v = Range("A1")
Set v = (Range("A1")) '<--- type mismatch here
Notice that you get a "Type Mismatch" on the marked line. Now add those two statements to the watch window and look at the "Type" column:
+-------------+-----+--------------+
| Expression |Value| Type |
+-------------+-----+--------------+
|Range("A1") | |Object/Range |
|(Range("A1"))| |Variant/String|
+-------------+-----+--------------+
When you surround an Object with grouping parenthesis, its default property is evaluated - in the case of the Range object, it is the Value property.
So it's really just a coincidence that VBA allowed you to get away with "putting parenthesis around the argumentlist" - really, VBA just interprets this as grouping parenthesis and evaluates the value accordingly. You can see by trying the same thing on a Sub with multiple parameters that it is invalid in VBA to invoke a Sub with parenthesis around the argument list.
#PaulG
Try this:
Public Sub Main()
Debug.Print TypeName(Range("A1"))
Debug.Print TypeName((Range("A1")))
End Sub
okay, I knew after I posted this question I'd be struck by lighting and receive an answer.
When passing an object VARIABLE to a sub-function and wishing to use parentheses "()", one must use CALL! Thus the correction to my code sample is:
**CALL doSomethingToRows(RegionOfInterest)**
Thank you!
Maybe we're talking about different things, but here's an example to make it a bit clearer what I mean.
Option Explicit
Sub TestDisplay()
Dim r As Range
'Create some range object
Set r = Range("A1")
'Invoke with Call.
Call DisplaySomething(r)
'Invoke without Call.
DisplaySomething r
End Sub
Sub DisplaySomething(ByVal Data As Range)
Debug.Print "Hi my type is " & TypeName(Data)
End Sub
Both calls work perfectly. One with Call and the other without.
Edit:
#Conintern. Thanks for explaining that. I see what is meant now.
However, I still respectively disagree.
If I declare the following:
Function DisplaySomething(ByVal Data As String)
DisplaySomething = "Hi my type is " & TypeName(Data)
End Function
and invoke it:
Debug.print DisplaySomething(Range("A1"))
I believe that Excel has been clever and converted to a string. It can do that by invoking the Default Parameter and can convert to a string.
However, as in the original parameter example, If I declare the following:
Function DisplaySomething(ByVal Data As Range)
DisplaySomething = "Hi my type is " & TypeName(Data)
End Function
There is no call on the Default Parameter, however it is called, because Excel was able to resolve it to that type.
Function DisplaySomething(ByVal Data As Double)
DisplaySomething = "Hi my type is " & TypeName(Data)
End Function
will return a double because it was able to coerce to a double.
Indeed in those examples the Default was called.
But in this example we are defining as Range. No Default called there however it is invoked - brackets or no brackets.
I believe this is more to do with Excel and data coercion.
Similar to the following:
Public Function Test(ByVal i As String) As Integer
Test = i
End Function
and invoking with:
Debug.print Test("1")
BTW, yes I know this isn't an object without a Default parmeter. Im pointing out data coercion. Excel does its best to resolve it.
Could be wrong mind you...
When compiling some code (declarations shown below) I get the error message 'Compile Error: Ambiguous name detected. SixTables'. I have looked here and elsewhere but cannot find anything that matches my problem. What appear to be the most common causes of this error, declaring two variables with identical names or giving the same name to a function and the sub that it is called from, do not apply. And yes, I know I could just change the name to something the system was happy with, but (1) I wouldn't learn what I'm doing wrong, and (2) I chose that name for a reason - it fits its purpose exactly :-)
Option Explicit
Dim ArmOfService As Byte
Dim CharacterNumber As Long
Dim CurrentTerm As Byte
Dim DecorationRollMade As Byte
Dim DecorationRollNeeded As Byte
Dim DiceSize As Byte
Dim GenAssignment
Dim GenAssignmentSwitchInt As Byte
Dim GenAssignmentSwitchOff As Byte
Dim iLoopControl
Dim jLoopControl
Dim kLoopControl
Dim LineIncrement As Integer
Dim lLoopControl
Dim Merc(100)
Dim NoOfDice As Byte
Dim OfficerPromotion(63 To 78) As Byte
Dim PromotionRollMade As Byte
Dim PromotionRollNeeded As Byte
Dim Roll As Byte
Dim SkillColumn
Dim SixTables
Dim SpecAssignmentSwitchEnd As Byte
Dim SurvivalRollMade As Byte
Dim SurvivalRollNeeded As Byte
Dim TechLevel As Byte
Dim Temp As Integer
Dim Term As Byte
Dim TestCount
Dim UnitAssignment
Dim WhichTable
Dim Year As Byte
EDIT: I'm so embarrassed that I can hardly bring myself to explain what the problem was. I knew I hadn't duplicated a name, since I was only using it once - as a function! Thanks all for your help, I'm now going to go and hide my face in shame...
From MSDN :
More than one object in the same scope may have elements with the
same name.
Module-level identifiers and project-level identifiers (module names
and referenced project names) may be reused in a procedure, although
it makes programs harder to maintain and debug. However, if you want
to refer to both items in the same procedure, the item having wider
scope must be qualified. For example, if MyID is declared at the
module level of MyModule , and then a procedure-levelvariable is
declared with the same name in the module, references to the
module-level variable must be appropriately qualified:
Dim MyID As String
Sub MySub
MyModule.MyID = "This is module-level variable"
Dim MyID As String
MyID = "This is the procedure-level variable"
Debug.Print MyID
Debug.Print MyModule.MyID
End Sub
An identifier declared at module-level conflicts with a procedure name.
For example, this error occurs if the variable MyID is declared at
module level, and then a procedure is defined with the same name:
Public MyID
Sub MyID
. . .
End Sub
Having had this issue many times, and not fully understanding why, I think there is an important fact that I have tested, and someone can confirm. It may be obvious to professional programmers, but I place this here for those that seek these answers on discussions, who are usually not professional programmers.
A variable declared within a sub() is not "declared" (memory assigned) until that sub() is executed. And when the execution of that sub() is complete, the memory is released. Until now, I thought Public variables acted in similar way; available to any module that used it, --BUT only existing as long as the module where they were declared was still executing.
However, for any Public variable who's declaration line is in any module in the workbook, that variable is declared and available at the execution of any module within the workbook, even if the module where the Public declaration exists is never called or executed.
Therefore, if you have a Public variable declaration in one module, and then again in a separate module that you plan to run independent of the first, Excel still sees 2 declarations for the same variable, and thus it is ambiguous.
To prove it, you can create a blank module within a workbook project, and add absolutely nothing except two Public declaration lines,
Public VariableX as String
Public VariableY as Integer
and those variables will be declared and available throughout the project, for any sub() executed. This fact is also probably the reason for the tip about minimizing the use of public variables.
Again, this may have been kindergarten level information for professional programmers, but most on this are not pros, and have to learn these lessons the hard way, or through discussion boards like this. I hope this helps someone.
I want to do this but it won't compile:
Public MyVariable as Integer = 123
What's the best way of achieving this?
.NET has spoiled us :)
Your declaration is not valid for VBA.
Only constants can be given a value upon application load. You declare them like so:
Public Const APOSTROPHE_KEYCODE = 222
Here's a sample declaration from one of my vba projects:
If you're looking for something where you declare a public variable and then want to initialize its value, you need to create a Workbook_Open sub and do your initialization there.
Example:
Private Sub Workbook_Open()
Dim iAnswer As Integer
InitializeListSheetDataColumns_S
HideAllMonths_S
If sheetSetupInfo.Range("D6").Value = "Enter Facility Name" Then
iAnswer = MsgBox("It appears you have not yet set up this workbook. Would you like to do so now?", vbYesNo)
If iAnswer = vbYes Then
sheetSetupInfo.Activate
sheetSetupInfo.Range("D6").Select
Exit Sub
End If
End If
Application.Calculation = xlCalculationAutomatic
sheetGeneralInfo.Activate
Load frmInfoSheet
frmInfoSheet.Show
End Sub
Make sure you declare the sub in the Workbook Object itself:
Just to offer you a different angle -
I find it's not a good idea to maintain public variables between function calls. Any variables you need to use should be stored in Subs and Functions and passed as parameters. Once the code is done running, you shouldn't expect the VBA Project to maintain the values of any variables.
The reason for this is that there is just a huge slew of things that can inadvertently reset the VBA Project while using the workbook. When this happens, any public variables get reset to 0.
If you need a value to be stored outside of your subs and functions, I highly recommend using a hidden worksheet with named ranges for any information that needs to persist.
Sure you know, but if its a constant then const MyVariable as Integer = 123 otherwise your out of luck; the variable must be assigned an initial value elsewhere.
You could:
public property get myIntegerThing() as integer
myIntegerThing= 123
end property
In a Class module then globally create it;
public cMyStuff as new MyStuffClass
So cMyStuff.myIntegerThing is available immediately.
Little-Known Fact: A named range can refer to a value instead of specific cells.
This could be leveraged to act like a "global variable", plus you can refer to the value from VBA and in a worksheet cell, and the assigned value will even persist after closing & re-opening the workbook!
To "declare" the name myVariable and assign it a value of 123:
ThisWorkbook.Names.Add "myVariable", 123
To retrieve the value (for example to display the value in a MsgBox):
MsgBox [myVariable]
Alternatively, you could refer to the name with a string: (identical result as square brackets)
MsgBox Evaluate("myVariable")
To use the value on a worksheet just use it's name in your formula as-is:
=myVariable
In fact, you could even store function expressions: (sort of like in JavaScript)
(Admittedly, I can't actually think of a situation where this would be beneficial - but I don't use them in JS either.)
ThisWorkbook.Names.Add "myDay", "=if(isodd(day(today())),""on day"",""off day"")"
Square brackets are just a shortcut for the Evaluate method. I've heard that using them is considered messy or "hacky", but I've had no issues and their use in Excel is supported by Microsoft.
There is probably also a way use the Range function to refer to these names, but I don't see any advantage so I didn't look very deeply into it.
More info:
Microsoft Office Dev Center: Names.Add method (Excel)
Microsoft Office Dev Center: Application.Evaluate method (Excel)
As told above, To declare global accessible variables you can do it outside functions preceded with the public keyword.
And, since the affectation is NOT PERMITTED outside the procedures, you can, for example, create a sub called InitGlobals that initializes your public variables, then you just call this subroutine at the beginning of your statements
Here is an example of it:
Public Coordinates(3) as Double
Public Heat as double
Public Weight as double
Sub InitGlobals()
Coordinates(1)=10.5
Coordinates(2)=22.54
Coordinates(3)=-100.5
Heat=25.5
Weight=70
End Sub
Sub MyWorkSGoesHere()
Call InitGlobals
'Now you can do your work using your global variables initialized as you wanted them to be.
End Sub
You can define the variable in General Declarations and then initialise it in the first event that fires in your environment.
Alternatively, you could create yourself a class with the relevant properties and initialise them in the Initialise method
This is what I do when I need Initialized Global Constants:
1. Add a module called Globals
2. Add Properties like this into the Globals module:
Property Get PSIStartRow() As Integer
PSIStartRow = Sheets("FOB Prices").Range("F1").Value
End Property
Property Get PSIStartCell() As String
PSIStartCell = "B" & PSIStartRow
End Property
there is one way to properly solve your question. i have the same concern with you for a long time. after searching and learning for a long time, finally i get a solution for this kind of question.
The solution is that no need to declare the variable and no need to set value to the variable, and even no need VBA code. Just need the "named range" in excel itself.
For example, the "A1" cell content is "hello, world". and we define the "A1" cell a name as "hello", that is, the "A1" cell have a name now, it's called "hello".
In VBA code, we just need use this method [hello], then we can get the "A1" value.
Sub test()
msgbox [hello]
end sub
the msgbox will show "Hello, word".
this way, we get a global variable without any declaration or assignment. it can be used in any Sub or Function.
we can define many named range in excel, and in VBA code we just use [] method to get the range value.
in fact, the [hello] is a abbreviation of the function Evaluate["Hell"], but it's more shorter.
It's been quite a while, but this may satisfy you :
Public MyVariable as Integer: MyVariable = 123
It's a bit ugly since you have to retype the variable name, but it's on one line.