Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

非同期でfindstrを実行する文字列検索ツールの作成

外部プロセスの起動と非同期処理の実行

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2005/06/01 10:00

コマンドラインツールであるfindstrをWindowsアプリケーションから呼び出すサンプルプログラムを例に、.NET Frameworkで外部プログラムを実行する方法、および非同期で処理を実行する方法を説明します。

文字列検索ツールの実行例
文字列検索ツールの実行例

はじめに

 Windowsのコマンドラインツールにfindstrという文字列を検索するツールがあります。複数のファイルを対象に、特定の文字列がそのファイル中に存在するかを検索するにはfindstrは便利なツールなのですが、コマンドラインから実行するツールであるため残念ながら使い勝手があまりよくありません。そこで、Windowsアプリケーションからfindstrを呼び出して使い勝手のよい文字列検索ツールを作成してみました。

 このツールを例に、.NET Frameworkから外部のプログラムを実行する方法と非同期で処理を実行する方法を説明します。

対象読者

 .NET Frameworkを用いてWindowsアプリケーションを開発している方。

必要な環境

 .NET Framework 1.1がインストールされたWindows XPマシン。

サンプルプログラムの概要

 このサンプルプログラムでは、上段のテキストボックスに検索するフォルダを入力します。ここでフォルダボタンをクリックし、フォルダの参照ダイアログボックスから検索したいフォルダを選択することも可能です。下段のテキストボックスには検索する文字列を入力します。オプションとしてサブフォルダの検索、大文字小文字の区別を設定して検索ボタンをクリックすると、指定したフォルダ内のすべてのファイルに対して文字列の検索が実行されます。

 検索した結果としてリストボックスにはファイル名、行番号、行の内容が表示されます。この結果をダブルクリックするとメモ帳が起動され、選択された結果のファイルがメモ帳内に表示されます。

外部プログラムを起動する

 外部プログラムを起動する場合の簡単な例として、メモ帳を表示する部分のプログラムを見てみましょう。

 サンプルプログラムでは、resultlist_DoubleClickメソッド内に以下の記述があります。

メモ帳を起動する
System.Diagnostics.Process.Start("notepad.exe", fname);

 外部プログラムを起動するにはSystem.Diagnostics名前空間に含まれるProcessクラスのStartメソッドを利用します。

 ここでは最初の引数にメモ帳を指定しています。この引数を、自分がよく使っているテキストエディタを指すように変更すれば、メモ帳ではなくそのエディタを起動することができます。

 また、2番目の引数にはファイル名を設定しています。この2番目の引数は最初の引数である「notepad.exe」の起動時に引数として渡されます。コマンドラインから「notepad.exe ファイル名」と入力したときと同じことが実行されるわけです。

補足説明1:拡張子の関連づけを利用して外部プログラムを起動する
 サンプルでは必ずメモ帳を起動してその中にファイルの内容を表示するようにしていますが、.txtファイルだったらメモ帳で表示、.csファイルだったらVisual Studio .NETで表示、といったようにファイルの種類によって表示するソフトを変更したい場合もあると思います。実はStartメソッドはこのような要望にも答えてくれます。
Process.Start("ファイル名");
 Startメソッドにファイル名だけを渡すと、そのファイル名の拡張子に関連付けられた外部プログラムが起動されます。ここで拡張子の関連付けが正しくできていない場合には例外が発生しますので注意してください。
補足説明2:外部プログラムに複数の引数を渡す
 メーリングリストや掲示板で、外部プログラムの起動時に複数の引数を渡したい場合にどうしたらよいか、という話題が出たことがあります。たとえば「sample.exe ファイル名1 ファイル名2」としたい場合です。
 この場合はStartメソッドに渡す2番目の引数をどのような文字列にすればよいかを考えれば簡単です。
Process.Start("sample.exe", "ファイル名1 ファイル名2");
 このように、1つの文字列の中にすべての引数をスペースで区切って含めれば複数の引数を外部プログラムに渡すことが可能です。

外部プログラムを起動し、結果を受け取る

 外部プログラムを単純に起動するだけでなくその結果を受け取りたい場合、ProcessクラスのStartメソッドを実行する前にProcessStartInfoクラスに各種の設定をすることで、起動時の動作を制御して対応します。

 サンプルプログラムではstartbutton_Clickメソッド内に以下のように記述しています。

findstrを起動する
// プロセスクラスのインスタンスを生成する
this.findProcess = new System.Diagnostics.Process();

// プロセスからの出力をリダイレクトするために
// UseShellExecuteプロパティをfalseに設定する(※)
this.findProcess.StartInfo.UseShellExecute = false;
// プロセスからの出力をProcess.StandardOutputにリダイレクトする(※)
this.findProcess.StartInfo.RedirectStandardOutput = true;

// プロセス用の新しいウィンドウを起動しない
this.findProcess.StartInfo.CreateNoWindow = true;

// 起動する外部プログラムを設定する
this.findProcess.StartInfo.FileName = "findstr.exe";
// 外部プログラムに渡す引数を設定する
if(this.subfolderchk.Checked)
    this.findProcess.StartInfo.Arguments += "/S ";
if(this.upperchk.Checked)
    this.findProcess.StartInfo.Arguments += "/I ";
this.findProcess.StartInfo.Arguments +=
    "/N \"" + findstr + "\" " + searchfiles;

// 外部プログラムを起動する
this.findProcess.Start();

 重要なのは※印の部分の設定です。この設定により、findstrの実行結果はStreamReaderであるthis.findProcess.StandardOutputに出力されます。出力された結果を読み込むにはReadLineメソッド等を利用することになります。

