UWPアプリにPythonでコーディングした処理を組み込む【UWP】

ここではUWPアプリにPythonでコーディングした処理を組み込む方法を説明します。UWPは強力なプラットフォームですが、統計処理や数値計算、バイオインフォマティクス、機械学習などの最新の研究にはPythonがよく用いられています。そこで、UWPアプリにPythonの処理を組み込む方法を見ていきましょう。

UWPアプリからPythonプログラムを起動する方法

UWPは強力なプラットフォームで、アプリ開発に必要な要素は一通りそろっていますが、それでもプラットフォームによって得意不得意はあるものです。例えば、統計処理や数値計算、バイオインフォマティクス分野などはC#よりもPythonを用いた処理が一般的であり、実績もあります。そのため、UWPアプリにそれらの処理を組み込む場合は、その処理だけはPythonでコーディングしたいところです。他には機械学習の分野ではPythonを用いた研究が一般的であり、最新の研究成果をアプリに生かすにはPythonのプログラムをUWPアプリから起動する必要があります。

それではUWPアプリにPythonプログラムを組み込むことは可能でしょうか?PythonのスクリプトそのままでUWPアプリに組み込むことはできませんが、以下のようにPythonスクリプトをexeファイル化することで、外部プログラムとして起動することは可能です。

サンプルアプリを作成する

サンプルアプリの概要

ここでは、実際にPythonのプログラムを組み込んだサンプルアプリを作成してみましょう。

統計処理はPythonの得意分野なので、「観測された測定値から、母集団の平均値の区間推定を行う」という統計処理のPythonプログラムをUWPから実行するアプリを作成してみます。

このアプリはMicrosoft Storeへも提出済みで、以下からダウンロードできます。


なお、このサンプルアプリの枠組みは以下の記事のものをもとに作成していますので、こちらも併せてご覧ください。

プロジェクトを作成する

新規の「Windowsアプリケーションパッケージプロジェクト」を「Python-UWP_Sample」というプロジェクト名で作成し、ソリューションに次の2つのプロジェクトを追加します。

  • 空白のアプリ(ユニバーサルWindows)
    • ここでは「MainApp」とします
  • コンソールアプリケーション
    • ここでは「Launcher」とします

Python-UWP_Sampleプロジェクトの「アプリケーション」に上記の2つのプロジェクトを追加しておき、MainAppの方をエントリポイントに設定します。続いて、Python-UWP_SampleプロジェクトのPackage.appxmanifestを右クリックして、コードの表示からコードを表示させ、以下のハイライト部分を追加します。

<?xml version="1.0" encoding="utf-8"?>

<Package
  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
  xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
  xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
  IgnorableNamespaces="uap rescap">

  ......省略......

  <Applications>
    <Application Id="App"
      Executable="$targetnametoken$.exe"
      EntryPoint="$targetentrypoint$">
      <uap:VisualElements
        DisplayName="Python-UWP サンプルアプリ"
        Description="Python-UWP サンプルアプリ"
        BackgroundColor="transparent"
        Square150x150Logo="Images\Square150x150Logo.png"
        Square44x44Logo="Images\Square44x44Logo.png">
        <uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" />
        <uap:SplashScreen Image="Images\SplashScreen.png" />
      </uap:VisualElements>
      <Extensions>
        <desktop:Extension Category="windows.fullTrustProcess" Executable="Launcher\Launcher.exe">
          <desktop:FullTrustProcess>
            <desktop:ParameterGroup GroupId="PythonCommand1" Parameters="PythonCommand1"/>
          </desktop:FullTrustProcess>
        </desktop:Extension>
      </Extensions>
    </Application>
  </Applications>

  ......省略......

</Package>

ランチャープログラムから起動するPythonプログラムをUWPアプリ側から選択可能にするために、28行目でFullTrustProcessLauncherの引数として「PythonCommand1」をあらかじめ登録しています。今回作成しているサンプルアプリではPythonプログラムは1つしかないので、実際には引数として登録しておく必要はないのですが、ここではあくまでもサンプルアプリであることを考慮して、拡張性を持たせるようにこのような表現をしています。

最後にUWPAppプロジェクトの参照の追加から「Windows Desktop Extenshons for the UWP」のチェックを入れて、Desktop Bridgeを有効化しましょう。

なお、アプリのパッケージ化の際にx64ではうまくいかなかったので、ここではx86環境とします。ソリューションのプラットフォームをx86に設定しておいてください。

ランチャーから起動させるPythonプログラムを作成する

続いて、引数として渡された数値のリストから母平均の区間推定を行うPythonプログラムを作成しておきます。

mean_interval.py
from scipy.stats import t
import numpy as np
import sys

args = sys.argv
data = []

# 与えられた引数が数値型(float)であるかどうかを判定する関数
def isfloat(s):
  try:
    float(s)
  except:
    return False
  return True

