VBA: Access to registry while preprocessing - vba

I want to conditionally compile code in one VBAproject, with a condition that depends on some registry-entry. Is this somehow possible in VBA?
I know that there are some simple preprocessing possibilites in VBA, but I can not see if it is possible to somehow access the registry while preprocessing. Or Maybe some other possibility to check the registry before compiling.
Since I get a compile error because of some missing reference(and thus missing class object), I aim to check the registry before compiling.
P.s. I only want to read registry-entries.
As an example. How to reach debug.print in the following, i.e. avoiding compile errors.
sub sub1()
dim testobj as new nonexistingobject
sub2 testobj
debug.print "Arrived at this point"
end sub
sub sub2( byref testobj as nonexistingobject)
*do some stuff with testobj*
end sub

Instead of using early binding (Dim obj as myObject), use late bindings with CreateObject. This way you will be able to handle the case where the object doesn't exists:
Sub test()
Dim obj As Object
On Error Resume Next
obj = CreateObject("myObject")
if Err then Exit Sub 'if the object wasn't found exit the function
on error goto 0 'set back the error handling to its previous state
'rest of the code
End Sub

VBA does not directly provide access to the entire Windows Registry, but you can use a workaround.
Dim RegObj as Object
Set RegObj = CreateObject("WScript.Shell")
RegObj.RegDelete RegKeyString
RegObj.RegWrite RegKeyString
Str = RegObj.RegRead RegKeyString
Set RegObj = Nothing
If the RegKeyString is not found, it'll throw an error, so you need some OnError -> Key Doesn't Exist kind of code.
Not sure of the preprocessing part, but you could run your code in the Workbook_Open event handler to make sure this part runs before anything else.

It is not feasible. All the symbols and conditions that are tested by VBA Directives are build from literals or expressions that include only operators (excepting the Is operator, I think).
Function calls are not allowed when declaring directive symbols and conditions, and the only way to access registry in VBA is via API (like WScript.Shell for example), which means function call.
Further reading: http://msdn.microsoft.com/en-us/library/tx6yas69.aspx

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.

VBA Error 424 Object Required on call to sub in Workbook_Open

I am very new to vba. I am getting a 424 Ojbect required error when I open the document. I tried using Call subName but that made no difference.
Please let me know what else I can send or explain.
Code found in ThisWorkbook:
Private Sub Workbook_Open()
Sheet1.Worksheet_Activate//<- Object Required
Sheet2.Worksheet_Activate
Sheet3.Worksheet_Activate
Sheet4.Worksheet_Activate
Sheet5.Worksheet_Activate
Sheet6.Worksheet_Activate
Sheet7.Worksheet_Activate
...
Code found in Sheet1:
'INITIAL SETUP OF THE SHEET
Public Sub Worksheet_Activate()
If Sheet9.Range("B1").Text = "TRUE" Then
'RUN INIT FUNCTIONS HERE
Me.initReqLink
Me.initVersion
Me.initCbApplicaiton
Sheet9.Range("B1").Text = "FALSE"
End If
End Sub
Thank you in advance.
Just change it to Sheet1.Activate and that event will be called.
You shouldn't be invoking event handlers manually, but that's not why you're having that error; by making the method Public you've exposed it onto Sheet1's interface, making these calls perfectly "legal" - so the methods do get invoked, no doubt.
Go to Tools / Options / General / Error Trapping, and set it to Break on Unhandled Errors.
Then you should have a better idea of exactly which statement is blowing up, most likely located somewhere in one of these:
Me.initReqLink
Me.initVersion
Me.initCbApplicaiton
..which you haven't posted.
Side note, Call doesn't make a difference, because it's an obsolete syntax that hasn't had a purpose since the advent of the implicit call syntax way back when. You never need an explicit Call statement. Never.

Missing VBA compiler message for wrong method name

