はじめに
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 ファイル名」と入力したときと同じことが実行されるわけです。
Start
メソッドはこのような要望にも答えてくれます。
Process.Start("ファイル名");
Start
メソッドにファイル名だけを渡すと、そのファイル名の拡張子に関連付けられた外部プログラムが起動されます。ここで拡張子の関連付けが正しくできていない場合には例外が発生しますので注意してください。この場合はStartメソッドに渡す2番目の引数をどのような文字列にすればよいかを考えれば簡単です。
Process.Start("sample.exe", "ファイル名1 ファイル名2");
外部プログラムを起動し、結果を受け取る
外部プログラムを単純に起動するだけでなくその結果を受け取りたい場合、Process
クラスのStart
メソッドを実行する前にProcessStartInfo
クラスに各種の設定をすることで、起動時の動作を制御して対応します。
サンプルプログラムではstartbutton_Click
メソッド内に以下のように記述しています。
// プロセスクラスのインスタンスを生成する 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
メソッドが非同期で実行したいメソッドです。
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
は呼び出すメソッドと同じ型の戻り値、同じ型の引数を持たなければいけません。
private delegate void ReadLineDelegate();
最後に、非同期の処理が終了した際に実行したい処理を1つのメソッドにまとめます。サンプルプログラム中ではSearchEnd
メソッドが終了時の実行メソッドとなります。
終了時に呼び出したいメソッドは、必ずIAsyncResult
型の引数を持たなければいけません。また終了時の処理として、必ずdelegate
のEndInvoke
メソッドを呼び出す必要があります。
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
メソッド内で実際に非同期処理を実行している部分を見てみましょう。
// delegateのインスタンス生成時に // 非同期実行するメソッドを引数として渡す ReadLineDelegate rd = new ReadLineDelegate(ReadLine); // BeginInvokeメソッド呼び出し時に // 終了時に実行するメソッドを使ってAsyncCallbackを生成して渡す rd.BeginInvoke(new AsyncCallback(SearchEnd), rd);
非同期処理中にコントロールを操作する
Windowsアプリケーションで非同期処理を記述するにあたっては、「ウィンドウに対する操作はウィンドウを生成したスレッドから行わなければならない」という原則に従わなければなりません。
サンプルプログラムの処理で言うと、
- findstrの処理の結果を読み込む処理はウィンドウを生成したのとは別スレッドで行う。
- 上記の結果をリストボックスに追加する処理はウィンドウを生成したスレッドで行う。
という形にしなければいけません。
ここでもdelegate
を使えば処理を簡単に記述できます。まず、リストボックスにデータを追加する処理を1つのメソッドにまとめて記述します。サンプルプログラムではAddItem
メソッドがその処理になります。
// データをリストボックスに追加し、表示をリフレッシュする private void AddItem(string resultline) { this.resultlist.Items.Add(resultline); this.resultlist.Refresh(); Application.DoEvents(); }
次にAddItem
メソッドを呼び出すためのdelegate
を定義します。
private delegate void AddItemDelegate(string s);
このdelegate
を利用して、Form
クラスのInvoke
メソッドからAddItem
メソッドを呼び出します。Form
クラスが持つInvoke
メソッドを利用すると、そこから呼び出されたメソッドはForm
クラスと同じスレッドで実行されることが保証されています。
前段のReadLine
メソッド中で詳細は後段に記述としていた部分がこの呼び出しにあたります。
// delegateのインスタンス生成時にウィンドウと同じスレッドで // 実行するメソッドを引数として渡す AddItemDelegate aid = new AddItemDelegate(AddItem); // Formクラスが持つInvokeメソッドを利用し、AddItemメソッドを呼び出す this.Invoke(aid, new object[] {resultline});
まとめ
- 外部アプリケーションの起動には
Process.Start
を利用します。 ProcessStartInfo
に詳細な設定をすることで外部プログラムの起動方法を変更し、結果を読み取ることができるようになります。- 非同期処理を実装するには
delegate
を作成し、delegate
のBegeinInvoke
メソッドを利用します。 - 非同期処理中にコントロールを操作するには、やはり
delegate
を作成してコントロールのInvoke
メソッドを利用します。