# 引数を取得し、数値型であればdataに格納します
for arg in args:
  if isfloat(arg):
    data.append(float(arg))

# dataの要素の不偏分散・標本平均・標本数・自由度を求めます
u_var = np.var(data, ddof=1)  # 不偏分散
s_mean = np.mean(data)  # 標本平均
n = len(data)  # 標本数
deg_of_freedom = n-1  # 自由度

# 母平均の区間推定を行います
if(u_var == 0):
  print('有効な数値が指定されていません')
elif(n < 2):
  print('有効な数値が指定されていません')
else:
  bottom, up = t.interval(0.95, deg_of_freedom, loc=s_mean, scale=np.sqrt(u_var/n))
  print('母平均の95%信頼区間は ' + str(bottom) + ' ~ ' + str(up) + ' です')

プログラムの実行時に、あるサンプルの標本値を引数として受け取って、正規母集団からの観測地と仮定した場合の母平均の区間推定を行うプログラムで、以下の記事のものをもとに作成しています。

これをC#から呼び出すためには実行形式(exeファイル)としておく必要があるので、PyInstallerで実行形式に変換しましょう。

Anacondaで構築した環境では、NumPyやSciPyを使っているとexeファイルが肥大化してしまうので、以下のように専用の環境を作っておき、NumPyやSciPyはpipでインストールしておきます。なお、ここではx86環境でアプリを作成しているので、Pythonも32bit版を用意する必要があるので、仮想環境上で環境変数CONDA_FORCE_32BITを1に設定しています。また、Anacondaに用意されているWindows 32bit版のPyInstallerはアップデートされていないので、PyInstallerもpipでインストールしておきましょう。

set CONDA_FORCE_32BIT=1
conda create -n env
activate env
conda install python
pip install pyinstaller
pip install numpy
pip install scipy

その上で、Pythonプログラムを保存してあるディレクトリに移動して、以下のコマンドでexeファイル化します。

cd 〇〇〇
pyinstaller mean_interval.py
set CONDA_FORCE_32BIT=

Python-UWP_Sampleプロジェクトに「PythonCommand」というフォルダを作って、生成されたexeファイルの含まれるフォルダの要素をすべてをそこにコピーしておきましょう。

なお、PyInstallerでexeファイル化する際に「–onefile」オプションをつけると1つの実行ファイルとして作成することができ、圧縮されているので容量も減らすことができます。ただし、解凍操作が必要になるためかexeファイルの初回実行に時間がかかる場合があったためにここでは「–onefile」オプションはつけませんでした。

UWPアプリのコーディング

続いてUWPアプリ(MainApp)のコーディングを行います。

MainPage.xaml
<Page  ...(省略)...>

    <StackPanel BorderThickness="20,20,20,20">
        <TextBlock Text="数値を半角スペース区切りで入力して下さい (例:10.2 11.5 9.8 9.5 10.8)" FontSize="20"/>
        <TextBox x:Name="txt_Input" FontSize="24"/>
        <Button x:Name="btn" Content="母集団の平均値を推定" Click="Button_Click" HorizontalAlignment="Center" Margin="0,20,0,0" FontSize="24"/>
        <TextBlock Text="↓" HorizontalAlignment="Center" FontSize="40"/>
        <TextBlock x:Name="txt_Result" FontSize="24" HorizontalAlignment="Center"/>
    </StackPanel>
</Page>
MainPage.xaml.cs
using System;
using System.IO;
using Windows.ApplicationModel;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace MainApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            //引数を取得する
            string parameter = txt_Input.Text;
            ApplicationData.Current.LocalSettings.Values["parameter"] = parameter;

            //識別キーを生成する
            ApplicationData.Current.LocalSettings.Values["key"] = Path.GetRandomFileName();

            //クリップボードを監視して、その内容の変更を検知できるようにする
            Clipboard.ContentChanged += OnContentChanged;

            //処理が終わるまでクリックできないようにする
            btn.IsEnabled = false;

            //コマンド1を指定してランチャーを起動させる
            await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("PythonCommand1");

            txt_Result.Text = "計算中です...";
        }


        private async void OnContentChanged(object sender, object e)
        {
            //クリップボードからのデータ取得に失敗した際の試行回数のカウンター
            int trialCount = 0;

            try
            {
                //クリップボードの内容が変更されたらそれを取得する
                DataPackageView dataPackageView = Clipboard.GetContent();

                //クリップボードに新たにテキストデータが保存された場合
                if (dataPackageView.Contains(StandardDataFormats.Text))
                {
                    //クリップボードに保存されたデータを取得する
                    string result = await dataPackageView.GetTextAsync();

                    //識別キーを取得する
                    string key = (string)ApplicationData.Current.LocalSettings.Values["key"];

                    //クリップボードに保存されたテキストデータが、識別キーで始まる場合
                    if (result.StartsWith(key))
                    {
                        txt_Result.Text = result.Remove(0, key.Length);
                        Clipboard.ContentChanged -= OnContentChanged;  //クリップボードの監視を終了する
                        Clipboard.Clear();
                        btn.IsEnabled = true;
                    }
                }
            }
            catch (Exception ex)
            {
                //クリップボードからのデータ取得に失敗した際は10回まで繰り返す
                if (trialCount < 10)
                {
                    OnContentChanged(sender, e);
                    trialCount++;
                }
                else
                {
                    txt_Result.Text = ex.ToString();
                    btn.IsEnabled = true;
                }
            }
        }
    }
}