Consider the following code:
Public Sub VBACompilerIsMad()
Dim Ap As Application
Dim Wb As Workbook
Dim Ws As Worksheet
Debug.Print Ap.XXX ' No compile error
Debug.Print Wb.XXX ' No compile error
Debug.Print Ws.XXX ' Compile error
End Sub
When I compile this, I get a compiler error for referring to an inexisting member of Worksheet. However, if I comment out the last line, there is no compiler error, even though neither Application nor Workbook have a method or property XXX. It is as if I declared Ap and Wb as Object variables.
Why does the compiler treat Application / Workbook differently from Worksheet?
Are there any other classes like this, that the compiler seems to treat as if they were Object?
As I have been explained (kudos go respectively), this is a COM feature.
By default COM assumes an interface is extensible, that is, it allows adding members at run time. If that is not the desired behaviour, one can apply the [nonextensible] attribute to the interface definition, which declares the interface only accepts methods explicitly defined in the type library.
dispinterface _Application and dispinterface _Workbook do not have this flag set in the Excel type library, dispinterface _Worksheet does.
Similarly, ADO's dispinterface _Connection does not have [nonextensible], dispinterface _Command does.
To learn which are extensible, add a reference to TypeLib Info in the project's References and run:
Dim t As tli.TLIApplication
Set t = New tli.TLIApplication
Dim ti As tli.TypeLibInfo
Set ti = t.TypeLibInfoFromFile("excel.exe")
Dim i As tli.InterfaceInfo
For Each i In ti.Interfaces
If (i.AttributeMask And tli.TYPEFLAG_FNONEXTENSIBLE) <> tli.TYPEFLAG_FNONEXTENSIBLE Then
Debug.Print i.Name
End If
Next
You will see that almost all interfaces are extensible here, so most of them get pushed out of the debug window and you will only see the last ones. Change the <> to = to print those that are not extensible, there are much less of them.
A bit of a hypothesis:
You can call a stored procedure on an ADODB.Connection object like a native method (at the bottom).
(The examples for this on several msdn sites look oddly messed up).
So there is some mechanism like 'anonymous/dynamic methods' in VBS/VBA.
It may be a similar mechanism activated here for Application and Workbook classes - although I don't see where and how exactly.
A test supports the basic idea:
I have tested this with a reference to Microsoft ActiveX Data Objects 2.8 Library:
Public Sub testCompiler()
Dim cn As ADODB.Connection
Dim cmd As ADODB.Command
Debug.Print cn.XXX
Debug.Print cmd.XXX
End Sub
cn.XXX does not throw a compile error, cmd.XXX does.
GSerg's answer is indeed outstanding, I love the whole COM type library IDL and how some attributes there can govern the behaviour in the Excel VBA IDE. Long may this arcane knowledge of COM be handed down! And, I realise this question has been bountied to give that answer more rep but when a bounty is set it appears on my radar and I have a view on this matter.
So although GSerg's answer gives the mechanism it does not give the rationale, i.e. it gives the how but not the why. I'll attempt to answer the why.
Some of the answer why is already given by Martin Roller (OP) in his comments about Application and WorksheetFunction. This, to me, is a convincing reason to keep Application extensible and I'll not consider Application further.
Let us turn to Workbook and Worksheet and we best start with some code to demonstrate, so you will need to begin with two fresh workbooks, call them MyWorkbook.xlsm and OtherWorkbook.xlsm. So some instructions:
In OtherWorkbook.xlsm go the code module ThisWorkbook and paste the code
Option Explicit
Public Function SomeFunctionExportedOffOtherWorkbook() As String
SomeFunctionExportedOffOtherWorkbook = "Hello Matt's Mug!"
End Function
In MyWorkbook.xlsm go the Sheet1 code module and paste the code
Option Explicit
Public Function SomeFunctionExportedOffCodeBehindSheet1() As String
SomeFunctionExportedOffCodeBehindSheet1 = "Hello Martin Roller!"
End Function
Now, in the VBA IDE change the codename of Sheet1 to codebehindSheet1
Now, in a new standard module in MyWorkbook.xlsm add the following code
Sub TestingObjectLikeInterfacesOfWorkbookAndCodeBehindWorksheet_RunMany()
'* For this example please rename the 'CodeName' for Sheet1 to be "codebehindSheet1" using the IDE
Debug.Assert ThisWorkbook.Worksheets.Item("Sheet1").CodeName = "codebehindSheet1"
Dim wb As Workbook
Set wb = Application.Workbooks.Item("OtherWorkbook")
'* Workbook dispinterface needs to not marked with nonextensible attribute
'* so that it doesn't trip up over exported function in another workbook
'* below SomeFunctionExportedOffOtherWorkbook is defined in the ThisWorkbook module of the workbook "OtherWorkbook.xlsm"
Debug.Print wb.SomeFunctionExportedOffOtherWorkbook
'*Not allowed --> Dim foo As Sheet1
'*have to call by the 'code behind' name which is usually Sheet1 but which we changed to illustrate the point
Debug.Print codebehindSheet1.SomeFunctionExportedOffCodeBehindSheet1
End Sub
Now run this code above.
You've probably read the code and hopefully understood the point I'm making but let me spell it out. We need Workbook to remain extensible because it may contain a reference to another workbook which may be exporting a method or function and we'd like no compile errors.
However, for the Worksheet, to do a similar export we again add code to the code behind module but there is a difference in referencing the module: one grabs a reference to that code behind module by using its VBA code name, most people do not change this from Sheet1 (that is why you were invited to change it above).
So the interface obtained by the code behind module name needs to extensible and not the Excel.Worksheet interface.
P.S. Anyone got a copy of TLI.dll?
As a workaround it could still be possible to create your own interface and implement this interface. Then declare a variable as INewInterface and all the compiler messages will be there :). Here simple example with custom interface for a UserForm. HTH
Interface
Public CancelButton As MSForms.CommandButton
Public DataList As MSForms.ListBox
Public CommandBox As MSForms.TextBox
Implementation
Implements IMyForm
Private Property Set IMyForm_CancelButton(ByVal RHS As MSForms.ICommandButton)
End Property
Private Property Get IMyForm_CancelButton() As MSForms.ICommandButton
End Property
Private Property Set IMyForm_CommandBox(ByVal RHS As MSForms.IMdcText)
End Property
Private Property Get IMyForm_CommandBox() As MSForms.IMdcText
End Property
Private Property Set IMyForm_DataList(ByVal RHS As MSForms.IMdcList)
End Property
Private Property Get IMyForm_DataList() As MSForms.IMdcList
End Property
Usage
Note: MyForm is existing VBA Form which has been added to the project.

Can I use late binding to check the existence of a library before using it via early binding?

I like to use early binding in my VBA projects, since I like the auto-complete of method names, etc. during development. I also like the confidence of knowing that the compiler will warn me if I've mis-spelled a method name.
However, to use early binding I need to add a reference to the relevant library (for example, the "Microsoft Scripting Runtime"). That's fine for "standard" libraries like that, but sometimes I want to use a library that may or may not be present on the user's machine.
Ideally, I'd like to display a useful message if the library is not present (such as "xyz is not installed on this computer, and so this feature cannot be used"). If I was using only late binding, then I could do this:
Dim o As Object
Set o = CreateObject("foo", "bar")
If o Is Nothing Then
MsgBox "nope"
End If
But, if I've added a reference to the library in order to use early binding, then if the library is not present I get a compile error when my VBA project is loaded. Thus, none of the code runs (including the code to detect the non-existence of the library).
Is there any way around this catch-22?
You could create a class module as a proxy for an object library --- wrap all the methods, properties, and constants needed by your other procedures.
All those procedures would use the proxy class the same way, so you wouldn't need to revise those procedures to switch between early and late binding. And Intellisense would show you everything you expose with the proxy class.
The class would be a single point of control to switch between early and late binding. You mentioned Excel as one example:
#Const DevStatus = "PROD"
#If DevStatus = "DEV" Then
Private objApp As Excel.Application
Private objBook As Excel.Workbook
Private objSheet As Excel.Worksheet
#Else 'assume PROD
Private objApp As Object
Private objBook As Object
Private objSheet As Object
#End If
If there is a possibility Excel may not be installed on any users' machines, you can check its availability during class initialize.
Dim blnExcelAvailable As Boolean
Private Sub Class_Initialize()
blnExcelAvailable = IsExcelAvailable()
End Sub
Private Function IsExcelAvailable() As Boolean
Dim blnReturn As Boolean
Dim objTest As Object
On Error GoTo ErrorHandler
Set objTest = CreateObject("Excel.Application")
blnReturn = True
ExitHere:
On Error GoTo 0
Set objTest = Nothing
IsExcelAvailable = blnReturn
Exit Function
ErrorHandler:
blnReturn = False
GoTo ExitHere
End Function
Then your procedures which use the proxy class could check a property to see whether Excel is available.
Public Property Get ExcelAvailable() As Boolean
ExcelAvailable = blnExcelAvailable
End Property
I think this approach is possible, and it satisfies your requirements AFAICT. However, I'm unsure whether it's reasonable. Back to the example of Excel, you could do something like this for a manageable subset of its object model. But if you need all or most of its methods, properties, and constants, the proxy class would be a huge undertaking.
Personally I wouldn't use this approach. It's less work for me to manage early/late binding as mwolfe02 and JP. described. However my impression is this is more burdensome in your situation, so perhaps you're willing to invest more effort than I am in something like this.
Not really.
However, one way I've dealt with this in development is to have two separate declaration lines. I comment one or the other depending on whether I am doing dev work or releasing to production. You can leave everything else alone (including the CreateObject line) and then you just need to remember to switch the commented line and add/remove the reference itself.
For example:
Dim o As foo.bar 'Comment out for production'
'Dim o As Object ''Comment out for dev work'
Set o = CreateObject("foo", "bar")
If o Is Nothing Then
MsgBox "nope"
End If