CreateProcess()、WaitForSingleObject()、MsgWaitForMultipleObjects() を使う。
まず最初に、CreateProcess() を使って待ち続ける方法からです。
List 1
function WinExecAndWait32(FileName: string; Visibility: Integer): Longword; var StartupInfo: TStartupInfo; ProcessInfo: TProcessInformation; begin FillChar(StartupInfo, SizeOf(StartupInfo), #0); StartupInfo.cb := SizeOf(StartupInfo); StartupInfo.dwFlags := STARTF_USESHOWWINDOW; StartupInfo.wShowWindow := Visibility; if not CreateProcess(nil, PChar(FileName), // pointer to command line string nil, // pointer to process security attributes nil, // pointer to thread security attributes False, // handle inheritance flag CREATE_NEW_CONSOLE or // creation flags NORMAL_PRIORITY_CLASS, nil, // pointer to new environment block nil, // pointer to current directory name StartupInfo, // pointer to STARTUPINFO ProcessInfo) // pointer to PROCESS_INF then Result := WAIT_FAILED else begin WaitForSingleObject(ProcessInfo.hProcess, INFINITE); GetExitCodeProcess(ProcessInfo.hProcess, Result); CloseHandle(ProcessInfo.hProcess); CloseHandle(ProcessInfo.hThread); end; end; { WinExecAndWait32 } procedure TForm1.Button1Click(Sender: TObject); begin WinExecAndWait32('Notepad.exe',SW_SHOW); ShowMessage('メモ帳が終了しました'); end;
ここではメモ帳を起動している。メモ帳の実行ファイルは Windows ディレクトリにあるので自動検索されるためファイル名だけでも成功するが、一般のアプリを起動するときはフルパスを設定する必要がある。ここでは終了待ちに WaitForSingleObject(ProcessInfo.hProcess, INFINITE); を使っている。WaitForSingleObject() をヘルプでしらべてみよう。
function WaitForSingleObject(hHandle: THandle; dwMilliseconds: DWORD): DWORD; stdcall;
hHandle には、オブジェクトのハンドルを設定する。ここでは、プロセスハンドルを設定する。上の例では、CreateProcess() の最後のパラメータである TProcessInformation 型レコードの hProcess フィールドが起動したプロセスのハンドルになっているのでそれを設定する。dwMilliseconds は待ち時間をミリ秒で設定する。この時間が経過すると WaitForSingleObject() はノンシグナル状態(終了していない)でも復帰する。INFINITE = DWORD($FFFFFFFF); を設定すると、オブジェクトがシグナル状態(終了した)になるまで待つ。この関数の返値は以下のようである。
const WAIT_OBJECT_0 = DWORD($00000000); // オブジェクトがシグナル状態になった(終了した) WAIT_ABANDONED = DWORD($00000080); // オブジェクトが mutex の場合だけ WAIT_TIMEOUT = DWORD($00000102); // 待ち時間が経過したがまだ終了していない
上の例では、待ち時間に INFINITE を設定している。したがって、WaitForSingleObject() は起動されたプロセスが終了するまで復帰しない。つまり、待っているほうのアプリは、一切の処理ができない。このことは特にウィンドウを表示している GUI アプリの場合は、終了待ちのあいだウィンドウの再描画ができないので図のように、変な表示をすることがある。この方法はもっとも簡単で、再描画の問題がない場合はこれを使う。
再描画の問題を解決する方法は、以下のようにして dwMilliseconds を明示的に設定するとよい。
List 2
function WinExecAndWait32(FileName: string; Visibility: Integer): Longword; var StartupInfo: TStartupInfo; ProcessInfo: TProcessInformation; begin FillChar(StartupInfo, SizeOf(StartupInfo), #0); StartupInfo.cb := SizeOf(StartupInfo); StartupInfo.dwFlags := STARTF_USESHOWWINDOW; StartupInfo.wShowWindow := Visibility; if not CreateProcess(nil, PChar(FileName), // pointer to command line string nil, // pointer to process security attributes nil, // pointer to thread security attributes False, // handle inheritance flag CREATE_NEW_CONSOLE or // creation flags NORMAL_PRIORITY_CLASS, nil, //pointer to new environment block nil, // pointer to current directory name StartupInfo, // pointer to STARTUPINFO ProcessInfo) // pointer to PROCESS_INF then Result := WAIT_FAILED else begin while WaitForSingleObject(ProcessInfo.hProcess,100) = WAIT_TIMEOUT do Application.ProcessMessages; GetExitCodeProcess(ProcessInfo.hProcess, Result); CloseHandle(ProcessInfo.hProcess); CloseHandle(ProcessInfo.hThread); end; end; { WinExecAndWait32 } procedure TForm1.Button1Click(Sender: TObject); begin Button1.Enabled := false; WinExecAndWait32('Notepad.exe',SW_SHOW); ShowMessage('メモ帳が終了しました'); Button1.Enabled := true; end;
ここでは待ちループを以下のようにしている。
while WaitForSingleObject(ProcessInfo.hProcess,100) = WAIT_TIMEOUT do Application.ProcessMessages;
起動アプリが終了したときは WaitForSingleObject() は WAIT_OBJECT_0 を返すので while ループが終了する。ループ内で Application.ProcessMessages; を実行しているので、メッセージ処理が定期的に行われるため右図のように再描画の問題は回避できるが、Button1 を押せてしまう。上のコードでは、Enabled プロパティーを設定して二度押しできないようにしている。
再描画の問題をもっと巧妙に回避することもできる。
List 3
procedure WaitFor(processHandle: THandle); var Msg: TMsg; ret: DWORD; begin repeat ret := MsgWaitForMultipleObjects(1, { 1 handle to wait on } processHandle, { the handle } False, { wake on any event } INFINITE, { wait without timeout } QS_PAINT or { wake on paint messages } QS_SENDMESSAGE { or messages from other threads } ); if ret = WAIT_FAILED then Exit; { can do little here } if ret = (WAIT_OBJECT_0 + 1) then begin { Woke on a message, process paint messages only. Calling PeekMessage gets messages send from other threads processed. } while PeekMessage(Msg, 0, WM_PAINT, WM_PAINT, PM_REMOVE) do DispatchMessage(Msg); end; until ret = WAIT_OBJECT_0; end; { Waitfor } function WinExecAndWait32V2(FileName: string; Visibility: Integer): DWORD; var StartupInfo: TStartupInfo; ProcessInfo: TProcessInformation; Msg: TMsg; begin { WinExecAndWait32V2 } FillChar(StartupInfo, SizeOf(StartupInfo), #0); StartupInfo.cb := SizeOf(StartupInfo); StartupInfo.dwFlags := STARTF_USESHOWWINDOW; StartupInfo.wShowWindow := Visibility; if not CreateProcess(nil, PChar(FileName), { pointer to command line string } nil, { pointer to process security attributes } nil, { pointer to thread security attributes } False, { handle inheritance flag } CREATE_NEW_CONSOLE or { creation flags } NORMAL_PRIORITY_CLASS, nil, { pointer to new environment block } nil, { pointer to current directory name } StartupInfo, { pointer to STARTUPINFO } ProcessInfo) { pointer to PROCESS_INF } then Result := DWORD(-1) { failed, GetLastError has error code } else begin Waitfor(ProcessInfo.hProcess); GetExitCodeProcess(ProcessInfo.hProcess, Result); CloseHandle(ProcessInfo.hProcess); CloseHandle(ProcessInfo.hThread); end; { Else } while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do; end; { WinExecAndWait32V2 } procedure TForm1.Button1Click(Sender: TObject); begin WinExecAndWait32V2('notepad.exe', SW_SHOWNORMAL); ShowMessage('メモ帳が終了しました'); end;
ここでは、終了待ちを手続き WaitFor() で行っている。終了待ち中でも、WM_PAINT メッセージと他のスレッドから SendMessage() で送られたメッセージだけは処理されるので、右図のように再描画の問題は回避される。終了待ち中でも Button1 や X ボタンを押すことはできるが、処理されずにメッセージキューにとどまっている。WinExecAndWait32V2() では、この関数が終了する直前に PeekMessage() を while ループで実行してこれを取り除いている。このようにして、事実上、終了待ち中は設定したメッセージ以外のメッセージを受け付けないようにしている。
MsgWaitForMultipleObjects() を調べよう。
function MsgWaitForMultipleObjects(nCount: DWORD; var pHandles; fWaitAll: BOOL; dwMilliseconds, dwWakeMask: DWORD): DWORD; stdcall;
nCount には、待つ対象となるオブジェクトの数を設定する。ここでは一つのプロセスだけなので1を設定する。pHandles は、SDK ではオブジェクトのハンドルを設定した配列へのポインタを設定するが、これに相当する Delphi のインタフェースでは形無し変数の参照渡しになっている。ここでは、対象オブジェクトの数は一つだけなので、プロセスIDを設定した変数そのものを設定する。fWaitAll は、true のときは対象オブジェクトがすべてシグナル状態(終了した状態)になったときにこの関数が復帰する、false のときは、どれかひとつがシグナル状態になったときに復帰する。dwMilliseconds は待ち時間をミリ秒で指定する。dwWakeMask は、この関数が復帰するためのメッセージの受け取り条件を以下の定数で設定する。
QS_KEY = $0001; // WM_KEYUP, WM_KEYDOWN, WM_SYSKEYUP, or WM_SYSKEYDOWN QS_MOUSEMOVE = $0002; // WM_MOUSEMOVE QS_MOUSEBUTTON = $0004; // WM_LBUTTONUP, WM_RBUTTONDOWN などのマウスボタンメッセージ QS_POSTMESSAGE = $0008; // キューに何かのメッセージが入ったとき QS_TIMER = $0010; // WM_TIMER QS_PAINT = $0020; // WM_PAINT QS_SENDMESSAGE = $0040; // 他のスレッドから送られたメッセージがあるとき QS_HOTKEY = $0080; // WM_HOTKEY QS_ALLPOSTMESSAGE = $0100; // キューに何かのメッセージが入ったとき QS_MOUSE = (QS_MOUSEMOVE or QS_MOUSEBUTTON); QS_INPUT = (QS_MOUSE or QS_KEY); QS_ALLEVENTS = (QS_INPUT or QS_POSTMESSAGE or QS_TIMER or QS_PAINT or QS_HOTKEY); QS_ALLINPUT = (QS_INPUT or QS_POSTMESSAGE or QS_TIMER or QS_PAINT or QS_HOTKEY or QS_SENDMESSAGE);
復帰したときの戻り値からどの復帰条件で戻ってきたのかを判断する。
WAIT_OBJECT_0 to (WAIT_OBJECT_0 + nCount - 1) fWaitAll が true の場合、すべてのオブジェクトがシグナル状態になった(終了した) fWaitAll が false の場合、戻り値から WAIT_OBJECT_0 を引いた値のオブジェクトが シグナル状態になった(終了した) WAIT_OBJECT_0 + nCount dwWakeMask に設定した条件で復帰した WAIT_TIMEOUT 待ち時間が経過したので復帰した WAIT_FAILED この関数の実行が失敗した
以上のことを考慮して待ちループを見てみよう。
repeat ret := MsgWaitForMultipleObjects(1, { 1 handle to wait on } processHandle, { the handle } False, { wake on any event } INFINITE, { wait without timeout } QS_PAINT or { wake on paint messages } QS_SENDMESSAGE { or messages from other threads } ); if ret = WAIT_FAILED then Exit; { can do little here } if ret = (WAIT_OBJECT_0 + 1) then begin { Woke on a message, process paint messages only. Calling PeekMessage gets messages send from other threads processed. } while PeekMessage(Msg, 0, WM_PAINT, WM_PAINT, PM_REMOVE) do DispatchMessage(Msg); end; until ret = WAIT_OBJECT_0;
これを見ると、待ち関数が失敗したらすぐに復帰する。dwWakeMask に設定した条件で復帰した場合は、メッセージループを実行して、メッセージ処理を行う。そして、シグナル状態になるまでループする。という意味が読みとれるだろう。この待ち関数は非常に巧妙であり、いろいろ応用が効く。コピペでよいのだったら、これを使うべきだろう。