|
Die Shell-Funktion in Visual Basic ermöglicht leider nur eine asynchrone Ausführung einer über sie aufgerufenen weiteren Anwendung. Das bedeutet, dass Ihre Anwendung nicht auf eine Beendigung der aufgerufenen Anwendung wartet und somit der dem Aufruf nachfolgende Code sofort weiter ausgeführt wird. Bei einer synchronen Ausführung würde der Funktionsaufruf hingegen erst dann zurückkehren, wenn die aufgerufene Anwendung wieder (auf welche Weise auch immer) beendet worden ist.
Eine einfache Lösung der Aufgabe beruht weiterhin auf dem Aufruf der Shell-Funktion. Nun wird allerdings eine Schleife nachgeschaltet, die erst dann verlassen wird, wenn die aufgerufene Anwendung tatsächlich wieder beendet worden ist. Um dies festzustellen, müssen wir den Prozess der aufgerufenen Anwendung befragen. Auch hier ist ein einfacher Weg, den Prozess nach seinem Exit-Code zu befragen. So lange der Prozess noch aktiv ist, entspricht sein Exit-Code dem Konstant-Wert STILL_ACTIVE. Wenn der Prozess beendet wird, ist der Exit-Code im Normalfall 0 - es sei denn die beendete Anwendung hätte selbst explizit einen bestimmten Exit-Code gesetzt (beispielsweise siehe: "Rückgabewert an Stapeldatei" - wobei ein Error-Code und ein Exit-Code ein und dasselbe sind).
Den zu befragenden Prozess öffnen wir anhand der von der Shell-Funktion zurückgegebenen Task-Id mit der API-Funktion OpenProcess im Modus PROCESS_QUERY_INFORMATION. In der besagten Schleife rufen wir den aktuellen Status des Exit-Codes über die API-Funktion GetExitCodeProcess ab. Ändert sich der Exit-Code, wird die Schleife verlassen. Nun entsorgen wir noch das Prozess-Handle mittels der API-Funktion CloseHandle.
Packen Sie das Ganze in eine Funktion, entspricht der Aufruf dem der gewohnten Shell-Funktion. Als kleinen nützlichen und informativen Nebeneffekt können Sie hier sogar noch den Exit-Code als Rückgabewert der Funktion verwerten.
Private Declare Function CloseHandle Lib "kernel32" _
(ByVal hObject As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32" _
(ByVal hProcess As Long, ExitCode As Long) As Long
Private Declare Function OpenProcess Lib "kernel32" _
(ByVal DesiredAccess As Long, ByVal InheritHandle As Long, _
ByVal ProcessId As Long) As Long
Public Function ShellWait(Exec As String, _
Optional WindowStyle As VbAppWinStyle = vbMinimizedFocus) _
As Long
Dim nTaskId As Long
Dim nHProcess As Long
Dim nExitCode As Long
Const STILL_ACTIVE = &H103
Const PROCESS_QUERY_INFORMATION = &H400
nTaskId = Shell(Exec, WindowStyle)
nHProcess = OpenProcess(PROCESS_QUERY_INFORMATION, False, nTaskId)
Do
DoEvents
GetExitCodeProcess nHProcess, nExitCode
Loop While nExitCode = STILL_ACTIVE
CloseHandle nHProcess
ShellWait = nExitCode
End Function
Bei der eigentlich beabsichtigten synchronen Ausführung ist die Gewinnung des Exit-Codes eher nur ein Abfallprodukt. Wenn Sie hingegen die Shell-Funktion wie gewohnt asynchron aufrufen, aber trotzdem über das Beenden der aufgerufenen Anwendung informiert werden möchten, Müssen Sie den Shell-Aufruf und die Überwachungsschleife entkoppeln. Den Exit-Code gewinnen Sie auch hier wieder als Abfallprodukt.
Die Entkopplung erfolgt über einen Timer, der auf einem ansonsten unsichtbar bleibenden Form platziert ist, während die Information über die Beendigung der aufgerufenen Anwendung über ein Ereignis (ShellExit) erfolgt. Wie in der oben stehenden Funktion werden die Shell-Funktion zum Start der gewünschten Anwendung aufgerufen und das Prozess-Handle ermittelt. Anschließend wird jedoch der Timer aktiviert und der Aufruf der hier als Shell-Methode des besagten Forms angelegten Aufruf-Prozedur kehrt unmittelbar danach, also asynchron, zurück. Die Task-Id und das Prozess-Handle werden in privaten Variablen des Forms abgelegt und können über Eigenschaften-Prozeduren zwischendurch abgefragt werden. Die Aufruf-Methode gibt die TaskId zurück, anhand derer später auch eine eindeutige Zuordnung des ShellExit-Ereignisses erfolgen kann.
Private Declare Function CloseHandle Lib "kernel32" _
(ByVal hObject As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32" _
(ByVal HProcess As Long, ExitCode As Long) As Long
Private Declare Function OpenProcess Lib "kernel32" _
(ByVal DesiredAccess As Long, ByVal InheritHandle As Long, _
ByVal ProcessId As Long) As Long
Public Event ShellExit(ByVal TaskId As Long, _
ByVal ExitCode As Long)
Private pTaskId As Long
Private pHProcess As Long
Private pExitCode As Long
Public Function Shell(Exec As String, _
Optional WindowStyle As VbAppWinStyle = vbMinimizedFocus) _
As Long
Const PROCESS_QUERY_INFORMATION = &H400
pTaskId = VBA.Shell(Exec, WindowStyle)
pHProcess = _
OpenProcess(PROCESS_QUERY_INFORMATION, False, pTaskId)
Shell = pTaskId
tmr.Enabled = True
End Function
Public Property Get TaskId() As Long
TaskId = pTaskId
End Property
Public Property Get hProcess() As Long
hProcess = pHProcess
End Property
Der Eintritt in die Überwachungsschleife erfolgt im Timer-Ereignis des Timers. Dort deaktiviert sich zunächst der Timer selbst und entlädt das Form (das ja durch die Aktivierung des Timers unweigerlich geladen worden ist). Wenn die nachfolgende Schleife verlassen wird, wird das ShellExit-Ereignis ausgelöst, das als Parameter die Task-Id und den Exit-Code mitnimmt.
Private Sub tmr_Timer()
Const STILL_ACTIVE = &H103
tmr.Enabled = False
Unload Me
Do
DoEvents
GetExitCodeProcess pHProcess, pExitCode
Loop While pExitCode = STILL_ACTIVE
CloseHandle pHProcess
pHProcess = 0
RaiseEvent ShellExit(pTaskId, pExitCode)
End Sub
Der Aufruf einer Anwendung und die Auswertung ihrer Beendigung über ein solches Entkoppelungsform (frmShell) könnte zum Beispiel so aussehen
Private WithEvents eShell As frmShell
Private Sub Command1_Click()
Set eShell = New frmShell
eShell.Shell "calc.exe", vbNormalFocus
MsgBox "Rechner 2 geöffnet"
End Sub
Private Sub eShell_ShellExit(ByVal hTask As Long, _
ByVal ExitCode As Long)
MsgBox "Rechner 2 geschlossen"
Set eShell = Nothing
End Sub
|