FullTrustProcessLauncherの引数として起動する外部プログラム(Pythonプログラム)を指定しています。ここで指定できる引数はマニフェストに登録したものになります。この際、テキストボックスに数値を半角スペース区切りで入力して、ランチャープログラムに渡します。UWPアプリとランチャープログラムとの情報のやり取りはApplicationData.LocalSettingsとクリップボードを使用しています。

ここでのコーディングは以下をもとにしているので、詳細はこちらも併せてご覧ください。

ランチャープログラムのコーディング

最後にPythonプログラムを起動させるためのランチャープログラムを作成しましょう。

Program.cs
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using Windows.Storage;

namespace Launcher
{
    class Program
    {
        [STAThreadAttribute]
        static void Main(string[] args)
        {
            //ルートパスを取得する
            DirectoryInfo assemblyInfo = new DirectoryInfo(Assembly.GetExecutingAssembly().Location);
            string rootPath = assemblyInfo.Parent.Parent.FullName;

            if (args.Length > 2)
            {
                //ランチャーから起動する外部プログラムによって処理を分岐する
                switch (args[2])
                {
                    //「PythonCommand1」を実行する場合
                    case "PythonCommand1":

                        //引数と識別キーを取得する
                        string parameter = (string)ApplicationData.Current.LocalSettings.Values["parameter"];
                        string key = (string)ApplicationData.Current.LocalSettings.Values["key"];

                        //起動する外部プログラムの設定を行う
                        ProcessStartInfo processInfo = new ProcessStartInfo
                        {
                            FileName = rootPath + @"\PythonCommand\mean_interval\mean_interval.exe",
                            Arguments = parameter,
                            CreateNoWindow = true,
                            RedirectStandardOutput = true,
                            UseShellExecute = false
                        };

                        //外部プログラムを起動して処理を待つ
                        Process process = Process.Start(processInfo);
                        process.WaitForExit();

                        //外部プログラムの処理結果を取得し、クリップボードに書き込む(余分な改行は削除します)
                        string output = process.StandardOutput.ReadToEnd().TrimEnd('\r', '\n');
                        System.Windows.Forms.Clipboard.SetText(key + output);

                        break;
                }
            }
        }
    }
}

UWPアプリ側で指定された外部プログラムを起動します。ここでは引数として受け取る「PythonCommand1」を今回作成したPythonプログラム(mean_interval.py)に対応させています。このmean_interval.pyから作成したexeファイルはPython-UWP_Sampleプロジェクトの「PythonCommand\mean_interval」フォルダ内にあるmean_interval.exeなので、33行目でそのパスを指定しています。

ここでのコーディングは以下をもとにしているので、詳細はこちらも併せてご覧ください。

なお、このコードではApplicationData.LocalSettingsにアクセスするためにWinRT APIを用いており、またクリップボードへのアクセスはWindowsFormのAPIを用いています。これらのAPIを使用可能にするためにこのプロジェクトの.csprojファイルを以下のように変更しています。出力タイプもコンソール画面が表示されないように「Windowsアプリケーション」に設定しています。

<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows10.0.17763.0</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
  </PropertyGroup>
 
</Project>

さらに、C#コードの11行目でメインメソッドにSTAThreadAttribute属性を適用させています。


以上でサンプルアプリが完成しました。

これを実行すると次のように入力した数値から「その母集団の平均値を区間推定する」という統計処理を行ってくれます。(外部プログラムを起動する必要があるので処理には若干時間がかかってしまいます)

この程度の簡単な処理ならC#だけでも難しくはないかもしれませんが、複雑な統計解析になってくると情報が豊富なPythonを用いた方が有利になってきます。そのほかにも機械学習などPythonの得意分野をUWPアプリに組み合わせられればその可能性は大きく広がると思います。

Microsoft Store へ提出する

アプリが完成したら、Microsoft Storeに提出してみましょう。Desktop Bridgeを用いて作成したアプリをMicrosoft Storeに提出する際の注意事項など、ここで説明したサンプルアプリをMicrosoft Storeに提出した際の記録は以下の記事をご覧ください。


関連記事・スポンサーリンク

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)