Archive for the ‘Application Object’ Category.

Preventing Event Conflicts

I have two custom add-ins loaded that use application-level events. I only want those events to run when a workbook associated with my application is active. Otherwise, the events in my purchase order application will try and do stuff to my invoices and vice versa. Not good.

For every application, the first thing I do is test to make sure I'm dealing with an appropriate object; be it a sheet, workbook, or whatever. I use two utilities to do the testing. The first utility verifies an open workbook and the second verifies a closed one.

As described in Custom Document Properties, I use custom document properties in my templates to identify the workbook as being part of the application. The utilities check the property and return True if it's there.

Function IsOpenInvoice(ByRef Wb As Workbook, _
    Optional ByVal sProperty As String = gsCDPINVAPP) As Boolean
   
    Dim bTemp As Boolean
   
    On Error Resume Next
        bTemp = Wb.CustomDocumentProperties(sProperty).Value
    On Error GoTo 0
   
    IsOpenInvoice = bTemp
   
End Function
 
Function IsInvoice(ByVal sName As String, _
    Optional ByVal sProperty As String = gsCDPINVAPP) As Boolean
       
    With Application.FileSearch
        .NewSearch
        .FileType = msoFileTypeAllFiles
        .Filename = Dir(sName)
        If Not sName = Dir(sName) Then
            .LookIn = Replace(sName, Dir(sName), "")
        End If
        .PropertyTests.Add sProperty, msoConditionIsYes
       
        .Execute
       
        IsInvoice = .FoundFiles.Count> 0
    End With
   
End Function

In IsOpenInvoice, the property value from the supplied workbook is set to a Boolean variable. If the property value is False or if the property doesn't exist, the variable is False. Of course the property value will never be false. I created the properties in the template and set the value to True. It will either exist or not.

For closed workbooks, IsInvoice uses FileSearch to find a file with the proper name and with the correct property. The argument sName is the full path and name of the file. If the file exists in the directory and has the property, True is returned. This is typically used before a file is opened, such as after GetOpenFilename is used but before the file is actually opened.

Both functions have an optional second argument, sProperty, that defaults to the property name that identifies the application in general. I also use these utilities to identify particular templates within my app. I may need to know, for instance, if the open workbook is an invoice, a sales order, or a report in particular, rather than just part of my app in general. I have constants set up for each template type that I want to test.

Here's an example of an application level event in a custom class module that uses IsOpenInvoice:

Private Sub mApp_SheetChange(ByVal Sh As Object, ByVal Target As Range)
   
    If IsOpenInvoice(Sh.Parent, gsINVOICE) Then
        'Do stuff
    End If
   
End Sub

More Dueling Banjos

I tried to use EnableCancelKey to allow myself to stop the music and still make the appropriate API calls to reset the midi out. It didn't work so well for me. Here's another way.

I created two global variables. One to determine whether the procedure was already running and one to determine if the user wants to stop the procedure.

Public gbUserCancel As Boolean
Public gbAppStarted As Boolean

These are declared in a standard module outside of any procedures so that they're accessible from anywhere in the project.

Next, I modified the click event of the button that's used to start the procedure.

Private Sub CommandButton1_Click()
   
    If gbAppStarted Then
        gbUserCancel = True
        gbAppStarted = False
    Else
        gbAppStarted = True
        PlayWorksheetNotes
    End If
   
End Sub

If the procedure is running (gbApppStarted is True), then it assumed the user wants to stop the procedure and gbUserCancel is set to True. If the procedure is not running, it's called so that it starts running. Finally, I modified the PlayWorksheetNotes procedure to check the gbUserCancel variable.

Sub PlayWorksheetNotes()
    Dim r As Long
   
    On Error GoTo ErrHandler
   
    gbUserCancel = False
 
    For r = 2 To Application.CountA(Range("A:A"))
        If gbUserCancel Then Err.Raise 9999
        Cells(r, 2).Select
        Call PlayMIDI(Cells(r, 1), Cells(r, 2), Cells(r, 3))
        DoEvents
    Next r
 
ProcExit:
    On Error Resume Next
    midiOutReset hMidiOut
    'Stop
    Exit Sub
   
ErrHandler:
    If Err.Number <> 9999 Then
        MsgBox Err.Description
    End If
    'Stop
    Resume ProcExit
 
End Sub

If the button is clicked while the notes are playing, gbUserCancel is set to True. Inside the loop, that variable is check. Once True, an error is raised and the error handler is called. Any error that's not the one I made up (9999) is shown in a message box. Then the procedure resumes at ProcExit which resets the midi out for the future.

EnableCancelKey

The EnableCancelKey property of the Application object determines what will happen when a user interrupts your code. Users can interrupt code by pressing Esc or Cntl+Break. EnableCancelKey has three possible settings:

  • xlDisable - Prevents the user from interrupting. I've never used this and can't think of why I ever would.
  • xlInterrupt - Normal operation. The debugger is shown and the code is in debug mode at whichever line it happened to be when it was interrupted.
  • xlErrorHandler - Raises error number 18 and reacts just like any other error. If you have error handling set up, it's called.

