why is a string variable strVar not the same as cStr(strVar)? - vba

Why does a string variable need to be enclosed in a cStr() conversion to be used as a key string for the CreateObject("WScript.Shell").SpecialFolders collection?
Demonstration of this absurdity(?):
Sub sdfgdfsg()
Const strCon_SpecialFolderName As String = "MyDocuments"
Dim strVar_SpecialFolderName As String
strVar_SpecialFolderName = "MyDocuments"
Debug.Print CreateObject("WScript.Shell").SpecialFolders(strCon_SpecialFolderName) ' CORRECT
Debug.Print CreateObject("WScript.Shell").SpecialFolders(strVar_SpecialFolderName) ' WRONG! (it returns the Desktop path instead)
Debug.Print CreateObject("WScript.Shell").SpecialFolders(CStr(strVar_SpecialFolderName)) ' CORRECT
Debug.Print CreateObject("WScript.Shell").SpecialFolders("MyDocuments") ' CORRECT
End Sub
I read the documentation about the index argument for a collection without finding an answer.

The WScript library was designed to be used from scripting languages such as VBScript which use Variants. The SpecialFolders.Item that you are calling expects a Variant containing a string, too.
The result you are seeing appears to come from the fact that the library is not able to read the Variant-wrapped string value VB passes, and does something wrong instead. You can achieve the same result with
Dim strVar_SpecialFolderName As String
strVar_SpecialFolderName = vbNullString
'Returns C:\Users\Public\Desktop
Debug.Print CreateObject("WScript.Shell").SpecialFolders(strVar_SpecialFolderName)
For reasons I don't fully understand, there sometimes is a problem with passing Variant-wrapped data from VBA to an external object. I speculate that it may have something to do with the presence or absence of the VT_BYREF flag in the Variant that VB(A) produces, and it produces it differently for constants, local variables and temporaries.
I believe this to be a problem on the receiving side, not in VBA.
Workarounds include:
Declaring the variable as Variant to begin with:
Dim strVar_SpecialFolderName As Variant
strVar_SpecialFolderName = "MyDocuments"
Debug.Print CreateObject("WScript.Shell").SpecialFolders(strVar_SpecialFolderName)
Forcing an evaluation which turns the local variable into a temporary (that is what your CStr does, too) which apparently changes how VB packs it into a Variant:
Dim strVar_SpecialFolderName As String
strVar_SpecialFolderName = "MyDocuments"
Debug.Print CreateObject("WScript.Shell").SpecialFolders((strVar_SpecialFolderName))

Related

VBA Option Explicit fails to detect undeclared variables in Function

