If you find this page useful, please consider making a quick and secure donation (powered by PayPal) to keep it free!
 
 

Knowledge Base/VBA/General

 

From The Thalesians

Jump to: navigation, search

Contents

Built-in volatile functions

Here is the complete list of built-in volatile functions in Excel:

  • CELL
  • INDIRECT
  • INFO
  • OFFSET
  • NOW
  • RAND
  • TODAY

Converting a date to string

To convert a date to string in Excel, you can use the TEXT function:

=TEXT(NOW(),"yyyymmdd")

The Immediate Window

Despite (or, perhaps, in virtue of) its simplicity, diagnostic output remains a useful debugging tool. In languages such as Perl this was, for a while, the only debugging tool. VBA Editor has a fully functional integrated debugger, yet it does not obviate the need for diagnostic output.

What are the options?

First, we have message boxes:

MsgBox "Variable sFoo = " & sFoo

By default these are modal, i.e. they interrupt the execution of the program. Message boxes also require the programmer to press the "OK" button, which can seriously slow down debugging.

Second, we have Excel worksheets. Suppose that DebugSheet is a possibly hidden worksheet that you use for debugging output. Then you could do this:

DebugSheet.Range("A1").Value = sFoo

Is this any more convenient than message boxes? Probably not. Our DebugSheet is a nuisance. We need to hide it from the users. It increases the complexity of our workbook solution. Finally, while we are debugging, we need to keep looking at it. It's often useful to see the diagnostic output in the order in which it was produced. So we need to iterate through a range in DebugSheet... It only gets worse.

But we have another option: the Immediate Window. In the VBA Editor (press Alt-F11 in Excel to invoke it) press Ctrl-G. The immediate window will be displayed. Now you can write to it as follows:

Debug.Print "Variable sFoo = " & sFoo

The output will be added to the immediate console. It solves all the problems that I have mentioned above. Also note that it is unnecessary to hide the Immediate Window from the user (and alter your code before productionising the spreadsheet). The Immediate Window will only be visible in the VBA Editor — not in Excel itself.

In everything that follows I assume that you are aware of the Immediate Window and know how to display it and write to it. There are plenty of Debug.Prints in the code examples.

Renaming modules in VBA Editor

Go to the VBA Editor (Alt-F11). Make sure the Project Explorer is visible (Ctrl-R); it presents a tree view of the project. Make sure the Properties Window is visible (F4).

Select the module you wish to rename in Project Explorer. It will be under "VBAProject (<filename.xls>)", "Modules". You will see its "(Name)" in the Properties Window. Edit it and press Enter.

Your module has been renamed.

Speeding up the execution of VBA code in Excel

There are several things that could be done to make the code run faster.

  • Hiding the VBE Editor via Application.VBE.MainWindow.Visible may improve the performance in some cases.
  • Setting Application.Calculation controls the recalculation of the cells while executing code. Setting it to false should speed up the execution of the code that changes the cell values.
  • The Application.ScreenUpdating property controls most display changes on the monitor while a procedure is running. When this property is set to false, toolbars remain visible and Excel can still use the status bar prompts, input boxes, dialog boxes and message boxes to input and output the data. Setting this property to false can speed up the execution in many cases.
  • Disabling event handling may improve the performance, but this may delay the execution of the scheduled OnTime events and have other undesirable consequences. Application.EnableEvents should therefore be used with care.
  1. Private Sub FastSub()
  2. Dim originalVBEMainWindowVisible, _
  3. originalScreenUpdating, _
  4. originalEnableEvents As Boolean
  5. Dim originalCalculation As Integer
  6.  
  7. originalVBEMainWindowVisible = Application.VBE.MainWindow.Visible
  8. originalCalculation = Application.Calculation
  9. originalScreenUpdating = Application.ScreenUpdating
  10. originalEnableEvents = Application.EnableEvents
  11.  
  12. Application.VBE.MainWindow.Visible = False
  13. Application.Calculation = xlCalculationManual
  14. Application.ScreenUpdating = False
  15. Application.EnableEvents = False
  16.  
  17. ' Your code here
  18.  
  19. Application.EnableEvents = originalEnableEvents
  20. Application.ScreenUpdating = originalScreenUpdating
  21. Application.Calculation = originalCalculation
  22. Application.VBE.MainWindow.Visible = originalVBEMainWindowVisible
  23. End Sub