J-Walk posted about Musical Excel a while back. There was an issue with the midi out API not resetting properly and people could not run the code twice. Mpemba fixed it in the comments with a reset API call, but if the user was to interrupt the code, that API function would never get called.

I downloaded the dueling banjos file from J-Walk, and modified the code. This part was added:

Private Declare Function midiOutReset Lib "winmm.dll" _
    (ByVal hMidiOut As Long) As Long

and this part was modified:

Sub PlayWorksheetNotes()
    Dim r As Long
   
    On Error GoTo ErrHandler
   
    Application.EnableCancelKey = xlErrorHandler
 
    For r = 2 To Application.CountA(Range("A:A"))
        Cells(r, 2).Select
        Call PlayMIDI(Cells(r, 1), Cells(r, 2), Cells(r, 3))
        DoEvents
    Next r
 
ProcExit:
    On Error Resume Next
    midiOutReset hMidiOut
    Stop
    Exit Sub
   
ErrHandler:
    If Err.Number <> 18 Then
        MsgBox Err.Description
    Else
        Debug.Print Err.Description
    End If
    Stop
    Resume ProcExit
 
End Sub

Setting EnableCancelKey allows the code to exit gracefully and ensures that the midiOutReset call is always made. This resets the midi out and should prevent the problem of not being able to run the code twice. It shouldn't really matter whether you have an On Error statement before or after the EnableCancelKey statement.

Actually, this code would exit gracefully except that I have all those 'Stop' commands in there. Oddly, when I set EnableCancelKey to xlInterrupt, it works every time I press Cntl+Break (that is, the code stops and the debugger opens). When I set EnableCancelKey to xlErrorHandler, pressing Cntl+Break works about one out of every thousand times. That estimate is based on me pressing it about 100 times during code execution and the code executes all the way through nine out of ten times.

If you have the time, download dueling-banjos.xls, modified the code as I have and see if you can interrupt normally.

Listing an Object’s Properties and Methods

Hi All,

If you do some VBA programming regularly, you'll be acquainted with the Object browser, which neatly shows you all objects, properties and methods belonging to any object in Excel (or any other library you have referenced in your VBA project).

Whilst this is a tremendous tool to look for information on just a couple of properties I find it not very handy when you e.g. need to get all of an objects properties into your VBA code.

So I thought I'd roll my own object browser, just for the fun of it (and because it proved useful, I share it here).

The tool uses the tlbinf32.dll present on (I assume) any current Windows installation to fetch data from typelibraries. I grabbed some code from various places (amongst which Chip Pearson's great site and some newsgroup messages like this one) and created a userform with a treeview control (because this is the type of control that can show hierarchical information so nicely).

Here is a screenshot of the tool:

objlister01.gif

Look here for a bit more information and the download link:
Object Lister

Regards,

Jan Karel Pieterse
JKP Application Development Services

Caller and Custom Commandbars

I learned some things today. Apparently I've never used Application.Caller in a procedure called from a custom CommandBarButton. Or if I have, and have since forgot about it. As it turns out, Application.Caller returns a Variant array with two elements. The first element seems like it should be the index number of the CommandBarButton from which the procedure was called. That is, if the second button on the CommandBar runs the procedure, the first element would contain a 2. The second element contains string that is the Caption of the CommandBar.

I say "seems" when I refer to the first element, because my CommandBar only contains four buttons.

?CommandBars("BBash").Controls.Count
4

Yet the first element of Application.Caller returns a seven. All of the controls are CommandBarButtons. There are no popups or any other kind of control. I can't imagine where it's getting 7. Does anyone know what I'm missing here?

If you're as surprised as I am that I didn't know Application.Caller returned an array, then you may be equally surprised at lesson #2 of the day. I can use a For Each construct to access the elements of an array as long as the control variable is Variant. I've always looped through arrays with For Next constructs like this

For x = LBound(arr) To UBound(arr)
    Debug.Print arr(x)
Next x

Now I know I could use this

For Each x In arr
    Debug.Print x
Next x

as long as x is declared as a Variant. I don't really plan to use this as it seems like an unnecessary use of a Variant.

Finally, an old nugget that I just don't use enough: multiple statements in the Immediate Window. In VBA procedures, you can put multiple statements on the same line by separating them with a colon. Since statements in the Immediate Window are executed as soon as you enter them, it's sometimes necessary to employ this technique. When I was trying to figure out what in tarnation Application.Caller was doing, I used a loop in the Immediate Window. To wit:

If you didn't know you could loop in the Immediate Window, then maybe you learned something today too.

Bug in Application.InputBox function

This article describes a bug recently discovered by Ron de Bruin and which has also
been reported here.

The Application.InputBox function is very useful to get a range from
the user. Unfortunately, this function exposes a bug in Excel (all current
versions!). If the sheet on which a (range of) cell(s) is selected contains
conditional formatting using the : "Formula Is" option, the function may fail,
returning an empty range.

The only reliable workaround is to build a userform to request the range from
the user, which I have included as a download here.