非同期で処理を実行する

 サンプルプログラムでは、時間がかかるfindstrの実行中であっても、中断ボタンを押す等の作業ができるように非同期で処理を実行するようにしています。

 .NET Frmaeworkでは、この非同期処理が簡単に記述できるようになっています。

 まず最初の作業は、非同期で実行したい処理をまとめて1つのメソッドにします。サンプルプログラム中ではReadLineメソッドが非同期で実行したいメソッドです。

ReadLineメソッド
private void ReadLine()
{
    // findstrの結果を1行読み込む
    string resultline = this.findProcess.StandardOutput.ReadLine();
    // findstrの処理が終了するまで繰り返し
    while(resultline != null)
    {
        // 読み込んだデータをリストボックスに追加する
        // メソッドの呼び出し(詳細は後段に記述)
        AddItemDelegate aid = new AddItemDelegate(AddItem);
        this.Invoke(aid, new object[] {resultline});
        // findstrの結果の再読み込み
        resultline = this.findProcess.StandardOutput.ReadLine();
    }
}

 次に、ReadLineメソッドを呼び出すために利用するdelegateを定義します。delegateは呼び出すメソッドと同じ型の戻り値、同じ型の引数を持たなければいけません。

ReadLineDelegate
private delegate void ReadLineDelegate();

 最後に、非同期の処理が終了した際に実行したい処理を1つのメソッドにまとめます。サンプルプログラム中ではSearchEndメソッドが終了時の実行メソッドとなります。

 終了時に呼び出したいメソッドは、必ずIAsyncResult型の引数を持たなければいけません。また終了時の処理として、必ずdelegateEndInvokeメソッドを呼び出す必要があります。

SearchEndメソッド
private void SearchEnd(IAsyncResult iar)
{
    string messagestr;

    // 非同期処理を終了させる
    ReadLineDelegate rd = (ReadLineDelegate) iar.AsyncState;
    rd.EndInvoke(iar);

    // ボタンの状態から検索を終了したか中断したかを判断する
    if(this.stopbutton.Enabled)
    {
        messagestr = "検索が終了しました";
        this.stopbutton.Enabled = false;
    }
    else
    {
        messagestr = "検索を中断しました";
    }
    this.startbutton.Enabled = true;
    // 検索終了時のメッセージを表示する
    MessageBox.Show(messagestr);
}

 これで非同期処理の実行の準備は終了です。delegateのインスタンスを生成し、BeginInvokeメソッドを呼び出せば非同期で処理が開始されます。startbutton_Clickメソッド内で実際に非同期処理を実行している部分を見てみましょう。

SearchEndメソッド
// delegateのインスタンス生成時に
// 非同期実行するメソッドを引数として渡す
ReadLineDelegate rd = new ReadLineDelegate(ReadLine);
// BeginInvokeメソッド呼び出し時に
// 終了時に実行するメソッドを使ってAsyncCallbackを生成して渡す
rd.BeginInvoke(new AsyncCallback(SearchEnd), rd);

非同期処理中にコントロールを操作する

 Windowsアプリケーションで非同期処理を記述するにあたっては、「ウィンドウに対する操作はウィンドウを生成したスレッドから行わなければならない」という原則に従わなければなりません。

 サンプルプログラムの処理で言うと、

  • findstrの処理の結果を読み込む処理はウィンドウを生成したのとは別スレッドで行う。
  • 上記の結果をリストボックスに追加する処理はウィンドウを生成したスレッドで行う。

 という形にしなければいけません。

 ここでもdelegateを使えば処理を簡単に記述できます。まず、リストボックスにデータを追加する処理を1つのメソッドにまとめて記述します。サンプルプログラムではAddItemメソッドがその処理になります。

AddItemメソッド
// データをリストボックスに追加し、表示をリフレッシュする
private void AddItem(string resultline)
{
    this.resultlist.Items.Add(resultline);
    this.resultlist.Refresh();
    Application.DoEvents();
}

 次にAddItemメソッドを呼び出すためのdelegateを定義します。

AddItemDelegate
private delegate void AddItemDelegate(string s);

 このdelegateを利用して、FormクラスのInvokeメソッドからAddItemメソッドを呼び出します。Formクラスが持つInvokeメソッドを利用すると、そこから呼び出されたメソッドはFormクラスと同じスレッドで実行されることが保証されています。

 前段のReadLineメソッド中で詳細は後段に記述としていた部分がこの呼び出しにあたります。

ReadLineメソッド(部分)
// delegateのインスタンス生成時にウィンドウと同じスレッドで
// 実行するメソッドを引数として渡す
AddItemDelegate aid = new AddItemDelegate(AddItem);
// Formクラスが持つInvokeメソッドを利用し、AddItemメソッドを呼び出す
this.Invoke(aid, new object[] {resultline});

まとめ

  1. 外部アプリケーションの起動にはProcess.Startを利用します。
  2. ProcessStartInfoに詳細な設定をすることで外部プログラムの起動方法を変更し、結果を読み取ることができるようになります。
  3. 非同期処理を実装するにはdelegateを作成し、delegateBegeinInvokeメソッドを利用します。
  4. 非同期処理中にコントロールを操作するには、やはりdelegateを作成してコントロールのInvokeメソッドを利用します。

参考資料

  1. ステップ 7 ハンズオン:非同期処理を使ったパフォーマンスの向上
  2. ステップ 7 ハンズオン:非同期処理の注意点
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 小野 修司(オノ シュウジ)

    MVP for Visual Developer - Visual C# あおい情報システム株式会社勤務。 .NETに関する話題を扱う「どっとねっとふぁん」を運営。  

All contents copyright © 2005-2019 Shoeisha Co., Ltd. All rights reserved. ver.1.5