VBA: Two methods of seeing if a file is open - vba

I've got two methods which I feel should tell if a file is open or not.
Method 1 (Error: Subscript out of range):
If Not Workbooks(filename) Is Nothing Then
Workbooks.Open (filename)
End If
Method 2:
If Not IsWorkbookOpen(filename) Then
Workbooks.Open (filename)
End If
Where IsWorkbookOpen() is:
Private Function IsWorkbookOpen(wbname) As Boolean
Dim wBook As Workbook
Set wBook = Nothing
On Error Resume Next
Set wBook = Workbooks(wbname)
If wBook Is Nothing Then
IsWorkbookOpen = False
Else: IsWorkbookOpen = True
End If
End Function
Aside from On Error Resume Next, Method 1 appears to be nearly the same as Method 2.
Could anyone please explain why Method 1 gives the error it does?
Thank you.

They both give a subscript out of range error. But in Method 2 you suppress that error with On Error Resume Next.
Sub SeeError()
On Error Resume Next
Debug.Print Workbooks("DoesNotExist").Name
Debug.Print Err.Description
End Sub
This prints "Subscript Out of Range" in the Immediate Window. The On Error statement doesn't stop the error from occurring, it just handles it. Method 1 doesn't handle errors so the default error handling (stop execution and report the error) is in effect.

VBA tries to evaluate all the parts before it evaluates the conditional statement. So if I have a variable myvar = "xyz" and try to run the following lines...
If IsNumeric(myvar) And Round(myvar, 1) = 3 Then
'you will get an error before the IF is evaluated
End If
it will not work. VBA will evaluate IsNumeric(myvar) fine, then try to evaluate Round(myvar, 1) = 3 and get an error before it checks the entire conditional. So VBA will let you know about the error before it performs the AND operator. If VBA had short circuit evaluation, it would work fine since the first part would evaluate to false.
But the following will work
If IsNumeric(myvar) Then
If Round(myvar, 1) = 3 Then
'second IF statement is not touched since first IF statement evaluates to false
End If
End If
This works because IsNumeric(myvar) evaluates to false and therefore skips the nested statement.
So the error it throws on the Workbooks(filename) will just give the error unless you tell it to resume next. So the method I use is
On Error Resume Next
Set wb = Workbooks(file)
If wb Is Nothing Then
Set wb = Application.Workbooks.Open(dir & "\" & file, ReadOnly:=True)
End If
On Error GoTo 0
Edited to give more detail and correctly capture that the second example will not be evaluated as well as provide a useful solution for the question at hand.

Workbooks(filename) tries to get the element with the identifier (or 'index') filename from the collection Workbooks. If the collection does not contain such an element, you'll get the "Subscript out of range" error. (So in a nutshell: This code will fail whenever the file is not open. You probably don't want this.)
However, the knowledge that such an access will fail if the file is not open, i.e. an error is raised, is being made use of in the second method. The code tries to get the element with the identifier filename from the Workbooks collection and to assign it to the variable wBook. If it fails, the value of the variable wBook will stay Nothing. If it succeeds, the variable wBook will contain a reference to the respective Workbook object.

Related

Excel VBA Run-time error 438 first time through code

