MsgBox not accepting optional parameters - vba

I'm struggling with this basic piece of code :
If Dir(LocationAddress & "\" & chart & " Complete.pdf") = "" Then
MsgBox("The file wasn't created.", vbCritical + vbRetryCancel)
Else
MsgBox ("The file was created.")
End If
When I click "save" in the VBA editor, the line corresponding to the error message turns red, and when I try to execute, it tells me there's a syntax error. I found this similar code online, with a different syntax, which also doesn't work, even when copied and pasted into the editor.
MsgBox("Important message", MsgBoxStyle.Critical, "MsgBox Example")
I also ran my initial code with only one style instruction as an optional argument, to make sure the issue wasn't simply that I was combining them with improper syntax.
I hardly know anything about vba, I've only written a few subs by copying and editing code found online.

As igittr commented above, on the line MsgBox("The file wasn't created.", vbCritical + vbRetryCancel), the parenthesis aren't needed.
When there's only one statement on the line, then VBA knows that the arguments are for the MsgBox procedure. If you put brackets around the arguments, it will try to evaluate everything within the brackets first, resulting in the error (that's why MsgBox ("The file was created.") still works, even though the brackets again aren't needed).
So either write the line as MsgBox "The file wasn't created.", vbCritical + vbRetryCancel
Or, if you want to still use brackets, use Call MsgBox("The file wasn't created.", vbCritical + vbRetryCancel). This works because you have two statements, Call and MsgBox, and the brackets are needed to indicate what procedure the arguments belong to.

Since, you show the message with some buttons option, the working solution should look as:
Dim ans As VbMsgBoxResult
ans = MsgBox("The file wasn't created.", vbCritical + vbRetryCancel, "A choice...")
If ans <> vbRetry Then Exit Sub ' the code will stop even if the right corner 'x' will be clicked.
'your code if want it continuing...
'or viceversa according to what do you intend doing in case of Cancel option.

MsgBox("The file wasn't created.", vbCritical + vbRetryCancel)
This won't work as you are calling a function as a sub. When calling functions as subs it needs to be treated as a sub which means no brackets. This is core Basic - brackets for functions and no brackets for subs.
MsgBox ("The file was created.")
This will work because you aren't using brackets for the sub. Brackets around a parameter mean pass that parameter by value. Where ever possible computer languages pass by reference as it more efficient.
However it means that the string is copied, and the copy given to msgbox. So it uses more memory.
Call
call is obsolete. In QuickBasic it allowed a different calling convention when calling inline assembler functions. Assembler didn't return a value. So the convention was to pass a variable by reference as an input parameter and on return would hold the return value.
Dir
Dir is obsolete. It became obsolete in the 90s when it was replaced by the file system object. See https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/filesystemobject-object
Brackets in Basic
From https://ericlippert.com/2003/09/15/what-do-you-mean-cannot-use-parentheses/
The author was the maintainer programmer at Microsoft for VBScript.
Here’s the deal: parentheses mean several different things in VB and
hence in VBScript. They mean:
Define boundaries of a subexpression: Average = (First + Last) / 2
Dereference the index of an array: Item = MyArray(Index)
Call a function or subroutine: Limit = UBound(MyArray)
Pass an argument which would normally be byref as byval: in Result
= MyFunction(Arg1, (Arg2)) , Arg1 is passed by reference, Arg2is passed by value.
That’s confusing enough already. Unfortunately, VB and hence VBScript
has some weird rules about when #3 applies. The rules are
3.1) An argument list for a function call with an assignment to the returned value must be surrounded by parens: Result = MyFunc(MyArg)
3.2) An argument list for a subroutine call (or a function call with no assignment) that uses the Call keyword must be surrounded by
parens: Call MySub(MyArg)
3.3) If 3.1 and 3.2 do not apply then the list must not be surrounded by parens.
And finally there is the byref rule: arguments are passed by reference
when possible but if there are “extra” parens around a variable then
the variable is passed by value, not by reference.
Trivia
The msgbox function is a wrapper around the Windows MessageBox() functions. Basic passes on verbatim any parameters you pass. Of course only on Windows can you use the system parameters not listed in the Basic docs. See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox

Related

Subroutine will not compile

I'm using Access VBA, and I keep getting
Compile error: Argument not optional
whenever I try to pass a collection into a function. What is going on?
Private Sub btnTest_Click()
Dim GarbageLanguages As New Collection
GarbageLanguages.Add "VBA"
PrintCollectionCount (GarbageLanguages) '<-- error happens here
End Sub
Public Sub PrintCollectionCount(c As Collection)
Debug.Print c.Count
End Sub
Short Answer
Remove the parentheses from the following line:
PrintCollectionCount (GarbageLanguages)
Long Answer
For better or worse (mostly worse), VBA has both functions and subroutines:
Function - expression that must return a value
Subroutine - statement that cannot return a value
Unfortunately, using each of them requires slightly different syntax. Suprisingly, this is not a valid subroutine call:
Subroutine(arguments)
Instead, you need to use one of these two options:
Call Subroutine(arguments)
Subroutine arguments
It's even more unfortunate that when you use the wrong syntax, all you get is extremely cryptic error messages. Finally, it's also hard to get used to not using parenthesis because single arguments that are primitive types instead of objects actually work fine:
Subroutine(SomeString) ' works
Subroutine(SomeInteger) ' works
Subroutine(SomeObject) ' does not work
Subroutine(SomeString, SomeInteger) ' does not work
Aside from memorizing the awful error messages, you can try to train yourself to look out for whenever a space gets automatically inserted after the subroutine's name. This:
Subroutine(argument)
gets changed to this:
Subroutine (argument) '<-- RED FLAG

Input box getting a compile error in VBA