Generating #N/A values in VBA

Unfortunately, there is no such thing as Excel.WorksheetFunction.NA in VBA. So how can one generate the #N/A values in VBA, the same as you get when using the Excel formula =NA()?

You can use the following function:

  1. Function NaN() As Variant
  2. NaN = CVErr(xlErrNA)
  3. End Function

Note that the function's return type is Variant, not Double.

Checking if a value is #N/A in VBA

Fortunately, there is an Excel.WorksheetFunction.IsNA in VBA. Therefore it is straightforward to check if a value is #N/A. You can wrap this check into the following function:

  1. Function IsNaN(ByVal x As Variant) As Boolean
  2. IsNaN = Excel.WorksheetFunction.IsNA(x)
  3. End Function

We can also use it to test our NaN function. The following returns True:

IsNaN(NaN())

Checking if a dynamic array has been redimmed

A typical definition of a static array looks as follows:

Dim someStaticArr(1 To 10) As String
</pre>
 
This array is static since its size has been specified in the <tt>Dim</tt> statement that declares it; its size can never be changed.
 
'''Dynamic arrays''' are created as follows:
 
<code lang="vb">
Dim arr() As String

Notice that the size has not been specified, hence the array is dynamic. The array can be "redimmed" ("sized" is the right word) later on:

ReDim Preserve arr(5) As String

Before a dynamic array is "redimmed" it is known as empty or unallocated. If you try executing something like UBound on arr before it has been "redimmed" an error will result.

Thus you may need to check if the dynamic array has been "redimmed". Unfortunately, IsEmpty(arr) will return true, so this is not what we need. This is achieved as follows:

If (Not arr) = -1 Then ' It is empty

Since this code is not particularly intuitive, I prefer to wrap it into a function:

  1. Public Function IsUnallocated(ByRef arr() As Variant) As Boolean
  2. If (Not arr) = -1 Then
  3. IsUnallocated = True
  4. Else
  5. IsUnallocated = False
  6. End If
  7. End Function

There is a subtle problem with this function. If you are trying to call it on some v of type Variant (which may be a Variant(), but may be something else), VBA will not execute the function. It will come up with a "Type mismatch" message. However, chances are your variable is declared as Variant, not as Variant() even though it is, in fact, a Variant() or a Double(), etc. What to do?

In these cases you can use a (messier) function defined as follows:

  1. Public Function IsUnallocatedArray(ByRef arr As Variant) As Boolean
  2. If IsArray(arr) And Not IsRange(arr) Then
  3. Dim u As Long
  4.  
  5. On Error GoTo ErrorHandler
  6.  
  7. u = UBound(arr)
  8. End If
  9.  
  10. IsUnallocatedArray = False
  11.  
  12. Exit Function
  13.  
  14. ErrorHandler:
  15.  
  16. IsUnallocatedArray = True
  17. End Function

You can feed it pretty much anything you like; IsUnallocatedArray will return True if and only if its parameter is an unallocated array.

The IsRange function is defined elsewhere on this page. This check is necessary since if arr is a Range, then IsArray(arr) is True while UBound(arr) produces an error.

Determining the number of dimensions of an array

The following function returns the number of dimensions of a given array (1 for one-dimensional arrays, 2 for two-dimensional arrays, etc.):

  1. Public Function ArrayDimCount(ByRef arr As Variant)
  2. Dim d, errorCheck As Integer
  3.  
  4. On Error GoTo DimensionDetermined
  5.  
  6. ' VBA arrays can have up to 60000 dimensions
  7. For d = 1 To 60000
  8. errorCheck = LBound(arr, d)
  9. Next d
  10. DimensionDetermined:
  11. ArrayDimCount = d - 1
  12. End Function

Functions and procedures with variable arguments

VBA supports functions and procedures with variable arguments (variable arity functions and procedures) through parameter arrays introduced by the ParamArray keyword. For example:

  1. Function MultiplySum(factor As Integer, ParamArray params() As Variant)
  2. Dim total, p
  3. total = 0
  4. For Each p In params
  5. total = total + p
  6. Next
  7. MultiplySum = factor * total
  8. End Function