I'm a novice self-taught VBA programmer knowing just enough augment Excel/Access files here and there. I have a mysterious 438 error that only popped up when a coworker made a copy of my workbook (Excel 2013 .xlsm) and e-mailed it to someone.
When the file is opened, I get a run time 438 error when setting a variable in a module to a ActiveX combobox on a sheet. If I hit end and rerun the Sub, it works without issue.
Module1:
Option Private Module
Option Explicit
Public EventsDisabled As Boolean
Public ListBox1Index As Integer
Public cMyListBox As MSForms.ListBox
Public cMyComboBox As MSForms.Combobox
Public WB As String
Sub InitVariables()
Stop '//for breaking the code on Excel open.
WB = ActiveWorkbook.Name
Set cMyListBox = Workbooks(WB).Worksheets("Equipment").Listbox1
Set cMyComboBox = Workbooks(WB).Worksheets("Equipment").Combobox1 '//438 here
End Sub
Sub PopulateListBox() '//Fills list box with data from data sheet + 1 blank
Dim y As Integer
If WB = "" Then InitVariables
ListBox1Index = cMyListBox.ListBoxIndex
With Workbooks(WB).Worksheets("Equipment-Data")
y = 3
Do While .Cells(y, 1).Value <> ""
y = y + 1
Loop
End With
Call DisableEvents
cMyListBox.ListFillRange = "'Equipment-Data'!A3:A" & y
cMyListBox.ListIndex = ListBox1Index
cMyListBox.Height = 549.75
Call EnableEvents
End Sub
...
PopulateListBox is called in the Worksheet_activate sub of the "Equipment" sheet.
All my code was in the "Equipment" sheet until I read that was bad form and moved it to Module1. That broke all my listbox and combobox code but based on the answer in this post I created the InitVariables Sub and got it working.
I initially called InitVariables once from Workbook_open but added the If WB="" check after WB lost its value once clicking around different workbooks that were open at the same time. I'm sure this stems from improper use of Private/Public/Global variables (I've tried understanding this with limited success) but I don't think this is related to the 438 error.
On startup (opening Excel file from Windows Explorer with no instances of Excel running), if I add a watch to cMyComboBox after the code breaks at "Stop" and then step through (F8), it sets cMyComboBox properly without error. Context of the watch does not seem to affect whether or not it prevents the error. If I just start stepping or comment out the Stop line then I get the 438 when it goes to set cMyComboBox.
If I add "On Error Resume Next" to the InitVariables then I don't error and the project "works" because InitVariables ends up getting called again before the cMyComboBox variable is needed and the sub always seems to work fine the second time. I'd rather avoid yet-another-hack in my code if I can.
Matt
Instead of On Error Resume Next, implement an actual handler - here this would be a "retry loop"; we prevent an infinite loop by capping the number of attempts:
Sub InitVariables()
Dim attempts As Long
On Error GoTo ErrHandler
DoEvents ' give Excel a shot at finishing whatever it's doing
Set cMyListBox = ActiveWorkbook.Worksheets("Equipment").Listbox1
Set cMyComboBox = ActiveWorkbook.Worksheets("Equipment").Combobox1
On Error GoTo 0
Exit Sub
ErrHandler:
If Err.Number = 438 And attempts < 10 Then
DoEvents
attempts = attempts + 1
Resume 'try the assignment again
Else
Err.Raise Err.Number 'otherwise rethrow the error
End If
End Sub
Resume resumes execution on the exact same instruction that caused the error.
Notice the DoEvents calls; this makes Excel resume doing whatever it was doing, e.g. loading ActiveX controls; it's possible the DoEvents alone fixes the problem and that the whole retry loop becomes moot, too... but better safe than sorry.
That said, I'd seriously consider another design that doesn't rely so heavily on what appears to be global variables and state.

Determining if a cell is linked to a QueryTable via VBA in Excel

