Hauptseite >Tips zu VB5/6 >  Mehrfachstart einer Anwendung verhindern
 
Manchmal ist es wünschenwert, dass von der eigenen Anwendung nur eine Instanz gestartet werden kann. Beispielsweise, wenn diese eine Access-Datenbank exklusiv öffnet; eine zweite Instanz würde dann am Öffnen dieser Datenbank scheitern.
VB bietet mit der PrevInstance-Eigenschaft des App-Objekt eine komfortable Möglichkeit, um festzustellen, ob bereits eine Instanz läuft. Um die folgenden Beispiele auszuprobieren, erzeugen Sie ein neues Projekt mit einer Form und einem Standardmodul; legen Sie in letzterem eine Sub Main() an und legen Sie diese als Startobjekt fest.

Sub Main()
  If Not App.PrevInstance Then
    Form1.Show
  End If
End Sub
            
Wenn Sie mit diesem Code nun eine ausführbare Datei erzeugen und versuchen, diese mehrmals hintereinander zu starten, werden Sie sehen, dass keine weitere Instanz erzeugt wird, solange die erste noch existiert.

So weit, so gut. Die vorgestellte Lösung hat jedoch eine Reihe von Nachteilen. Der wohl gravierendste besteht darin, dass der Benutzer die ausführbare Datei nur in ein anderes Verzeichnis zu kopieren braucht, um die Sperre auszuhebeln. VB erkennt eine vorherige Instanz nicht mehr, wenn diese aus einem anderen Verzeichnis gestartet wurde.

Es gibt eine Reihe von Möglichkeiten, dieses Manko zu umgehen. Meine bevorzugte Variante besteht darin, mittels FindWindowEx() nach Vorgänger-Instanzen zu suchen:

'API-Deklarationen

Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" _
  (ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, _
  ByVal lpsz2 As String) As Long

Const VB_FORM_CLASS$ = "ThunderRT6FormDC"
            

Sub Main()
Dim l As Long

l = FindWindowEx(0&, 0&, VB_FORM_CLASS, "Form1")
  If l = 0 Then
    Form1.Show
  End If
End Sub
            
Im dritten Parameter des Aufrufs von FindWindowEx() übergeben Sie den Klassennamen aller von VB6 erzeugten Forms: "ThunderRT6FormDC". Wenn Sie mit VB5 arbeiten, ersetzen Sie diesen Wert durch "ThunderRT5Form". Nebenbei: läuft das Programm in der IDE, lauten die Klassennamen anders ("ThunderForm" in VB5, "ThunderFormDC" in VB6).
In vierten Parameter des Aufrufs von FindWindowEx() übergeben Sie die Titelzeile Ihres Hauptformulars, in unserem Testbeispiel "Form1".

Wenn Sie nun wieder das Programm kompilieren und die ausführbare Datei in zwei verschiedenen Verzeichnissen speichern, werden Sie sehen, dass sich das Programm jetzt nicht mehr doppelt starten lässt.

Der Nachteil an dieser Variante ist, dass Sie die Titelzeile Ihres Hauptformulars im Programmablauf nicht änderen dürfen. Damit funktioniert diese Lösung z.B. nicht mehr für MDI-Forms, die in der Titelleiste auch den Titel eines maximierten, aktiven MDI-Childs anzeigt. Zudem könnte ein anderes, in der gleichen VB-Version geschriebenes Programm zufällig den gleichen Text in der Titelleiste stehen haben und damit fälschlich als Vorgänger-Instanz des eigenen Programms identifiziert werden. Es empfiehlt sich also, ein anderes Erkennungskriterium als den Titelleisten-Text zu verwenden. Auch hier gibt es wieder mehrere denkbare Varianten; so könnte man z.B. das Hauptformular subclassen und eine eigene Fensternachricht definieren, auf die hin sich das Programm mit einem festgelegten Rückgabewert ausweist - oder auch nicht (Rückgabewert 0), wenn es sich um ein "fremdes" Fenster handelt.

Eine deutlich einfachere und vor allem IDE-stabile Möglichkeit bieten die API-Funktionen SetProp() und GetProp() an; mit SetProp() verpassen Sie Ihrem Formular eine ganzzahlige Eigenschaft (Long-Wert) mit einem bestimmten Namen; mit GetProp() können Sie ein beliebiges Fenster abfragen, ob es diese Eigenschaft besitzt; wenn nicht, liefert die Funktion 0& zurück:

'API-Deklarationen

Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" _
  (ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, _
  ByVal lpsz2 As String) As Long
Declare Function GetProp Lib "user32" Alias "GetPropA" _
  (ByVal hwnd As Long, ByVal lpString As String) As Long
Declare Function SetProp Lib "user32" Alias "SetPropA" _
  (ByVal hwnd As Long, ByVal lpString As String, ByVal hData As Long) As Long
Declare Function RemoveProp Lib "user32" Alias "RemovePropA" _
  (ByVal hwnd As Long, ByVal lpString As String) As Long

Public Const VB_FORM_CLASS$ = "ThunderRT6FormDC"
Public Const MY_IDENTIFIER_NAME$ = "EindeutigerNameFürMeinProgramm"
Public Const MY_IDENTIFIER_VALUE& = 876543
            

Sub Main()
Dim l As Long, p As Long

l = FindWindowEx(0&, 0&, VB_FORM_CLASS, vbNullString)
  Do While l
  p = GetProp(l, MY_IDENTIFIER_NAME)
    If p = MY_IDENTIFIER_VALUE Then
      Exit Sub
    End If
  l = FindWindowEx(0&, l, VB_FORM_CLASS, vbNullString)
  Loop