You don't need to know at design time how many arguments have been passed to MultiplySum. If you call MultiplySum as

=MultiplySum(10, 1, 2, 3, 4, 5)

the result will be 150, i.e. 10 * (1 + 2 + 3 + 4 + 5).

Functions and procedures with optional arguments

Functions and procedures with optional arguments can be defined using the Optional keyword:

  1. Sub Hello(ByVal name As String, Optional ByVal surname As String)
  2. MsgBox "Hello " & name & " " & surname
  3. End Sub

This can be called as Hello "Paul", "Bilokon" and Hello "Paul".

If the caller does not provide a value for an optional argument, it is automatically initialised in the same way it would be if it were a variable - String arguments are initialised to zero-length strings (""), numeric arguments to zero (0), Boolean arguments to False, etc. You can override this behaviour by providing default values:

  1. Sub Hello(ByVal name As String, Optional ByVal surname As String = "Bilokon")
  2. MsgBox "Hello " & name & " " & surname
  3. End Sub

If called as Hello "Paul", this will display "Hello Paul Bilokon".

If you want to know for certain that an optional argument has not been supplied by the caller then that argument must have the type Variant. Only Variants can contain the value Missing. This is how you can check for a missing optional argument:

  1. Sub Hello1(ByVal name As String, Optional ByVal surname As Variant)
  2. If IsMissing(surname) Then
  3. MsgBox "No surname"
  4. Else
  5. MsgBox "Hello " & name & " " & surname
  6. End If
  7. End Sub

Calling Hello1 as Hello1("Paul") will result in a message box: "No surname".

Any arguments that follow an optional argument in an argument list must also be optional.

Simulating "pointers to functions" in VBA

VBA does not support the C-style pointers to functions. However, with some work it is possible to call a function by its name using the Application.Run method. There are a few points to bear in mind though.

Create a new workbook in Excel and save it as C:\Iteration.xls, say. Press Alt-F11 to open the VBA Editor. Make sure the Project Explorer is visible. It presents a tree-view of the project. If you can't see it on the left, press Ctrl-R.

In Project Explorer, right-click on "VBAProject (Iteration.xls)" and select Insert > Module. A new module, a "Module1", will appear. Enter the following code:

  1. Public Function Quadratic(x)
  2. Quadratic = x ^ 2
  3. End Function

We will now call this function by name.

In Project Explorer, under "VBAProject (Iteration.xls)", "Microsoft Excel Objects" right-click on "ThisWorkbook". Type in the following code:

  1. Public Sub Iterate()
  2. Dim x, y As Double
  3.  
  4. For x = 1 To 10
  5. y = Application.Run("Module1.Quadratic", x)
  6. MsgBox y
  7. Next
  8. End Sub

Try running Iterate by pressing F5 while the cursor is in the body of the method.

Notice that you could supply the name of the method as a variable:

  1. Public Sub Iterate()
  2. Dim x, y As Double
  3. Dim methodName As String
  4.  
  5. functionName = "Module1.Quadratic"
  6.  
  7. For x = 1 To 10
  8. y = Application.Run(functionName, x)
  9. MsgBox y
  10. Next
  11. End Sub

This method name could be a parameter of Iterate this is about as close as you can get to "function pointers" in VBA.

Subroutines may be invoked with Application.Run as well as functions. E.g. you can modify the code in "ThisWorkbook" as follows:

  1. Public Sub MyCallback()
  2. MsgBox "In MyCallback"
  3. End Sub
  4.  
  5. Public Sub CallIt()
  6. Application.Run ("ThisWorkbook.MyCallback")
  7. End Sub

Notes

Now the caveats:

  1. You must qualify MyCallback with "ThisWorkbook." when referring to it in CallIt, otherwise you will get "Run-time error '1004': Application-defined or object-defined error".
  2. If Quadratic were defined in "ThisWorkbook" rather than a module, "Module1", Application.Run would for no apparent reason ignore its output and y wouldn't be set.
  3. This was tested in Microsoft® Office Excel 2003 SP2.

"Complile error: Sub or Function not defined" when trying to call a procedure or function from another project

You are getting the "Compile error: Sub or Function not defined" when trying to call a procedure or function from another project, which could be an XLA (add-in) and may also be open in VBA Editor.

