はじめに
Pythonには機械学習をはじめとする優れたライブラリがたくさんある。一方C#はGUIアプリケーションの開発に広く利用されている言語である。したがってPythonスクリプトをC#アプリケーションから呼び出すことができれば、C#アプリケーション開発者にとって便利であるし、何よりGUIアプリケーションの幅も広がるはずだ。そこで今回、C#のGUIアプリケーションからPythonスクリプトを呼び出す方法について調べ、プロトタイプを作成してみた。
環境
- Windows10
- C#
- Python
開発したいプロトタイプ
開発したいプロトタイプの要件を以下に洗い出してみた。
- Pythonのパス、実行ディレクトリ(Working Directory)、Pythonスクリプトを指定し、実行することができる。
- 実行に長時間かかる場合を考慮し、途中で処理をキャンセルすることができる。
- 実行に長時間かかる場合に進捗状況が分かるよう、全ての標準出力、標準エラー出力をGUIのTextBoxに表示する。
- 以前に書いた 標準入出力を介してMOLファイルをSMILESに変換するのように、標準入力を受け取るPythonスクリプトの場合、ファイルを介さずにGUI側のデータをPythonスクリプトに渡すことにより、実行結果を手軽に受け取ることができる。このため標準入力も指定できるようにしたい。
- Pythonスクリプトの終了コードにより、正常終了かエラー終了かを判定し、MessageBoxにより表示する。
できたもの
画面
画面の説明
- Python Pathには、python.exeの場所をフルパスで指定する。Anacondaの場合は、Anacondaの仮想環境のpython.exeの場所を調べて指定する。
- Working Directoryには、Pythonスクリプトの実行ディレクトリを指定する。引数にファイルパスを指定する場合は、ここを起点とする相対パスで記載することもできる。
- Python Commandには、Pythonスクリプトの場所をフルパスで指定する。また引数があればそれも指定する。
- Standard Input には、Pythonスクリプトの標準入力に渡したいデータを入力する。標準入力を使わないPythonスクリプトの場合、無視される。
- Standard Output and Standard Errorには、Pythonスクリプトの全ての標準出力、標準エラー出力が表示される。
- 「Execute」ボタンにより処理を開始し、「Cancel」ボタンにより処理をキャンセルすることができる。
ソース
ソースは以下の通りだ。とても長くなったが、編集が面倒なためそのまま張り付ける(手抜き)。デザイン側のコードは両略した。GUI部品のオブジェクトの変数名は、コードから読み取ってほしい。ソースの解説は次項で説明する。
usingSystem;usingSystem.Diagnostics;usingSystem.IO;usingSystem.Text;usingSystem.Threading;usingSystem.Windows.Forms;namespacePythonCommandExecutor{publicpartialclassForm1:Form{privateProcesscurrentProcess;privateStringBuilderoutStringBuilder=newStringBuilder();privateintreadCount=0;privateBooleanisCanceled=false;publicForm1(){InitializeComponent();}/// <summary>/// Textboxに文字列追加/// </summary>publicvoidAppendText(Stringdata,Booleanconsole){textBox1.AppendText(data);if(console){textBox1.AppendText("\r\n");Console.WriteLine(data);}}/// <summary>/// 実行ボタンクリック時の動作/// </summary>privatevoidbutton1_Click(objectsender,EventArgse){// 前処理button1.Enabled=false;button2.Enabled=true;isCanceled=false;readCount=0;outStringBuilder.Clear();this.Invoke((MethodInvoker)(()=>this.textBox1.Clear()));// 実行RunCommandLineAsync();}/// <summary>/// コマンド実行処理本体/// </summary>publicvoidRunCommandLineAsync(){ProcessStartInfopsInfo=newProcessStartInfo();psInfo.FileName=this.textBox2.Text.Trim();psInfo.WorkingDirectory=this.textBox3.Text.Trim();psInfo.Arguments=this.textBox4.Text.Trim();psInfo.CreateNoWindow=true;psInfo.UseShellExecute=false;psInfo.RedirectStandardInput=true;psInfo.RedirectStandardOutput=true;psInfo.RedirectStandardError=true;Processp=Process.Start(psInfo);p.EnableRaisingEvents=true;p.Exited+=onExited;p.OutputDataReceived+=p_OutputDataReceived;p.ErrorDataReceived+=p_ErrorDataReceived;p.Start();// 標準入力への書き込みusing(StreamWritersw=p.StandardInput){sw.Write(this.textBox5.Text.Trim());}//非同期で出力とエラーの読み取りを開始p.BeginOutputReadLine();p.BeginErrorReadLine();currentProcess=p;}voidonExited(objectsender,EventArgse){intexitCode;if(currentProcess!=null){currentProcess.WaitForExit();// 吐き出されずに残っているデータの吐き出しthis.Invoke((MethodInvoker)(()=>AppendText(outStringBuilder.ToString(),false)));outStringBuilder.Clear();exitCode=currentProcess.ExitCode;currentProcess.CancelOutputRead();currentProcess.CancelErrorRead();currentProcess.Close();currentProcess.Dispose();currentProcess=null;this.Invoke((MethodInvoker)(()=>this.button1.Enabled=true));this.Invoke((MethodInvoker)(()=>this.button2.Enabled=false));if(isCanceled){// 完了メッセージthis.Invoke((MethodInvoker)(()=>MessageBox.Show("処理をキャンセルしました")));}else{if(exitCode==0){// 完了メッセージthis.Invoke((MethodInvoker)(()=>MessageBox.Show("処理が完了しました")));}else{// 完了メッセージthis.Invoke((MethodInvoker)(()=>MessageBox.Show("エラーが発生しました")));}}}}/// <summary>/// 標準出力データを受け取った時の処理/// </summary>voidp_OutputDataReceived(objectsender,System.Diagnostics.DataReceivedEventArgse){processMessage(sender,e);}/// <summary>/// 標準エラーを受け取った時の処理/// </summary>voidp_ErrorDataReceived(objectsender,System.Diagnostics.DataReceivedEventArgse){processMessage(sender,e);}/// <summary>/// CommandLineプログラムのデータを受け取りTextBoxに吐き出す/// </summary>voidprocessMessage(objectsender,System.Diagnostics.DataReceivedEventArgse){if(e!=null&&e.Data!=null&&e.Data.Length>0){outStringBuilder.Append(e.Data+"\r\n");}readCount++;// まとまったタイミングで吐き出しif(readCount%5==0){this.Invoke((MethodInvoker)(()=>AppendText(outStringBuilder.ToString(),false)));outStringBuilder.Clear();// スレッドを占有しないようスリープを入れるif(readCount%1000==0){Thread.Sleep(100);}}}/// <summary>/// キャンセルボタンクリック時の動作/// </summary>privatevoidbutton2_Click(objectsender,EventArgse){if(currentProcess!=null){try{currentProcess.Kill();isCanceled=true;}catch(Exceptione2){Console.WriteLine(e2);}}}privatevoidbutton3_Click(objectsender,EventArgse){// 標準入力エリアのクリアthis.textBox5.Clear();// 標準出力エリアのクリアthis.textBox1.Clear();}}}
ソース解説
基本的には参考文献の寄せ集めになるのだが、説明を以下に記載する。
- RunCommandLineAsyncメソッド内でProcessクラスによりPythonスクリプトを実行している。
p.Start()
以降は処理が非同期になるため、これ以降UIを操作する場合は、UIスレッドから実行しないと怒られてしまう。this.Invoke((MethodInvoker)(() => AppendText(outStringBuilder.ToString(), false)));
のような呼び出しがところどころあるのはこのためである。 p.EnableRaisingEvents = true;
,p.Exited += onExited;
により、プロセス終了時にonExitイベントハンドラが実行されるため、ここに後始末的な処理や、終了コードの判定、完了ダイアログの表示等を記載している。- キャンセルについては、キャンセル時に実行されるイベントハンドラの中でProcessクラスのKillメソッドを呼び出している。するとonExitイベントハンドラが実行され、通常終了時と同じになるため、それ以外に特別なことはしていない。
- 標準入力にデータを食わせるところは、
using (StreamWriter sw = p.StandardInput)
から始まるところでやっている。 - 標準出力、標準エラー出力の取り出しについては、
p.OutputDataReceived += p_OutputDataReceived;
,ErrorDataReceived += p_ErrorDataReceived;
によりそれぞれのイベントハンドラで受け取った標準出力、標準エラー出力を処理するようにしている。p.BeginOutputReadLine();
,p.BeginErrorReadLine();
により1行出力がある度にそれぞれのイベントハンドラが実行されるため、その中でTextBoxへの出力を行っている。1行毎にTextBoxに書き出すと大量の出力があるアプリの場合にGUIの処理に時間がかかる可能性もあるため、ある程度まとめて出力する等の工夫を行っている。
実行例① 出力の多いPythonスクリプト(途中でエラー)を実行する
以下はサンプルとして作成した、ある程度標準出力や標準エラー出力の多いPythonスクリプトを実行した例である。標準エラーに出力されたエラーメッセージもTextBoxに出力され、かつ終了コードによりエラーのMessageBoxが表示されていることが分かる。
実行例② 標準入力を介してPythonスクリプトを実行する
以下は、「標準入出力を介してMOLファイルをSMILESに変換する」のスクリプトを本GUIを通して実行した図である。簡単なPythonスクリプトを書くだけでC#とPythonの処理結果の受け渡しができることを実感してもらえると思う。
おわりに
- async/awaitを使った方法で当初進めていたが、UIのデッドロックらしき現象が発生し、丸一日かけても解決できなかったため断念した。
- Pythonスクリプトの標準出力に進捗情報を出力することによって、C#側でプログレスバーによる進捗表示も簡単に行えると思う。
- 動作もまずまず安定しているため、このプロトタイプをベースに今後、C#からPythonの便利な機能をガンガン使い、魅力的なアプリを作ってみたい。