Handling Multiple Instances

The discussion of Windows so far in this chapter might have enlightened you, but it probably hasnít put much bread on your table. Hereís a more practical problem.

What happens if you try to start two copies of the same program? Some programs, such as Calculator, donít mind at all and will keep starting copies until you run out of memory. Others quit after displaying a message box saying that you can run only one copy. Still others, such as Microsoft Exchange, reactivate the current copy every time you try to start a new copy. For every program you write, you need to think about this issue and choose a strategy.

If itís OK to run multiple copies, you donít need to do anything. Thatís the default. If you want to run only one copy, terminating each additional attempt with an error message, you donít need to do much. Just put the following in Form_Load:

If App.PrevInstance Then
    MsgBox "You cannot start more than one copy"
End If

This technique has one problemóApp.PrevInstance is always False in the Visual Basic environment. You can launch multiple versions of Visual Basic running the same program, but each will think that itís the only one. Youíll have difficulty debugging complicated code that uses the PrevInstance property. You canít test it in the environment because it wonít behave the same, but you canít easily test it outside the environment because you donít have a debugger.

A bigger problem with putting up an error message in this situation is that itís rarely the right thing to do. Itís much better to change the focus to the first copy and terminate the second copy. On the surface, it doesnít seem to be hard to accomplish this in Visual Basic. The technique on the following page (from ALLABOUT.FRM) will work for many programs.

    If App.PrevInstance Then
        Dim sTitle As String
        ' Save my title
        sTitle = Me.Caption
        ' Change my title bar so I wonít activate myself
        Me.Caption = Hex$(Me.hWnd)
        ' Activate other instance
        AppActivate sTitle
        ' Terminate myself
    End If

Is changing the caption before activating the other instance a neat trick or what? This works great for programs that always have the same title, but what if the other instance has a different title? Notepadís title contains the name of the current file. So does the Visual Basic environment. In fact, the Windows interface standard specifies that any window representing a document should have the document name in the title. But if you donít know the document name, you canít call AppActivate.

WinWatch handles multiple copies the hard way. It calls the GetFirstInstWnd function to find out whether there is another instance. If so, it activates that window by calling the SetForegroundWindow API function. The call looks like this:

    Dim hWndOther As Long
    hWndOther = GetFirstInstWnd(Me.hWnd)
    If hWndOther <> hNull Then
        ' Uncomment this line for debugging
        'MsgBox "Activating first instance"
        SetForegroundWindow hWndOther
    End If

The GetFirstInstWnd function does the actual work. It works by looping through all the top windows until it finds one that has a different process ID, but the same module name. Thatís the duplicate. The code (in MODTOOL.BAS) looks like this:

Function GetFirstInstWnd(hWndMe As Long) As Long
    Dim hWndYou As Long, idMe As Long, sExeMe As String

    ' Get my own process ID and executable name
    idMe = MWinTool.ProcIDFromWnd(hWndMe)
    sExeMe = ExeNameFromWnd(hWndMe)
    ' Get first sibling to start iterating top-level windows
    hWndYou = GetWindow(hWndMe, GW_HWNDFIRST)
    Do While hWndYou <> hNull
        ' Ignore if process ID of target is same
        If idMe <> MWinTool.ProcIDFromWnd(hWndYou) Then
            ' Ignore if module name is different
            If sExeMe = ExeNameFromWnd(hWndYou) Then
                ' Return first with same module, different process
                GetFirstInstWnd = hWndYou
                Exit Function
            End If
        End If
        ' Get next sibling
        hWndYou = GetWindow(hWndYou, GW_HWNDNEXT)
End Function
This technique works for most applications, but itís not infallible. Your program might have multiple top-level windows, and the first one returned might not be the one you want to activate. Thatís why MODTOOL.BAS also contains the GetAllInstWnd function, which returns a Collection of all the window handles of other instances. GetAllInstWnd enables you to follow one of Joe Hackerís favorite design rules: "If in doubt, let the user decide." You could create a dialog box form with a list box of existing instances. Let the user decide whether to cancel the request, launch a new instance, or activate an existing one. This would be an excellent way to handle the new multiple instance model that is becoming popular as an alternative to the Multiple Document Interfaceóeach document is handled by a separate independent instance of a small program rather than by a separate MDI window of a large program.

WARNING Terminating a duplicate instance is one of those rare cases when you should actually use the End statement. Although the name sounds harmless, End is actually more like an Abort statement that means stop by any means even if it canít do normal cleanup. Experienced Visual Basic programmers terminate their programs by unloading all the active forms. Normally, all it takes is an Unload Me statement in the main form. But if youíre trying to terminate in the main Form_Load, there is, by definition, nothing to unload. So the rule is simple: Never use End except in Form_Load. In the first edition of this book, I violated this rule by providing a procedure that looked for a duplicate instance and terminated that instance with an End statement inside the procedure. I got a rude surprise when I tried to put that procedure in the VBCore component. Visual Basic wonít let you use the End statement in a DLL or a control. The GetFirstInstWnd function can be in a DLL because it returns the other instance rather than trying to take action about it. This is a more flexible structure anyway. The caller of GetFirstInstWnd can decide what it wants to do about the duplicate instance.