Go to the calling project in VBA Editor. Click on Tools > References. Make sure the project containing the procedure/function is ticked in the list of Available References. You may need to browse to the relevant XLA or DLL file.

Variant properties

Sometimes we may want to declare a property that can be set to both object reference and primitive values. However, here we face a hurdle:

  1. Private m_fooBarBaz As Variant
  2.  
  3. Public Property Get FooBarBaz() As Variant
  4. FooBarBaz = m_fooBarBaz
  5. End Property

This property definition will work if m_fooBarBaz is set to a value of a primitive type, such as Long but will fail miserably if it is set to an object reference ("Run-time error '438': Object doesn't support this property or method"). On the other hand,

  1. Private m_fooBarBaz As Variant
  2.  
  3. Public Property Get FooBarBaz() As Variant
  4. Set FooBarBaz = m_fooBarBaz
  5. End Property

will fail if m_fooBarBaz is, say, a Long ("Compile error: Object required").

How can we create truly polymorphic properties?

IsObject comes to the rescue. Here is our solution:

  1. Private m_fooBarBaz As Variant
  2.  
  3. Public Property Get FooBarBaz() As Variant
  4. If IsObject(m_fooBarBaz) Then
  5. Set FooBarBaz = m_fooBarBaz
  6. Else
  7. FooBarBaz = m_fooBarBaz
  8. End If
  9. End Property
  10.  
  11. Public Property Let FooBarBaz(ByVal newFooBarBaz As Variant)
  12. m_fooBarBaz = newFooBarBaz
  13. End Property
  14.  
  15. Public Property Set FooBarBaz(ByVal newFooBarBaz As Variant)
  16. Set m_fooBarBaz = newFooBarBaz
  17. End Property

We can test it as follows:

  1. Public Sub TestFooBarBaz()
  2. FooBarBaz = 5
  3. Debug.Print "We have " & FooBarBaz
  4.  
  5. FooBarBaz = "hello"
  6. Debug.Print "We have " & FooBarBaz
  7.  
  8. Set FooBarBaz = ThisWorkbook
  9. Debug.Print "We have " & FooBarBaz.Name
  10. End Sub

We get the following output in the Immediate window, as required:

We have 5
We have hello
We have QuaRT-Excel.xla

Distinguishing between the arrays and ranges

It is often possible to write polymorphic code that works on both the arrays and ranges. For Each comes in extremely useful. Here is an example of a function that may be passed an array as its parameter, but it will work equally well on a range:

  1. Public Function Numbers(ByRef r As Variant) As Variant()
  2. Dim result As New Stack
  3.  
  4. Dim c As Variant
  5. For Each c In r
  6. If Excel.WorksheetFunction.IsNumber(c) Then
  7. result.Push c
  8. End If
  9. Next c
  10. End Function

What happens when this polymorphism breaks? Arrays and ranges are in many ways different beasts. If x is a range, x.Count will work, UBound(x) will not. If it is an array, the situation is quite the opposite. In conclusion, we need a method for distinguishing between the two.

Unfortunately, VBA's IsArray returns True for ranges containing multiple cells. Thus calling

  1. Public Function IsArr(arr As Variant) As Boolean
  2. IsArr = IsArray(arr)
  3. End Function

as =IsArr(A1) from a worksheet will give FALSE as a result, while =IsArr(A1:A2) will return TRUE. IsArray is ignorant of the difference between arrays and ranges. However, we can define a function that will work as desired:

  1. Public Function IsRange(r As Variant) As Boolean
  2. On Error GoTo ErrorHandler
  3. IsRange = (TypeName(r) = "Range")
  4.  
  5. Exit Function
  6.  
  7. ErrorHandler:
  8.  
  9. IsRange = False
  10. End Function

We can call it from VBA or from a worksheet. =IsRange(A1:A2) and =IsRange(A1) are now true, =IsRange({1, 2, 3}) and =IsRange(123) are now false.

Now we can use it as follows:

  1. If IsRange(r) Then
  2. ' Range...
  3. ElseIf IsArray(r) Then
  4. ' Array...
  5. Else
  6. ' Scalar...
  7. End If

Using regular expressions in VBA

