OpenRCFとPythonを連携する方法

本稿では，OpenRCFからPythonで書かれたコードを実行する方法について紹介します．後述する方法では，PythonのコードはPython用の一般的な実行エンジンで動かしつつ，OpenRCF側で入出力の差し替えとPythonコードを実行するタイミングの制御を行います．つまり，（内部的には）OpenRCFとPythonはそれぞれ独立して動作しているため，依存関係のバグが起こりにくい（or 起こらない）はずです．

# 1 準備

### 1.1 Pythonのインストール
普通にPythonをインストールします．ChatGPTなどに聞きながらやれば，問題なく完了するはずです．
https://www.python.org/downloads/
コマンドプロンプトで「python --version」と入力したときに，バージョンが表示されていればOKです．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310597/4935eafb-b520-428e-96c2-f280e0a3f73e.png)

### 1.2 Python用のフォルダとスクリプト（コード）を用意

まず，OpenRCFの「program」フォルダ内に「PythonScripts」というフォルダを新規作成します．フォルダの名前（PythonScripts）は重要なので，変更しないでください．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310597/b9008df0-648e-455d-8441-6cd7dd7437bb.png)
さらに，「PythonScripts」フォルダの中に「test_scripts.py」というファイルを作成します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310597/a02f012e-0a8a-4dfc-9b67-a6a4c8d03d91.png)
適当なエディタ（VS Codeなど）を用いて，test_scripts.pyに以下のようなサンプルコードを書きます．
```python
import sys

a = int(sys.argv[1])  
b = int(sys.argv[2])  

print(a + b)
```
sys.argv[1], sys.argv[2]...がOpenRCF(C#)から受け取る値になります（不要な場合は無くてもＯＫ）．ここではint型としていますが，float型やdouble型にすることも可能です．また，python側でprintした文字列（ここでは a+b の値）は，OpenRCF(C#)側で読み込めるようになります．

### 1.3 OpenRCF側にPython用のクラス（C#）を用意
右クリック→「追加」→「新しい項目」で，「Python.cs」ファイルを作ります．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310597/d9fc9767-20de-43ff-a2fc-372d78194d9d.png)
さらに，「Python.cs」ファイルに下記のコードを書きます（内容は理解せずに丸々コピペしてもＯＫ）．
```C#
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;

namespace OpenRCF
{
    class Python
    {
        private ProcessStartInfo processInfo = new ProcessStartInfo();
        
        public Python()
        {
            processInfo.UseShellExecute = false;
            processInfo.RedirectStandardOutput = true;
            processInfo.RedirectStandardError = true;
            processInfo.CreateNoWindow = true;

            string pathToPythonFolder = @"..\..\PythonScripts\";
            if (Directory.Exists(pathToPythonFolder)) processInfo.WorkingDirectory = pathToPythonFolder;
            else Console.WriteLine("Error : PythonScripts folder not found. Please create it.");

            string pathToPythonExe = @SearchPathToPythonExe();
            if(pathToPythonExe != null) processInfo.FileName = pathToPythonExe;
            else Console.WriteLine("Error : python.exe not found. Please install Python.");
        }

        private static string SearchPathToPythonExe()
        {
            string defaultPythonPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "Python");
            if (!Directory.Exists(defaultPythonPath)) return null;
            var basePathDirs = Directory.GetDirectories(defaultPythonPath);
            var pythonPathList = basePathDirs.Select(dir => Path.Combine(dir, "python.exe")).Where(File.Exists).ToList();
            if (pythonPathList.Any()) return pythonPathList[pythonPathList.Count - 1];
            else return null;
        }

        private static string ToString(object obj)
        {
            if (obj is Array innerArray && !(obj is string))
            {
                return string.Join(" ", innerArray.Cast<object>().Select(x => x.ToString()));
            }
            return obj.ToString();
        }

        public string Execute(string fileName, params object[] sysArgv)
        { 
            if (!fileName.EndsWith(".py")) fileName += ".py";
            if(!File.Exists(processInfo.WorkingDirectory + fileName))
            {
                Console.WriteLine("Error : {0} not found in the {1} directory.", fileName, processInfo.WorkingDirectory);
                return "";
            }
            processInfo.Arguments = fileName;
            if (0 < sysArgv.Length) processInfo.Arguments += " " + string.Join(" ", sysArgv.Select(obj => ToString(obj)));
            Process process = Process.Start(processInfo);
            string printedData = process.StandardOutput.ReadToEnd();
            string errorMsg = process.StandardError.ReadToEnd();
            process.WaitForExit();
            if (!string.IsNullOrWhiteSpace(errorMsg)) Console.WriteLine("Error : " + errorMsg);
            return printedData.Replace("\r\n", "");
        }
    }
}
```
以上で準備完了！

# 2 OpenRCFからPythonのコードを実行し，結果を取得する

### 2.1 Pythonクラスの使用例
OpenRCFの「MainWindow.xaml.cs」ファイルでの使用例は以下の通りです．
```C#
Python Python = new Python();

void Button1_Click(object sender, RoutedEventArgs e)
{
    string output = Python.Execute("test_script.py", 1, 2);
    Console.WriteLine(output);
}
```
ボタン１を押すと，test_script.pyファイルに引数（1, 2）を送り，その引数に対するPythonコードの実行結果（Pythonの中でprintしたもの）を受け取ることができます．つまり，python側で「sys.argv[1]=1」，「sys.argv[2]=2」としてコードを実行し，そのときにprint関数で出力したもの（3）がoutputに格納されてます．

### 2.2 補足
Python側に送るデータが多くなる場合は，以下のように配列で渡すこともできます．
```C#
Python Python = new Python();

void Button1_Click(object sender, RoutedEventArgs e)
{
    int[] input = new int[2] { 1, 2 };
    string output = Python.Execute("test_script.py", input);
    Console.WriteLine(output);
}
```
注意点としては，Python側（test_script.pyファイル）で引数（sys.argv[1], sys.argv[2]など）をfloat型やdouble型にしている場合は，OpenRCF(C#)もそれらの型に合わせて引数を与えてあげる必要があります．
また，もしpython側で引数（sys.argv[1], sys.argv[2]など）を使用していなかった場合は，以下のようにして実行できます．
```C#
Python Python = new Python();

void Button1_Click(object sender, RoutedEventArgs e)
{
    string output = Python.Execute("test_script.py");
    Console.WriteLine(output);
}
```
Pythonのコードを実行しているのはOpenRCFではなく，一般的なPythonの実行エンジンなので，何かPythonのライブラリを使用したい場合は，コマンドプロンプトの方で pip install すると使えるようになります．