I'm linking an Excel worksheet to a data source using QueryTables which works well. I'd like to introduce logic to check a given cell and determine if that cell is the top left corner of an existing QueryTable. This seemed trivial in concept, but has proven elusive in practice. If I try to check any properties of the cell that relate to QueryTable I get an error. i.e. rng.querytable throws error 1004 if rng does not link to a QueryTable. So I messed around with testing if rng.ListObject is nothing based on some discussions I found online. It turns out that rng.ListObject is something if the cell is a table even if that table is not a QueryTable. So no dice there.
So how do I test if a target cell contains a QueryTable?
Here's a stub of my function I got from Mr. Excel:
Public Function IsRangeInQueryTable(rngCheck As Range) As Boolean
Dim QT As QueryTable
For Each QT In rngCheck.Parent.QueryTables
If Not Intersect(rngCheck, QT.ResultRange) Is Nothing Then
IsRangeInQueryTable = True
Exit Function
End If
Next QT
'If it got to here then the specified range does not lie
'in the result range of any query
IsRangeInQueryTable = False
End Function
The function above works in many cases, but if I have QueryTables that have become detached from their destination (which seems to happen in practice) the code throws error 1004 because the QueryTable has no destination. Here's what the watch shows when the QueryTable becomes decoupled from a destination:
FWIW, I also tried approaching this the reverse way and iterating through every QueryTable in a worksheet. It turns out if there's a QueryTable which has had its destination deleted, asking that QueryTable for its QueryTable.Destination throws an app error. So I could not figure out a reliable way to do that approach either.
This is wordier than it needs to be, but because the "OnError" statement redirects all errors, I want to eliminate the possibility of accidentally handling the wrong error, for example if rng was Nothing.
Public Function cell_has_query(rng As Range) As Boolean
If rng Is Nothing Then
cell_has_query = False
Exit Function
End If
If rng.ListObject Is Nothing Then
cell_has_query = False
Exit Function
End If
On Error GoTo ErrHandler
If Not rng.ListObject.QueryTable Is Nothing Then
cell_has_query = True
End If
Exit Function
ErrHandler:
If Err.Number = 1004 Then 'Application-Defined or Object-Defined Error
cell_has_query = False
Else
On Error GoTo 0
Resume
End If
End Function
#JDLong VBA error handling is odd. The default setting for errors is On Error GoTo 0, which means that (depending upon your VBA IDE settings; Tools-->Options-->General) will pop up a message box for unhandled errors. If you want to explicitly catch and handle errors, you create a label (e.g. "ErrHandler") and then make sure that section of code is unreachable normally by ending the function with Exit Function. In the block of code after the label, you can inspect the Err object properties and choose to Resume to retry the line of code that caused the error, Resume Next to run the line following the one that errored, or simply handle the error and let the function exit normally. You can also reraise the error by setting the mode back to On Error GoTo 0 and then Resumeing the line.
for some reason that is not clear to me, my querytables are not in listobjects. I thought all querytables are in listobjects, but I've run enough tests to demonstrate to myself that mine are not. So I slightly edited #blackhawk's function to this:
Public Function cell_has_query(rng As Range) As Boolean
If rng Is Nothing Then
cell_has_query = False
Exit Function
End If
On Error GoTo ErrHandler
If Not rng.QueryTable Is Nothing Then
cell_has_query = True
End If
Exit Function
ErrHandler:
If Err.Number = 1004 Then 'Application-Defined or Object-Defined Error - this throws if there is a querytable with no destination
cell_has_query = False
Else
On Error GoTo 0
Resume
End If
End Function

How to check the availability of a worksheet

I have to run a set of code related to worksheet "wins", but only if that worksheet exist.
Please share a code to check the availability of sheet "wins". If worksheet "wins" exist, then only I want to run that set of code, else I want to skip executing that set of code and move to next line of code.
You could use On Error Resume Next to skip the errror which occurs if you try access a not existing worksheet and assigning it to a object variable. So if the worksheet does not exist, no error occurs but the variable is Nothing. If the worksheet exists, then the variable is not Nothing.
Example:
Sub test()
Dim wsWins As Worksheet
On Error Resume Next
Set wsWins = ActiveWorkbook.Worksheets("wins")
On Error GoTo 0
If Not wsWins Is Nothing Then
MsgBox "Worksheet wins exists."
Else
MsgBox "Worksheet wins does not exist."
End If
End Sub
Axel's answer will work nicely. Some people prefer not to use error throwing to test if something exists. If you're one of them then I use the following quite a lot in a Utility module. It'll work for Worksheets, Charts, etc. (basically anything that's a collection with a 'Name' property):
Public Function ExcelObjectExists(testName As String, excelCollection As Object) As Boolean
Dim item As Object
On Error GoTo InvalidObject
For Each item In excelCollection
If item.Name = testName Then
ExcelObjectExists = True
Exit Function
End If
Next
ExcelObjectExists = False
Exit Function
InvalidObject:
MsgBox "Developer error: invalid collection object passed in ExcelObjectExists."
ExcelObjectExists = False
End Function
You can call it like this:
If ExcelObjectExists("wins", ThisWorkbook.Worksheets) Then

Why does assigning a reference in my spreadsheet sometimes work and sometimes not?

I have a few cells in my excel workbook which are available for a client to put his own values. I wanted the workbook to initialize those cells with default values. In order to do so I have a worksheet "Arkusz do makr", where I store the values.
In a module "GM" I declare a variable to reference my worksheet easier like this:
Public M As Worksheet
Then I initialize this variable and set my default values like this (in ThisWorkbook):
Private Sub Workbook_Open()
Set M = Worksheets("Arkusz do makr")
Worksheets("Values").Range("Value1") = M.Range("Value1")
Worksheets("Values").Range("Value2") = M.Range("Value2")
Worksheets("Values").Range("Value3") = M.Range("Value3") `etc
End Sub
Now sometimes this works like a charm, and sometimes, when I open the workbook I get a
Run-time error '91': Object variable or With block variable not set.
Could someone please explain this behaviour to me? Additionally I would like to ask if my approach makes sense, since I have a hard time grasping the order of events in excel as well as the range of its objects.
EDIT: Additionally I should mention that the Debug function highlights the first Worksheets... line in my code. In specific worksheets I reference the M object as well, though I thought it changes anything here...
Try to change the code of this Sub like below.
I have added a simple error handling - if there is no worksheet "Arkusze do makr" or "Values" in your workbook, warning message is displayed and default values are not copied.
You can find more comments in code.
Private Sub Workbook_Open()
Dim macrosSheet As Excel.Worksheet
Dim valuesSheet As Excel.Worksheet
'------------------------------------------------------------------
With ThisWorkbook
'This command is added to prevent VBA from throwing
'error if worksheet is not found. In such case variable
'will have Nothing as its value. Later on, we check
'the values assigned to those variables and only if both
'of them are different than Nothing the code will continue.
On Error Resume Next
Set macrosSheet = .Worksheets("Arkusz do makr")
Set valuesSheet = .Worksheets("Values")
On Error GoTo 0 'Restore default error behaviour.
End With
'Check if sheets [Values] and [Arkusz do makr] have been found.
'If any of them has not been found, a proper error message is shown.
'In such case default values are not set.
If valuesSheet Is Nothing Then
Call VBA.MsgBox("Sheet [Values] not found")
ElseIf macrosSheet Is Nothing Then
Call VBA.MsgBox("Sheet [Arkusz do makr] not found")
Else
'If both sheets are found, default values are copied
'from [Arkusz do makr] to [Values].
'Note that if there is no Range named "Value1" (or "Value2" etc.)
'in any of this worksheet, another error will be thrown.
'You can add error-handling for this case, similarly as above.
With valuesSheet
.Range("Value1") = macrosSheet.Range("Value1")
.Range("Value2") = macrosSheet.Range("Value2")
.Range("Value3") = macrosSheet.Range("Value3")
End With
End If
End Sub

Microsoft VBA idiom (Visio) For Testing Non-Existence of a property?

I need to ensure a Macro which works on Visio 2003 doesn't cause problems on lower versions of Visio: specifically because I'm writing to a property which doesn't exist on lower versions of Visio. Currently I'm doing this:
...
On Error GoTo NoComplexScriptFont:
Set cellObject = shapeObject.Cells("Char.ComplexScriptFont")
On Error GoTo ErrHandler
...
NoComplexScriptFont:
Rem MSGBOX only for debug
MsgBox "No Such Property"
GoTo endsub
ErrHandler:
Rem put in general error handling here
GoTo endsub
endsub:
End Sub
...
Which works, but its a little messy I think. I have toyed with the idea of using 'Application.version' (Which returns '11' for Visio 2003), but I would like to avoid assumptions about what properties are available in any particular release and just test for the property itself.
What's the nice proper idiom for doing this in VBA ?
Thanks
--- Got a few answers below, my preferred solution was this one:
If shapeObject.CellExists("Char.ComplexScriptFont", 0) Then
msgbox "Property exists"
else
msgbox "Property does not exist"
end if
I would use a wrapper function for accessing the property so that you don't mess up your normal error handling, like this:
...
Set cellObject = GetCellObject(shapeObject)
If Not cellObject Is Nothing Then
' Do something with cellObject
End If
...
Private Function GetCellObject(ByVal shapeObject As Object) As Object
On Error Resume Next
Set GetCellObject = shapeObject.Cells("Char.ComplexScriptFont")
End Function
(Note: I'm only using Object above because I don't know what type cellObject etc. is)
I often use the same technique even for properties that I know do exist, but which will raise an error under certain circumstances. For example (Excel), if I'm going to access a worksheet by name (which will raise an error if no such worksheet exists), then I'll have a wrapper function that calls Worksheets(name) and either returns a Worksheet object or Nothing:
Private Function GetWorksheet(ByVal strName as String) As Worksheet
On Error Resume Next
Set GetWorksheet = Worksheets(strName)
End Function
This makes for much cleaner calling code, since you can simply test the return value rather than worrying about error handling.
You can use the CellExists property of the shape object to see if a particular cell exists. You have to pass in the localeSpecificCellName, which you already seem to be using, and then you pass in an integer fExistsLocally, which specifies the scope of the search for the cell; if you specify 0 then the CellExists will return true if the cell is inherited or not...if it's 1 then CellExists will return false if the cell is inherited.