他のプログラムを起動してそれが終了するのを待つ

参照元

redball.gif目次へ戻る

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);   // 待ち時間が経過したがまだ終了していない
n00101.gif

上の例では、待ち時間に 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;
n00102.gif

起動アプリが終了したときは 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;
n00103.gif

ここでは、終了待ちを手続き WaitFor() で行っている。終了待ち中でも、WM_PAINT メッセージと他のスレッドから SendMessage() で送られたメッセージだけは処理されるので、右図のように再描画の問題は回避される。終了待ち中でも Button1X ボタンを押すことはできるが、処理されずにメッセージキューにとどまっている。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 に設定した条件で復帰した場合は、メッセージループを実行して、メッセージ処理を行う。そして、シグナル状態になるまでループする。という意味が読みとれるだろう。この待ち関数は非常に巧妙であり、いろいろ応用が効く。コピペでよいのだったら、これを使うべきだろう。

redball.gif目次へ戻る