I have always used Option Explicit in each module. I never gave it much thought, until now.
Code:
Option Explicit
Function ParseJSON(ByVal strJSON As String) As String
strJSON = "New String"
'MsgBox (strJSON)
ParseJSON = strJSON
End Function
Sub Test()
Dim strJSON As String
strJSON = "Old String"
MsgBox (ParseJSON(strJSON))
MsgBox (strJSON)
End Sub
This piece of code is just a test and has nothing to do with JSON. When I run the code, I expected it to throw an error as strJSON is never declared in ParseJSON, and it should be a new variable as the original one is passed ByVal and thus cannot be changed, the last MsgBox() confirms this.
Is there anything that I didn't get? My hunch points to the ByVal part., or maybe Option Explicit only checks Sub?
the point of Option Explicit is to have you explicitly declare all variables you're using, and that's what actually happens in Function ParseJSON(ByVal strJSON As String) As String: that strJSON As String is declaring variable strJSON you're going to use inside the function (and it's also declaring it as of String type)
then, you're also giving it a value passed by the calling sub, and that ByVal simply means that whatever value the function strJSON variable is going to assume it won't affect the calling sub variable (if any, and that may be incidentally named after strJSON, but it's distinct from function strJSON) you passed the value of
that's why if you try
Function ParseJSON(ByVal strJSON As String) As String
strJSON = Range("A1:A3")
'MsgBox (strJSON)
ParseJSON = strJSON
End Function
you'll get a run time type mismatch error as soon as the strJSON = Range("A1:A3") line is processed
and that's why if you try
Sub Test()
Dim strJSON As String
strJSON = "Old String"
MsgBox (ParseJSON(Range("A1:A3")))
MsgBox (strJSON)
End Sub
you'll get the same run time type mismatch error as soon as the MsgBox (ParseJSON(Range("A1:A3"))) line is processed
You ARE declaring properly.
In the Test function you declare it as a string with the DIM statement.
And by making it an argument in the function you are also declaring it as a string, for use in the function.
Because the function uses it byVal, and changes you make IN the function, will not affect the value of the string.
It you want to change the value of it, it has to be passed byRef.
ByRef effectively passed the actual variable. - It could be known by a different name even, but any changes will also change the original variable.
byVal is effectively passing a copy of the variable, which you can play about with but not change the original.
Imagine as a piece of paper.
byRef you hand over the actual piece of paper to be marked up
byVal you hand over a photocopy, but keep the original to yourself.

Conversion From String To Integer Not Valid Error But No Numbers Or Integer Types Specified

I'm new to VB and I'm hoping someone can help with the first major problem I've encountered.
I've created a form, which:
Allows me to specify a SearchString in a text box
To specify a FolderPath using FolderBrowserDialog in a text box
Pass the values in the text boxes as variables
Return all files in the FolderPath, containing the SearchString, with wildcards, to a ListBox when a button is clicked.
The code behind the button is as follows:
Private Sub ListButton_Click(sender As Object, e As EventArgs) Handles ListButton.Click
Dim fls
Dim FolderPath As String
Dim SearchString As String
FolderPath = FolderPathBox.Text
SearchString = SearchStringBox.Text
fls = My.Computer.FileSystem.GetFiles(FolderPath,"*" & SearchString & "*")
For Each f As String In fls
MatchingFilesBox.Items.Add(f)
Next
End Sub
However, after populating the SearchString and FolderPath text boxes with the following values respectively:
(1)
C:\Backup\Files
and clicking the button, the following error is returned:
Additional information: Conversion from string "* (1)*" to type 'Integer' is not valid.
The same error is displayed even if I do not specify a number e.g. "an" and I've not specifically configured any text boxes or classes or variables as data type integer.
I've simplified the code by removing the variables and wildcards from the equation and hard coding the path and a file name:
'fls = My.Computer.FileSystem.GetFiles(FolderPath,"*" & SearchString & "*")
fls = My.Computer.FileSystem.GetFiles("C:\Backup\Files", "abandoning.docx")
But the same error on converting to data type integer is displayed:
Additional information: Conversion from string "abandoning.docx" to type 'Integer' is not valid.
I'm flummoxed as to why or how an integer is being passed or retrieved in the file path. I've searched for answers to the error, but the articles I've read relate to number values, while mine doesn't; or to empty textboxes, which I believe I've eliminated; or use Replace, which I'm not.
Can anyone offer any guidance on overcoming this issue, so I can return all files in a folder containing a specific string in the filename?
You are passing in the incorrect number of parameters. The second parameter is supposed to be an enumeration of which is always a number. The last parameter is your wild card which can be a string. Try this:
fls = My.Computer.FileSystem.GetFiles("C:\Backup\Files", SearchOption.SearchTopLevelOnly "*.docx")
Look here for the reference as to what you are to pass in to the GetFiles() function:
https://msdn.microsoft.com/en-us/library/t71ykwhb(v=vs.90).aspx

Conditional guard IF Not (x = Empty) Then

I have the following
Public Sub BreakAllLinks(ByRef aWkBook As Excel.Workbook)
Dim Link As Variant
Dim myLinks As Variant
myLinks = aWkBook.LinkSources(Type:=Excel.xlLinkTypeExcelLinks)
If Not (myLinks = Empty) Then
For Each Link In myLinks
aWkBook.BreakLink Name:=Link, Type:=Excel.xlLinkTypeExcelLinks
Next Link
End If
End Sub 'BreakAllLinks
If myLinks is empty then it works well and avoids the For Each loop but if myLinks contains some links then I get the following error
Runtime error '13'
What is wrong with If Not (myLinks = Empty) Then?
LinkSources returns either Empty or an array.
You cannot compare an array with Empty using the equality operator.
The documentation shows you the right way of checking the result - using the IsEmpty function.
The function succeeds regardless of the value type stored in your Variant.
If e.g. you had a Nothing in there, you would get error 91.
Or, if you had an object reference in there, your comparison would try to fetch the default property of the stored object and compare that to Empty.
Which is why you should never check for = Empty really, and only use IsEmpty.

Reference to Window Object in VBA

I have a problem referencing a windows object in VBA. it throws the following error: "Error 5 (Invalid procedure call or argument). I cannot find the cause, because I see no programming error.
Public Sub TestWindowhandle()
Dim lResult As Long
Dim objShell, wins, winn
Dim IE_Count As Long, i As Long, This_PID As Long
On Error GoTo TestWindowhandle_Error
Set objShell = CreateObject("Shell.Application")
Set wins = objShell.Windows
IE_Count = wins.Count
For i = 0 To (IE_Count - 1)
Set winn = wins.Item(i)
Next i
On Error GoTo 0
Exit Sub
TestWindowhandle_Error:
MsgBox "Error " & Err.Number & " (" & Err.Description & ") in line " & Erl & " in procedure TestWindowhandle of Module Module1"
Stop
End Sub
Something odd with that interface, it seems to only work with a copy of the control variable so:
Set winn = wins.Item(i + 0)
or
Set winn = wins.Item((i))
I believe here is what is happening.
The Item method accepts a Variant parameter.
When calling an external method that accepts a Variant parameter, VB likes to create and pass Variants that provide the value by reference - that is, with the VT_BYREF flag set.
However VB does not set this flag when sending out intermediate results (temporaries, not stored in a variable), which makes sense because even if the called method updates the value, no one will be able to see it.
So when you call .Item(i), VB sends out a Variant of type VT_I4 | VT_BYREF, and when you call .Item(i + 0), a Variant of type VT_I4 is dispatched, without the VT_BYREF.
In most situations the difference is not significant because VARIANT-aware methods should be able to cope with either. However this particular method does different things depending on exactly which VT_ it receives, and because of this it is explicitly willing to reject any VT_s other than the three accepted ones. So in order to call it you need to trick VB into removing the byref flag.
Interestingly, when you declare the variable as Variant, VB still dispatches a VT_VARIANT | VT_BYREF, but the method seems to support this situation and correctly resolves to the pointed-to inner Variant that has the non-reference type of VT_I4.
Note this has nothing to do with VB's ByVal/ByRef - this is about the internal structure of the VARIANT data type.

How do I search an ActiveX/COM object for a method?

I have an ActiveX/COM DLL. It contains many methods and properties. I would like to be able to ask it if it has a particular symbol, as per the following snippet:
If HasMethod( "StdLib.DLL", "ReadFileE" ) Then
...
End If
Is there a way to do this from, say, VBScript or JScript? If not, where do I go to get the information I need?
After Googling around not quite finding what I wanted, I remembered the Edanmo site which got me thinking about TLBINF32.DLL, downloading Microsoft's TLBINF32.CHM and reading up on GetMembersWithSubStringEx. Below is the implementation of it (done in VB6 with a reference to TLBINF32.DLL), some demo VBScript and output, and the wrapping of that functionality in some VBA.
Public Function SearchTLIMethodsAndProperties(sTypelib As Variant, sSymbol As Variant) As Variant
Dim SI As SearchItem
Dim aResults As Variant
Dim bFound as boolean
Dim Groups(1) As InvokeKinds
Groups(0) = INVOKE_FUNC Or INVOKE_PROPERTYGET Or _
INVOKE_PROPERTYPUT Or INVOKE_PROPERTYPUTREF
ReDim aResults(0)
bFound = False
With TypeLibInfoFromFile(sTypelib)
.SearchDefault = tliStClasses Or tliStEvents
For Each SI In .GetMembersWithSubStringEx(sSymbol, Groups)
bFound = True
arr.AAdd_PostIncrement aResults, SI.Name
Next
End With
if bFound then
ReDim Preserve aResults(UBound(aResults) - 1)
end if
SearchTLIMethodsAndProperties = aResults
End Function
VBScript demo. The above code was included in my StdLib DLL in the Registry coclass.
Dim O, R
Set O = CreateObject("Std.Registry")
Set R = CreateObject("Std.Arrays")
WScript.Echo R.ShowStructure( O.SearchTLIMethodsAndProperties( "MSSCRIPT.OCX",""))
Output from the demo (script was run in SciTE).
>cscript "C:\foo\foo.vbs"
{Add,AddCode,AddObject,AllowUI,Clear,CodeObject,Column,Count,Description,Error,Eval,ExecuteStatement,HasReturnValue,HelpContext,HelpFile,Item,Language,Line,Modules,Name,NumArgs,Number,Procedures,Reset,Run,SitehWnd,Source,State,Text,Timeout,UseSafeSubset}
>Exit code: 0
Finally, the VBA code. A cell has a symbol in it and this routine finds it or returns an error string.
Public Function LookupSymbol(sSym As String) As String
Dim aRes As Variant
aRes = reg.SearchTLIMethodsAndProperties("MSSCRIPT.OCX", sSym)
Dim i As Integer
LookupSymbol = "!!NotFound!!"
For i = 0 To UBound(aRes)
If LCase$(aRes(i)) = LCase$(sSym) Then
LookupSymbol = sSym
Exit For
End If
Next
End Function
Looking back on it now, I think I might pass in the path to the DLL/OCX as the first parameter.
I have used Microsofts interactive OLE/COM-Object viewer to find mehods and their parameters in ActiveX-DLLs. Maybe looking at the source code of the viewer will lead you in the right direction: MSDN OleView sample
If you want to do it programmatically - I'm not aware of a simple way to do that. Anyway, if you really need to (and if your programming language is capable enough) - you can query the type library (refer to ITypeLib description somewhere at http://msdn.microsoft.com/en-us/library/ms221549.aspx).
Also, if you already have an IDispatch pointer - you might consider using its services to dynamically enumerate methods supported by the interface (refer to IDispatch description in MSDN).