Regular expressions are a set of extremely powerful text processing techniques which were popularised in Perl but have recently found their way into almost all modern programming languages, in one form or another.

Although many people seem to be unaware of the fact, regular expressions are available in VBA via the Microsoft VBScript Regular Expressions 5.5 framework.

In order to use regular expressions in VBA, first open the VBA Editor (Alt-F11), select Tools > References, and in the list of available references tick Microsoft VBScript Regular Expressions 5.5.

You are now ready to use regular expressions in your code:

  1. Sub RegExTest()
  2. Dim s As String
  3.  
  4. s = " Bilokon , Paul, 1982-07-04"
  5.  
  6. Dim re As RegExp
  7. Set re = New RegExp
  8.  
  9. re.Pattern = "^\s*(\w+)\s*,\s*(\w+)\s*,\s*(\d{4})-(\d{2})-(\d{2})\s*$"
  10. re.Global = True
  11.  
  12. Dim matches As MatchCollection
  13. Set matches = re.Execute(s)
  14.  
  15. Dim m As Match
  16. Set m = matches(0)
  17.  
  18. Debug.Print "Name: " & m.SubMatches(1)
  19. Debug.Print "Surname: " & m.SubMatches(0)
  20. Debug.Print "Born: " & m.SubMatches(2)
  21. End Sub

This will produce the following output:

Name: Paul
Surname: Bilokon
Born: 1982

For more information on regular expressions please read this.

Obtaining the formatted text of a cell

Press Alt-F11. In Visual Basic Editor, create a new module and type in the following function:

  1. Public Function CellText(r As Range)
  2. CellText = r.Text
  3. End Function

Enter the value 357 in A1. Click on this cell, then right-click and select "Format Cells..." Select "Currency" and click "OK". The cell will be formatted as "£357.00" (say).

Suppose you want to use "£357.00" in a formula. E.g. you may want to concatenate it:

="Value: " & A1

But you get "Value: 357", not "Value: £357.00". To get the latter, use the following:

=CellText(A1)

Given a certain range on Sheet x, obtaining the "same" range on Sheet y

Say you have

  1. Dim r As Range
  2. r = Sheet1.Range("A1")

r is now "forever" associated with Sheet1, as you can easily check:

Debug.Print r.Parent.Name

