携帯版
5090 (isweb) + 79520 (Vector) + Counter by XREA.COM


メニュー
アンテナ
Autch.net > 小ネタ > GUI アプリからコンソールアプリを実行するには Last-Modified: 2008-09-10 16:40:03 (JST)

[Delphi] GUI アプリからコンソールアプリを実行するには

概要

GUI アプリからコンソールアプリを単に実行するだけならば,

ShellExecute(Handle, nil, 'command.com /c dir c:\', nil, nil, 0);

とでもすればいいが,これだと DOS 窓が出てきてしまい,こっそりバックグラウンドで 実行したいようなときには具合が悪い。最後の引数に SW_HIDE を指定すれば 一応非表示にはなるが、この方法ではコンソールアプリの標準入出力を GUI 側で操作したり、 コンソールアプリの終了のタイミングを把握することはできない。

この方法でそれを実現するには,まず command.com を使ってファイルへリダイレクトする (command.com /c sort < source.txt > result.txt とか)という方法が考えられるが、 この方法だと command.com が必ず必要になってしまう。 Windows は設定によって DOS サブシステムの動作を禁止することができるので、 あらゆる環境でこの方法が使えることを保証することはできない (NT 系なら cmd.exe を使うようにする?)。

もうひとつの手は、指定されたプログラムを起動して、その標準入力としてファイルを与え、 標準出力をファイルへ吐き出すようなコンソールプログラム(DJGPP の redir.exe みたいなやつ←誰が覚えてる?) を作成し、それを実行させるというものだが、わざわざそんなものをメインプログラムとは別に作らなくても、 GUI アプリから直接標準入出力のリダイレクトをすることができる。

コード

GrabStdOut 関数(ユニットではない)
type
  // CreateProcess() が失敗したときに投げられる例外
  // この関数を置くユニットの interface にでも書いて
  ELaunchFailed = class(Exception)
  end;

function GrabStdOut(sCommandLine: string; StdIn, StdOut, StdErr: TStream): DWORD;
const
  BUFFER_SIZE = 8192;
  WAIT_FOR_READY = 1000;
  WAIT_FOR_RUN = 20;
var
  hReadPipe, hWritePipe: THandle;
  hStdInReadPipe, hStdInWritePipe, hStdInWritePipeDup: THandle;
  hErrReadPipe, hErrWritePipe: THandle;
  sa: TSecurityAttributes;
  StartupInfo: TStartupInfo;
  ProcessInfo: TProcessInformation;
  dwStdOut, dwStdErr, dwRet: DWord;
  StreamBufferSize, nWritten: DWord;
  abyBuffer: array[0..BUFFER_SIZE] of Byte;
begin
  ZeroMemory(@sa, sizeof(TSecurityAttributes));
  with sa do
  begin
    nLength := sizeof(TSecurityAttributes);
    lpSecurityDescriptor := nil;
    bInheritHandle := true;
  end;

  Result := $ffffffff;

  hReadPipe := 0; hWritePipe := 0;
  hErrReadPipe := 0; hErrWritePipe := 0;

  CreatePipe(hStdInReadPipe, hStdInWritePipe, @sa, BUFFER_SIZE);
  DuplicateHandle(GetCurrentProcess(), hStdInWritePipe, GetCurrentProcess(),
                  @hStdInWritePipeDup, 0, false, DUPLICATE_SAME_ACCESS);
  CloseHandle(hStdInWritePipe);
  CreatePipe(hReadPipe, hWritePipe, @sa, BUFFER_SIZE);
  try
    CreatePipe(hErrReadPipe, hErrWritePipe, @sa, BUFFER_SIZE);
    try
      ZeroMemory(@StartupInfo, sizeof(TStartupInfo));
      with StartupInfo do
      begin
        cb := sizeof(TStartupInfo);
        dwFlags := STARTF_USESTDHANDLES;
        // これがないと DOS 窓が表示されてしまう
        wShowWindow := SW_HIDE;
        // 標準 IO にパイプの端っこを指定してやる
        hStdInput := hStdInReadPipe;
        hStdOutput := hWritePipe;
        hStdError  := hErrWritePipe;
      end;

      // コンソールアプリ起動
      if CreateProcess(nil, PChar(sCommandLine), @sa, nil, true, DETACHED_PROCESS,
                       nil, nil, StartupInfo, ProcessInfo) then
      begin
        // 入力待ちになるまで待ってから,
        WaitForInputIdle(ProcessInfo.hProcess, WAIT_FOR_READY);

        StreamBufferSize := BUFFER_SIZE;
        while StreamBufferSize = BUFFER_SIZE do
        begin
          // 入力を与える
          StreamBufferSize := StdIn.Read(abyBuffer, BUFFER_SIZE);
          WriteFile(hStdInWritePipeDup, abyBuffer, StreamBufferSize, nWritten, nil);
        end;
        // 入力を与え終わった
        CloseHandle(hStdInWritePipeDup);

        try
          repeat
            // Fixed 2006.08.26: バッファの最後を取りこぼすのを修正 
            dwRet := WaitForSingleObject(ProcessInfo.hProcess, WAIT_FOR_RUN);
            // 標準出力パイプの内容を調べる
            PeekNamedPipe(hReadPipe, nil, 0, nil, @dwStdOut, nil);
            if (dwStdOut > 0) then
            begin
              // 内容が存在すれば、読み取る
              ReadFile(hReadPipe, abyBuffer, dwStdOut, dwStdOut, nil);
              StdOut.WriteBuffer(abyBuffer, dwStdOut);
            end;

            // 同様にエラー出力の処理
            PeekNamedPipe(hErrReadPipe, nil, 0, nil, @dwStdErr, nil);
            if (dwStdErr > 0) then
            begin
              ReadFile(hErrReadPipe, abyBuffer, dwStdErr, dwStdErr, nil);
              StdErr.WriteBuffer(abyBuffer, dwStdErr)
            end;

          until (dwRet = WAIT_OBJECT_0);        // コンソールアプリのプロセスが存在している間
          GetExitCodeProcess(ProcessInfo.hProcess, Result);
        finally
          CloseHandle(ProcessInfo.hProcess);
          CloseHandle(ProcessInfo.hThread);
          CloseHandle(hStdInReadPipe);
        end;
      end
      else
        raise ELaunchFailed.Create('CreateProcess() failed!');
    finally
      CloseHandle(hErrReadPipe);
      CloseHandle(hErrWritePipe);
    end;
  finally
    CloseHandle(hReadPipe);
    CloseHandle(hWritePipe);
  end;
end;

解説

引数 sCommandLine で指定されたアプリケーションを起動し, その標準入力として TStreamStdIn を与え, アプリケーションの標準出力とエラー出力を TStream のインスタンスに入れて 返す。

上記の動作を実現するために,CreatePipe() 関数によって無名パイプを作成し, それの一端を起動させるアプリケーションの入出力にあてがっている。たとえば DOS や Unix で、

grep foo | sort | uniq

としたときには,grep の結果を並び替えて同一行を削除するが,これらのコマンドの間にあるのが パイプで,一方の出力をそのままもう一方の入力へ流し込むときによく用いる。 この関数では,GUI 側からパイプに書き込むと,それがそのままコンソールアプリの標準入力へ 流れるようになっているし,コンソールアプリの標準出力に関してはその逆となっている。

パイプと与えるデータが用意できたら,あとはコンソールアプリが終了するまで, 標準出力とエラー出力を取り出して,ストリームに収める。

使い方

新規フォームを作成し、TButton を 1 個、TMemo を 3 個、TEdit を 1 個置いて、 TButton の onClick に次のようなコードを書く。

GrabStdOut() の使い方
procedure TForm1.Button1Click(Sender: TObject);
var
  stdOut, stdIn, stdErr: TStream;
begin
  stdOut := TMemoryStream.Create;
  stdIn := TMemoryStream.Create;
  stdErr := TMemoryStream.Create;
  try
    try
      memo1.Lines.SaveToStream(stdIn);
      stdIn.Position := 0;                        // これを忘れると標準入力を与えられない
      GrabStdOut(Edit1.Text, stdin, stdout, stderr);
      stdOut.Position := 0; stdErr.Position := 0; // これをしないと memo に入らない
      memo2.Lines.LoadFromStream(stdOut);
      memo3.Lines.LoadFromStream(stdErr);
    except
      on ELaunchFailed do
      begin
        Application.MessageBox('CreateProcess() failed!', 'ヽ(`Д´)ノ', 16);
      end;
    end;
  finally
    stdOut.Free;
    stdIn.Free;
    stdErr.Free;
  end;
end;

TForm1 のユニットに GrabStdOut() を、名前をあわせつつコピペして実行する。

TEdit にコマンドラインを、Memo1 に標準入力の内容を入れて Button1 を押すと、 Memo2 に標準出力、Memo3 にエラー出力が入る。プロセスの起動に失敗すると ダイアログを出す。

応用

ストリームへの書き込みの代わりにコールバックを呼ぶようにすれば、 コンソールアプリの終了を待たずに、吐き出したデータを利用できるようになる。 スレッド化するとさらに美味しくなると思う。

出典

モルタルコのプログラマ日記 2000.05

auROra の公式サイト告知取得機能のソース TranslateBySED 関数

改版履歴

初版はいつ書いたんだっけ…….

2004/01/30
  • 「概要」に加筆修正.
2005/07/29
  • プロトタイプを TStream に変更、エラー出力にも対応
  • CreateProcess() 失敗時例外を投げるようになった
  • 戻り値を DWORD で返すようになった
  • 加筆修正
2005/08/25
  • 本記事のネタ元を「出典」に追記
2006/08/26
  • コンソールアプリが終了間際にはいたデータを取りこぼす可能性があったのを修正
  • CPU 負荷を軽減した

Autch.net > 小ネタ > GUI アプリからコンソールアプリを実行するには Last-Modified: 2008-09-10 16:40:03 (JST)
Valid XHTML 1.0! Valid CSS! Made with Cascading Style Sheets Powered by PHP Powered by Smarty
転載・引用・リンク・アンリンク自由。一切のコンテンツは無保証。
Copyright © 2000 - 2008, Autch.net. "gray_forest" theme designed by OCEAN-NET.