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
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
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
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)
Loop
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.