Load Form1
SetProp Form1.hwnd, MY_IDENTIFIER_NAME, MY_IDENTIFIER_VALUE
Form1.Show
End Sub
            
Wählen Sie für MY_IDENTIFIER_NAME eine möglichst eindeutige Bezeichnung und für MY_IDENTIFIER_VALUE einen beliebigen ganzzahligen Wert; damit wird es äusserst unwahrscheinlich, dass eine andere Applikation die gleiche Kombination zur Selbst-Identifikation verwendet.

Bevor das Formular entladen wird, sollte man die Fenstereigenschaft zurücksetzen:

Private Sub Form_Unload(Cancel As Integer)
'Unload-Event des Form-Moduls
RemoveProp Me.hwnd, MY_IDENTIFIER_NAME
End Sub
            

Es ist natürlich nicht damit getan, die zweite Instanz einfach nicht starten zu lassen; es sollte schon irgendetwas passieren, wenn der Benutzer eine solche zweite Instanz starten will. Naheliegend wäre, dass die zweite Instanz die erste noch sichtbar macht und aktiviert, bevor sie sich verabschiedet:

'API-Deklarationen

Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" _
  (ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, _
  ByVal lpsz2 As String) As Long
Declare Function GetProp Lib "user32" Alias "GetPropA" _
  (ByVal hwnd As Long, ByVal lpString As String) As Long
Declare Function SetProp Lib "user32" Alias "SetPropA" _
  (ByVal hwnd As Long, ByVal lpString As String, ByVal hData As Long) As Long
Declare Function RemoveProp Lib "user32" Alias "RemovePropA" _
  (ByVal hwnd As Long, ByVal lpString As String) As Long
Declare Function IsIconic Lib "user32" (ByVal hwnd As Long) As Long
Declare Function ShowWindow Lib "user32" _
  (ByVal hwnd As Long, ByVal nCmdShow As Long) As Long
Declare Function GetForegroundWindow Lib "user32" () As Long
Declare Function GetWindowThreadProcessId Lib "user32" _
  (ByVal hwnd As Long, lpdwProcessId As Long) As Long
Declare Function AttachThreadInput Lib "user32" _
  (ByVal idAttach As Long, ByVal idAttachTo As Long, ByVal fAttach As Long) _
  As Long
Declare Function SetForegroundWindow Lib "user32" (ByVal hwnd As Long) As Long

Public Const SW_RESTORE& = 9&

Public Const VB_FORM_CLASS$ = "ThunderRT6FormDC"
Public Const MY_IDENTIFIER_NAME$ = "EindeutigerNameFürMeinProgramm"
Public Const MY_IDENTIFIER_VALUE& = 876543
            

Sub Main()
Dim l As Long, p As Long, fgw As Long, tfgw As Long, tl As Long

l = FindWindowEx(0&, 0&, VB_FORM_CLASS, vbNullString)
  Do While l
    p = GetProp(l, MY_IDENTIFIER_NAME)
      If p = MY_IDENTIFIER_VALUE Then
        fgw = GetForegroundWindow()
          If fgw <> l Then
            tfgw = GetWindowThreadProcessId(fgw, ByVal 0&)
            tl = GetWindowThreadProcessId(l, ByVal 0&)
              If tfgw <> tl Then
                AttachThreadInput tfgw, tl, True
                SetForegroundWindow l
                AttachThreadInput tfgw, tl, False
              Else
                SetForegroundWindow l
              End If

              If IsIconic(l) Then
                ShowWindow l, SW_RESTORE
              End If
          End If
        Exit Sub
      End If
    l = FindWindowEx(0&, l, VB_FORM_CLASS, vbNullString)
  Loop
Load Form1
SetProp Form1.hwnd, MY_IDENTIFIER_NAME, MY_IDENTIFIER_VALUE
Form1.Show
End Sub
            
Bleibt noch ein Problem: Wenn die zweite Instanz mit einem Parameter, beispielsweise mit einem Dateinamen, aufgerufen wird, sollte diese Information an die erste Instanz weitergegeben werden, damit diese entsprechend reagieren kann. Auch hier gibt es wieder verschiedene Möglichkeiten wie z.B. DDE. Für eine schlanke, einfache Möglichkeit können wir ebenfalls das Gespann SetProp() und GetProp() einsetzen: Verpassen Sie Ihrem Formular eine versteckte Textbox (Visible = False), in die die zweite Instanz ihren Parameter-Wert schreiben kann; die erste Instanz hat nun die Aufgabe, im Change-Event die Information zu verarbeiten. Damit die zweite Instanz erfahren kann, welches Window-Handle diese Textbox hat, machen Sie es einfach über eine WindowProperty öffentlich.

Ein vollständiges Beispielprojekt können Sie hier herunterladen. Bitte passen Sie die globale Konstante VB_FORM_CLASS$ in Module1 entsprechend der verwendeten VB-Version an. In der IDE ist ein Test naturgemäss wenig anschaulich, schliesslich können Sie das gleiche Projekt nicht mehrmals in der IDE öffnen und starten. Kompilieren Sie das Programm und starten Sie es mehrfach hintereinander mit unterschiedlichen Aufrufparametern:

Projekt1.exe AAA
Projekt1.exe BBB
Projekt1.exe CCC
...

Minimieren Sie ruhig zwischendurch das Programmfenster; es wird bei erneutem Programmaufruf wiederhergestellt.
Sollte sich das Programm wider Erwarten mehrfach starten lassen, überprüfen Sie bitte die globale Konstante VB_FORM_CLASS$ in Module1.
Hauptseite >  Tips zu VB5/6 >  diese Seite