This prints "Sheet1" (provided that's the name of Sheet1). r.Parent is a read-only property. You cannot change it.

Suppose you want to "move" r to Sheet2 while preserving the cell addresses. Is this achievable?

The answer is, you cannot achieve this by changing r. You have to construct a new Range object.

Let us define the following function:

  1. Public Function SameRangeOnSheet(r As Range, sh As Worksheet) As Range
  2. Set SameRangeOnSheet = sh.Range(r.Address)
  3. End Function

Now we can achieve our goal by simply calling

  1. Dim s As Range
  2. Set s = SameRangeOnSheet(r, Sheet2)

This method works for ranges containing multiple areas, such as

$A$1:$A$5,$C$1:$C$5

Hiding gridlines

To hide the gridlines, go to Tools > Options and untick "Gridlines" under "Window Options". Although it may appear that this setting pertains to your Excel application, it actually affects the workbook. So if you distribute it to your users, they won't see the gridlines either.

Unhiding all worksheets quickly

In general, you would use Format > Sheet > Unhide... to unhide an Excel sheet. Unfortunately, you can only unhide the sheets one by one using this method. To unhide all sheets quickly, you can use the following macro:

  1. Sub UnhideSheets()
  2. Dim s As Worksheet
  3.  
  4. Application.ScreenUpdating = False
  5.  
  6. For Each s In ActiveWorkbook.Sheets
  7. s.Visible = xlSheetVisible
  8. Next
  9.  
  10. Application.ScreenUpdating = True
  11. End Sub

Renaming a chart object in Excel

By default, chart object will have non-informative names, such as "Chart 14". To rename an embedded chart object, select any cell, hold Shift, and click anywhere on the chart. Release Shift. The chart is now surrounded by white resizing handles. Type the new name in the Name Box (right above cell A1) and press Enter. The chart has been renamed.

Checking that the value entered in a combo box appears on the combo box' list

Use the following:

  1. If SomeComboBox.ListIndex > -1 Then
  2. ' OK, the entered/chosen value appears on the list
  3. Else
  4. ' The user has entered a value that is not on the list
  5. End If

Using a form with a Cancel button to enable the user to interrupt a long calculation

Your spreadsheet needs to perform an operation that takes minutes. Hours perhaps. You would like to enable the user to interrupt this operation. So you create a UserForm with a "Cancel" button. Let's call it CancelButton. You write a Click handler for this function button, like so:

  1. Private Sub CancelButton_Click()
  2. Cancelled = True
  3. End Sub

Cancelled is a property. We have defined it in our UserForm like so:

  1. Private m_bCancelled As Boolean
  2.  
  3. Public Property Get Cancelled() As Boolean
  4. Cancelled = m_bCancelled
  5. End Property
  6.  
  7. Public Property Let Cancelled(ByVal bCancelled As Boolean)
  8. m_bCancelled = bCancelled
  9.  
  10. If bCancelled Then
  11. CancelButton.Caption = "Cancelled..."
  12. CancelButton.Enabled = False
  13. Else
  14. CancelButton.Caption = "Cancel"
  15. CancelButton.Enabled = True
  16. End If
  17.  
  18. Repaint
  19. End Property

So far so good. Now we can check whether the user has cancelled the execution or not by querying the Cancelled property.

So let us write our calculation code (of course, it won't be in our UserForm, it will be in another module):

  1. Public Sub PerformCalculation()
  2. CancelForm.Show vbModeless
  3.  
  4. Dim lIteration As Long
  5.  
  6. For lIteration = 1 To 100000
  7. If CancelForm.Cancelled Then
  8. MsgBox "Cancelled by user"
  9. Exit For
  10. End If
  11.  
  12. ' ... Perform a very slow calculation here ...
  13. Next lIteration
  14. End Sub

Here we are assuming that our UserForm is called CancelForm. Try running this code. Does it work?

This depends on the nature of the "very slow calculation". In most cases, the answer is no. Most calculations won't yield the execution to enable the operating system to process the events. The user will see the CancelForm but he will be unable to click on the "Cancel" button. The mouse cursor will be the hourglass. All the clicks over the "Cancel" button will be ignored.

There is a solution: DoEvents(). Let's add this line to PerformCalculation:

  1. Public Sub PerformCalculation()
  2. CancelForm.Show vbModeless
  3.  
  4. Dim lIteration As Long
  5.  
  6. For lIteration = 1 To 100000
  7. If CancelForm.Cancelled Then
  8. MsgBox "Cancelled by user"
  9. Exit For
  10. End If
  11.  
  12. ' ... Perform a very slow calculation here ...
  13.  
  14. DoEvents
  15. Next lIteration
  16. End Sub

Now it works — the user's clicks are no longer ignored. What happened?

DoEvents passes control to the operating system. Control is returned after the operating system has finished processing the events in its queue and all SendKeys events have been executed. This includes the events that take care of the user's clicks.

Displaying a UserForm defined in another project

According to XL97: Unable to Show UserForms in Other Projects, "there is no way to directly show a UserForm that is contained in another Microsoft Visual Basic for applications project".

There are two possible workarounds. Either import the UserForm from the other project, or reference the other project and call a subroutine (which shows the UserForm) in the other project.

Both workarounds are explained in detail in XL97: Unable to Show UserForms in Other Projects.

Closing the current Excel application from a VBA procedure

You want to write a VBA procedure that will close the current Excel application along with the workbook that defines this procedure. You can achieve this as follows:

  1. Sub CloseExcel()
  2. ' Prevent Excel from prompting the user to save the open workbook(s):
  3. Application.DisplayAlerts = False
  4. Application.Quit
  5. End Sub

Note that you should not do this before calling Application.Quit:

ActiveWorkbook.Close False

Otherwise the execution of your procedure will stop before it reaches Application.Quit.

Disabling the "Do you want to save the changes you made to '<workbook_name>'?" message

In the Workbook_BeforeClose event handler enter the following code:

ThisWorkbook.Saved = True

Listing all available add-ins and finding out which XLA and XLL files are behind them

Use the following code to list all available add-ins, whether enabled ("On") or disabled ("Off") along with the names and paths of the corresponding XLA and XLL files: