VBA: Set variable to “Empty” or “Missing”? Handling multiple optional arguments?

时光怂恿深爱的人放手 提交于 2019-12-24 14:19:49

问题


Is it possible, or desirable, to set objects/data to an "Empty" or "Missing" variant? I want to be able to conditionally pass optional arguments to a function. Sometimes I want to use an optional argument, sometimes I don't.

In Python, you could easily pass through whichever optional arguments you wanted by using **kwdargs to unpack a dictionary or list into your function arguments. Is there something similar (or a way to hack it in VBA) so you can pass in Empty/Missing optional arguments?

In particular, I'm trying to use Application.Run with an arbitrary number of arguments.

EDIT:

I'm basically trying to do this:

Public Function bob(Optional arg1 = 0, Optional arg2 = 0, Optional arg3 = 0, Optional arg4 = 0)
    bob = arg1 + arg2 + arg3 + arg4
End Function

Public Function joe(Optional arg1)
    joe = arg1 * 4
End Function


Public Sub RunArbitraryFunctions()
    'Run a giant list of arbitrary functions pseudocode

    Dim flist(1 To 500)
    flist(1) = "bob"
    flist(2) = "joe"
    flist(3) = "more arbitrary functions of arbitrary names"
    flist(N) = ".... and so on"

    Dim arglist1(1 To 4)        'arguments for bob
    Dim arglist2(1 To 1)        'arguments for joe 
    Dim arglist3(1 To M number of arguments for each ith function)


    For i = 1 To N
        'Execute Application.Run,
        'making sure the right number of arguments are passed in somehow.
        'It'd also be nice if there was a way to automatically unpack arglisti
        Application.Run flist(i) arglisti(1), arglisti(2), arglisti(3), ....
    Next i

End Sub

Because the number of arguments changes for each function call, what is the acceptable way to make sure the right number of inputs are input into Application.Run?

The equivalent Python code would be

funclist = ['bob', 'joe', 'etc']
arglists = [[1,2,3],[1,2],[1,2,3,4,5], etc]

for args, funcs in zip(arglists, funclist):
    func1 = eval(funcs)
    output = func1(*args)

回答1:


in VBA you use ParamArray to enter option inputs to functions.

See Pearson Material




回答2:


There are two ways in which a routine can change the number of arguments that has to be provided to it:

  • declare some of the trailing arguments as Optional
  • declare the last argument as ParamArray

A single routine can use either or both.


An Optional parameter may have a strict type (e.g. Optional s As String), but then it will be impossible to detect whether it was passed. If you don't pass a value for such argument, the correct flavour of "blank" will be used, which is indistinguishable from passing that blank value manually.
So, having Public Sub Bob(Optional S As String), you cannot detect from inside of Bob whether it was called as Bob or as Bob vbNullString.
An optional parameter may have a default value, which suffers from the same problem. So, having Public Sub Bob(Optional S As String = "Default Value"), you cannot detect if Bob was called as Bob or as Bob "Default Value".

To be able to truly detect whether an optional parameter was passed, they have to be typed as Variant. Then a special function, IsMissing, can be used inside the routine to detect if a parameter was passed.

Public Sub Bob(Optional a, Optional b, Optional c, Optional d)
    Debug.Print IsMissing(a), IsMissing(b), IsMissing(c), IsMissing(d)
End Sub
Bob 1, , 3  ' Prints False, True, False, True

ParamArray can only be the last argument, and it allows an infinite* number of arguments to be passed starting from this position. All these arguments arrive packed in a single Variant array (no option for static typing here).

The IsMissing function does not work on the ParamArray argument (always returns False). The way to know how many arguments were passed is to compare UBound(args) with LBound(args). Note that this only tells you how many argument "slots" were used, but some of them can be in fact missing!

Public Sub BobArray(ParamArray a())
    Dim i As Long
    For i = LBound(a) To UBound(a)
        Debug.Print IsMissing(a(i)), ;
    Next
    Debug.Print
End Sub
BobArray                 ' Prints empty line (the For loop is not entered due to UBound < LBound)
Sheet1.BobArray 1, 2, 3  ' Prints False, False, False
Sheet1.BobArray 1, , 3   ' Prints False, True, False

Note that you cannot pass "missing" value for the trailing arguments of the ParamArray, i.e. this is illegal:

Sheet1.BobArray 1, , 3,   ' Does not compile

However, you can work around this using the trick described below.


An interesting use case that you touch in your question is preparing an array of all arguments in advance, passing it to the function, filling all the arguments "placeholders", but still expecting the function to detect that some of the arguments are missing (not passed).

Normally this is not possible, because if anything is passed (even "blank" values, such as Empty, Null, Nothing of vbNullString), then it still counts as passed, and IsMissing() will return False.

Fortunately, the special Missing value is nothing but a specially constructed Variant, and even without knowing how to construct that value manually, we can trick the compiler to give it away:

Public Function GetMissingValue(Optional ByVal IgnoreMe As Variant) As Variant
    If IsMissing(IgnoreMe) Then
        GetMissingValue = IgnoreMe
    Else
        Err.Raise 5, , "I told you to ignore me, didn't I"
    End If
End Function
Dim missing As Variant
missing = GetMissingValue()

Dim arglist1(1 To 4) As Variant
arglist1(1) = 42
arglist1(2) = missing
arglist1(3) = missing
arglist1(4) = "!"

Bob arglist1(1), arglist1(2), arglist1(3), arglist1(4)   ' Prints False, True, True, False

Now, we can work around the inability to pass "missing" to the trailing "slots" of ParamArray:

Dim arglist1(1 To 4) As Variant
arglist1(1) = 42
arglist1(2) = missing
arglist1(3) = missing
arglist1(4) = missing

BobArray arglist1(1), arglist1(2), arglist1(3), arglist1(4) ' Prints False, True, True, True

Note, however, that this workaround will only work if you call BobArray directly. If you use Application.Run, it will not work because the Run method will discard any trailing "missing" arguments before passing them onto the called routine:

Dim arglist1(1 To 4) As Variant
arglist1(1) = 42
arglist1(2) = missing
arglist1(3) = missing
arglist1(4) = missing

Application.Run "BobArray", arglist1(1), arglist1(2), arglist1(3), arglist1(4)
' Prints False, because only one argument is passed



回答3:


So addressing the question of optionally using arguments it looks like my question in Calling vba macro from python with unknown number of arguments, check it out accordingly.

Hence: Using Python:

def run_vba_macro(str_path, str_modulename, str_macroname, **kwargs):
    if os.path.exists(str_path):
        xl=win32com.client.DispatchEx("Excel.Application")
        wb=xl.Workbooks.Open(str_path, ReadOnly=0)
        xl.Visible = True
        if kwargs:
                params_for_excel = list(kwargs.values())
                xl.Application.Run(os.path.basename(str_path)+"!"+str_modulename+'.'+str_macroname,
                                          *params_for_excel,)
        else:
                xl.Application.Run(os.path.basename(str_path)
                                          +"!"+str_modulename
                                          +'.'+str_macroname)
        wb.Close(SaveChanges=0)
        xl.Application.Quit()
        del xl

#example
kwargs={'str_file':r'blablab'}
run_vba_macro(r'D:\arch_v14.xlsm',
              str_modulename="Module1",
              str_macroname='macro1',
              **kwargs)
#other example
kwargs={'arg1':1,'arg2':2}
run_vba_macro(r'D:\arch_v14.xlsm',
              str_modulename="Module1",
              str_macroname='macro_other',
              **kwargs)

Using VBA:

Sub macro1(ParamArray args() as Variant)
    MsgBox("success the str_file argument was passed as =" & args(0))
End Sub

Sub macro_other(ParamArray args() as Variant)
    MsgBox("success the arguments have passed as =" & str(args(0)) & " and " & str(args(1)))
End Sub


Also another use case only using VBA is here for reference. It is a question that has not been answered and is around for long, although recently it was updated by the community server automatically with some good ideas related links accordingly.

Here is an answer you can do it if you use this:

Sub pass_one()
    Call flexible("a")
End Sub
Sub pass_other()
    Call flexible("a", 2)
End Sub
Sub flexible(ParamArray args() As Variant)
    Dim i As Long
    MsgBox ("I have received " & _
            Str(UBound(args) + 1) & _
            " parameters.")
    For i = 0 To UBound(args)
        MsgBox (TypeName(args(i)))
    Next i
End Sub

Only for developers that also use Python:
If you are using Python's kwargs, simply starr expression and pass a Python tuple. Here it is (it is related with my question in Calling vba macro from python with unknown number of arguments)
Cheers.



来源:https://stackoverflow.com/questions/26025187/vba-set-variable-to-empty-or-missing-handling-multiple-optional-arguments

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!