I am learning how to create input boxes and I keep getting the same error. I have tried two different computers and have received the same error. The error I get is a "Compile Error: Wrong number of arguments or invalid property assignment"
Here is my code:
Option Explicit
Sub InputBox()
Dim ss As Worksheet
Dim Link As String
Set ss = Worksheets("ss")
Link = InputBox("give me some input")
ss.Range("A1").Value = Link
With ss
If Link <> "" Then
MsgBox Link
End If
End With
End Sub
When I run the code, it highlights the word "inputbox"
And help would be greatly appreciated.
Thanks,
G
Three things
1) Call your sub something other than the reserved word InputBox as this may confuse things. *Edit... and this alone will resolve your error. See quote from #Mat's Mug.
2) A̶p̶p̶l̶i̶c̶a̶t̶i̶o̶n̶.̶I̶n̶p̶u̶t̶B̶o̶x̶(̶"̶g̶i̶v̶e̶ ̶m̶e̶ ̶s̶o̶m̶e̶ ̶i̶n̶p̶u̶t̶"̶)̶ Use VBA.Interaction.InputBox("give me some input"). You can do this in addition to the first point. Documentation here.
3) Compare with vbNullString rather than "" . See here. Essentially, you will generally want to do this as vbNullString is, as described in that link, faster to assign and process and it takes less memory.
Sub GetInput()
Dim ss As Worksheet
Dim Link As String
Set ss = Worksheets("ss")
Link = VBA.Interaction.InputBox("give me some input")
ss.Range("A1").Value = Link
' With ss ''commented out as not sure how this was being used. It currently serves no purpose.
If Link <> vbNullString Then
MsgBox Link
End If
' End With
End Sub
EDIT: To quote #Mat's Mug:
[In the OP's code, what is actually being called is] VBA.Interaction.InputBox, but the call is shadowed by the procedure's identifier "InputBox", which is causing the error. Changing it to Application.InputBox "fixes" the problem, but doesn't invoke the same function at all. The solution is to either fully-qualify the call (i.e. VBA.Interaction.InputBox), or to rename the procedure (e.g. Sub DoSomething(), or both.
Sub InputBox()
That procedure is implicitly Public. Presumably being written in a standard module, that makes it globally scoped.
Link = InputBox("give me some input")
This means to invoke the VBA.Interaction.InputBox function, and would normally succeed. Except by naming your procedure InputBox, you've changed how VBA resolves this identifier: it no longer resolves to the global-scope VBA.Interaction.InputBox function; it resolves to your InputBox procedure, because VBAProject1.Module1.InputBox (assuming your VBA project and module name are respectively VBAProject1 and Module1) are always going to have priority over any other function defined in any other referenced type library - including the VBA standard library.
When VBA resolves member calls, it only looks at the identifier. If the parameters mismatch, it's not going to say "hmm ok then, not that one" and continue searching the global scope for more matches with a different signature - instead it blows up and says "I've found the procedure you're looking for, but I don't know what to do with these parameters".
If you change your signature to accept a String parameter, you get a recursive call:
Sub InputBox(ByVal msg As String)
That would compile and run... and soon blow up the call stack, because there's a hard limit on how deep the runtime call stack can go.
So one solution could be to properly qualify the InputBox call, so that the compiler knows exactly where to look for that member:
Link = VBA.Interaction.InputBox("give me some input")
Another solution could be to properly name your procedure so that its name starts with a verb, roughly describes what's going on, and doesn't collide with anything else in global scope:
Sub TestInputBox()
Another solution/work-around could be to use a similar function that happens to be available in the Excel object model, as QHarr suggested:
Link = Application.InputBox("give me some input")
This isn't the function you were calling before though, and that will only work in a VBA host that has an InputBox member on its Application class, whereas the VBA.Interaction.InputBox global function is defined in the VBA standard library and works in any VBA host.
A note about this:
If Link <> "" Then
This condition will be False, regardless of whether the user clicked OK or cancelled the dialog by "X-ing out". The InputBox function returns a null string pointer when it's cancelled, and an actual empty string when it's okayed with, well, an empty string input.
So if an empty string needs to be considered a valid input and you need to be able to tell it apart from a cancelled inputbox, you need to compare the string pointers:
If StrPtr(Link) <> 0 Then
This condition will only be False when the user explicitly cancelled, and will still evaluate to True if the user provided a legit empty string.

How do I pass a range obj variable to a sub in Excel VBA (2016) [duplicate]

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...

VBA error in saving file using Format (date) function

I'm trying to save an activeworkbook but when I use the following code, I keep getting the error "compile error: expected function or variable" with the word "format" highlighted.
It boggles my mind because I used the exact same function and format in another macro and it saved the file perfectly. I also made sure they had the same types of variables defined already...
Here's the one line code
ActiveWorkbook.SaveAs Filename:=SavedPath & format(Date, "mmddyyyy") & " 4512 GLUpload.xlsm"
The variable savedpath is fine because when I run this line without the format part, it saves the file, but not sure why this screw it up. Also noticed in my other code, format is capitalized but it's not here.
The compiler error you are getting indicates that VBA is expecting an assignable value (either a literal, a variable, or the return value of a function). This means that one of the identifiers in the statement to the right of the equals sign doesn't fall into those categories. So, either SavedPath is defined somewhere as Sub SavedPath(), or there is a Sub Format(arg1, arg2) defined somewhere (if it had a different number of arguments you would get a "Wrong number of arguments or invalid property assignment" error). The second clue (in the comments) is that changing format to the strongly typed Format$ gave a "Type-declaration character does not match declared data type" error. This points to the compiler not treating the symbol format as a function call (Format$() is the strongly typed version of Format()). The solution is to track down the errant use of the VBA function name and re-name it.
A perfect example of why avoiding VBA keywords and function names is good practice.

VB script issue - Bitlocker status into file named with computer name

I need to adjust the below script to get Bitlocker status to a text file that should have the computer name as for its filename. So for a PC named Station10 the script would output a text file called Station10_enabled.txt. Or, if Bitlocker is disabled, it would create a file called Station10_disabled.txt.
I have below code, which seems to make sense, but is not working.
The code produces the error "Can not use parentheses when calling a sub", how can I fix that?
I also fear once the code runs the %computername% variable will not work or create a syntax error, but I can't test because of above issue.
If anyone out there knows how to gracefully handle what I am trying to do, please help.
strComputer = "."
Set objShell = CreateObject("Wscript.Shell")
strEnvSysDrive = objShell.ExpandEnvironmentStrings("%SystemDrive%")
Set objWMIServiceBit = GetObject("winmgmts:\\" & strComputer & "\root\CIMV2 \Security\MicrosoftVolumeEncryption")
Set colItems = objWMIServiceBit.ExecQuery("SELECT * FROM Win32_EncryptableVolume",,48)
For Each objItem in colItems
If objItem.DriveLetter = strEnvSysDrive Then
strDeviceC = objItem.DeviceID
DriveC = "Win32_EncryptableVolume.DeviceID='"&strDeviceC&"'"
Set objOutParams = objWMIServiceBit.ExecMethod(DriveC, "GetProtectionStatus")
If objOutParams.ProtectionStatus = "1" Then
My.Computer.FileSystem.WriteAllText("C:\cos\.txt","Bitlocker is enabled.",True)
Else
My.Computer.FileSystem.WriteAllText("C:\cos\test.txt","Bitlocker is disabled.",True)
End if
End If
Next
VB is a bit picky about the round brackets:
Argument lists for Function calls must be specified if you use the function result.
Argument lists for Sub calls must not be specified. The same holds true for argument lists of function calls if you do not use the function result.
The pitfall is that you usually get away if you call a sub (or function, and don´t use its result) that expects exactly one parameter, and specify that parameter in round brackets. You might believe that since this works, you may (or must) specify round brackets. This is untrue, because in that case, the round brackets can be (and are) taken as part of the expression that is evaluated to deliver the value to be passed as an actual paarmeter. That is why writing
My.Computer.FileSystem.WriteAllText("C:\cos\test.txt","Bitlocker is disabled.",True)
is syntactically invalid, while
AnyRoutineThatAcceptsOneArgument (Value)
is okay, but exactly the same as
AnyRoutineThatAcceptsOneArgument Value
If you don´t understand that, consider
AnyRoutineThatAcceptsOneArgument ((((((((Value))))))))
which is legal, but full of useless (redundant) brackets.
So the only two context where there are round brackets around routine arguments required is:
Calls to functions that expect exactly one argument, and that use the
function result.
In contrast to that, for Sub calls or function calls that don´t use the function result, you are not allowed to specify the brackets. The fact that in one special case, which is the routine call with only one argument, you are allowed to specify as many round brackets as you want has nothing to do with this. It just is an expression, and you can use as many (superfluous) brackts as you want.