UWPアプリからFullTrustProcessLauncherを用いて外部プログラムを起動することができますが、この方法では起動できるプログラムが1つだけであったり、引数を自由に設定できないなど様々な制約があります。この制約を回避する方法としてUWPアプリからまずランチャープログラムを起動して、そこから任意の外部プログラムを起動するようにする方法があります。このランチャープログラムはUWPアプリではないので、外部プログラムを起動する際にも制約なく実行することができます。
[toc]UWPアプリから外部プログラムを起動する方法
UWPアプリから外部プログラムを起動する際の制約
Desktop Bridgeを使えば、UWPアプリからFullTrustProcessLauncherを用いて外部プログラムを起動することができます。
ここで起動した外部プログラムには以下のようにして引数を渡すこともできます。
ただし、ここで起動できる外部プログラムは1つだけであり、また外部プログラムに渡す引数の組み合わせをあらかじめマニフェストで宣言しておく必要があるなど、大きな制約があります。
そこで次に示すようにUWPアプリからランチャープログラムを起動して、そのランチャーを介して外部プログラムを起動するようにすることで、UWPアプリから複数のプログラムを起動できるようになります。
ランチャープログラムを介して起動する
FullTrustProcessLauncherからは「登録できる外部プログラムは1つだけ」「引数を自由に渡せない」という制約がある一方で、UWPアプリから直接外部プログラムを起動する手段はFullTrustProcessLauncherしかありません。そこで、FullTrustProcessLauncherから外部プログラムとしてランチャープログラムを起動させ、そのランチャープログラムからはProcess.Startメソッドを使って他のプログラムを起動させるようにすれば、UWPアプリから自由に外部プログラムを起動させることができます。
それでは、実際にUWPアプリからランチャープログラムを介して外部プログラムを起動する方法を見ていきましょう。
なお、UWPアプリからランチャープログラムを介して外部プログラムを起動させる方法は以下のサイトにも詳しく解説されており、実際にMicrosoft Storeに提出されたサンプルアプリもありますので、こちらも併せて参考にしてください。
サンプルアプリを作成する
サンプルアプリの概要
「基本構造はUWPアプリで、UWPだけでは実装するのが難しいいくつかの特殊な処理だけはWin32で実装する」というようなケースを想定してサンプルアプリを作成してみましょう。
サンプルアプリではテキストボックスに文字列を入力して「コマンド1」「コマンド2」を実行すると次のような処理を行います。
- コマンド1 → 入力した文字列をそのまま表示させる
- コマンド2 → 入力した文字列の文字数をカウントして表示させる
なおここでは、UWPアプリとランチャープログラムとの連携はLocalSettingsやクリップボードを用いた簡易的な連携としています。クリップボードを用いたUWPアプリと外部プログラムの連携については以下の記事もご覧ください。
クリップボードを用いるとアプリ内のデータのやり取りが外部からも見られてしまう可能性があり、またクリップボード監視プログラムが常駐しているとアプリの動作が不安定になる可能性もあるので、実際のアプリ開発にはAppServiceを用いた連携をお勧めします。
プロジェクトを作成する
新規の「Windowsアプリケーションパッケージプロジェクト」を「SampleApp」というプロジェクト名で作成し、ソリューションに次の2つのプロジェクトを追加します。
- 空白のアプリ(ユニバーサルWindows)
- ここでは「UWPApp」とします
- コンソールアプリケーション
- ここでは「Launcher」とします
SampleAppプロジェクトの「アプリケーション」に上記の2つのプロジェクトを追加しておき、UWPAppの方をエントリポイントに設定します。続いて、SampleAppプロジェクトの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="SampleApp"
Description="SampleApp"
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="Command1" Parameters="command1"/>
<desktop:ParameterGroup GroupId="Command2" Parameters="command2"/>
</desktop:FullTrustProcess>
</desktop:Extension>
</Extensions>
</Application>
</Applications>
......省略......
</Package>
28-29行目ではランチャーから起動する外部プログラムを引数で指定できるようにあらかじめ登録しているので、ここは適宜変更してください。なお、外部プログラムに引数を渡して実行する方法については以下もご参照ください。
最後にUWPAppプロジェクトの参照の追加から「Windows Desktop Extenshons for the UWP」のチェックを入れて、Desktop Bridgeを有効化しましょう。
なお、実行する際はソリューションのプラットフォームをx86かx64のいずれかにする必要があります。
ランチャーから起動させる外部コマンドを作成する
続いて、ランチャーから起動させる外部コマンドを作成します。ソリューションに以下の2つの新しいプロジェクトを追加しましょう。
- Command1 (コンソールアプリケーション)
- Command2 (コンソールアプリケーション)
ここで作成したプロジェクトもSampleAppプロジェクトの「アプリケーション」に追加しておきます。
それぞれ以下のようにプログラムを作成します。
Program.cs (Command1)
using System;
namespace Command1
{
class Program
{
static void Main(string[] args)
{
if (args.Length > 0)
{
Console.WriteLine("Command1出力:" + args[0]);
}
else
{
Console.WriteLine("文字列を入力してください。");
}
}
}
}
Command1のプログラムは実行時の引数として取得した文字列に「Command1出力:」という文字列を付加して標準出力に返すプログラムです(11行目)。
Program.cs (Command2)
using System;
namespace Command2
{
class Program
{
static void Main(string[] args)
{
if (args.Length > 0)
{
//文字列の長さをカウントする
int l = args[0].Length;
Console.WriteLine("Command2出力:文字数は" + l + "文字です。");
}
else
{
Console.WriteLine("文字列を入力してください。");
}
}
}
}
Command2のプログラムは実行時の引数として取得した文字列の文字数をカウントして「Command2出力:」という文字列を付加して標準出力に返すプログラムです(12-13行目)。
UWPアプリのコーディング
MainPage.xaml
<Page ...(省略)...>
<StackPanel>
<TextBox x:Name="txtBox"/>
<Button x:Name="btn1" Content="コマンド1" Click="btn1_Click"/>
<Button x:Name="btn2" Content="コマンド2" Click="btn2_Click"/>
<TextBlock x:Name="txtBlock"/>
</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 UWPApp
{
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
//コマンド1の実行
private async void btn1_Click(object sender, RoutedEventArgs e)
{
//引数を取得する
string parameter = txtBox.Text;
ApplicationData.Current.LocalSettings.Values["parameter"] = parameter;
//識別キーを生成する
ApplicationData.Current.LocalSettings.Values["key"] = Path.GetRandomFileName();
//クリップボードを監視して、その内容の変更を検知できるようにする
Clipboard.ContentChanged += OnContentChanged;
//コマンド1を指定してランチャーを起動させる
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("Command1");
}
//コマンド2の実行
private async void btn2_Click(object sender, RoutedEventArgs e)
{
//引数を取得する
string parameter = txtBox.Text;
ApplicationData.Current.LocalSettings.Values["parameter"] = parameter;
//識別キーを生成する
ApplicationData.Current.LocalSettings.Values["key"] = Path.GetRandomFileName();
//クリップボードを監視して、その内容の変更を検知できるようにする
Clipboard.ContentChanged += OnContentChanged;
//コマンド2を指定してランチャーを起動させる
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("Command2");
}
//クリップボードの監視
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))
{
txtBlock.Text = result.Remove(0, key.Length);
Clipboard.ContentChanged -= OnContentChanged; //クリップボードの監視を終了する
Clipboard.Clear();
}
}
}
catch (Exception ex)
{
//クリップボードからのデータ取得に失敗した際は10回まで繰り返す
if(trialCount < 10)
{
OnContentChanged(sender, e);
trialCount++;
}
else
{
txtBlock.Text = ex.ToString();
}
}
}
}
}
まずはUWPアプリ側のコーディングです。
19-33行目で「コマンド1」ボタンをクリックしたときの処理を、36-50行目で「コマンド2」ボタンをクリックしたときの処理を書いています。UWPアプリから外部プログラムにFullTrustProcessLauncherで直接渡せる引数はあらかじめマニフェストに登録したものだけに限定されているので、ここにはランチャーに起動させる外部プログラムを指定しています。UWPアプリではApplicationData.LocalSettingsというアプリ内で共有できる主に設定情報などのデータ保存スペースが確保されているので、そちらに起動した外部プログラムに渡したい引数を保存しましょう。また、戻り値はクリップボードから取得するようにしているので、クリップボードに追加されたデータがここで起動したコマンドの戻り値であることを識別できるようにランダムな識別キーを発行して、それもApplicationData.LocalSettingsに保存してランチャーに渡しています。UWPアプリ側では外部プログラムの終了を検知できないので、代わりにクリップボードを監視しておいてデータが書き込まれるのを待ちましょう。
53-94行目でクリップボード監視の処理を書いています。クリップボードに新たなデータが書き込まれたら、それが先ほど発行した識別キーを含んでいるかどうかを判定します。識別キーが一致すればそのデータを取得して、クリップボードの監視を終了させます(73-78行目)。
ランチャープログラムのコーディング
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])
{
//「コマンド1」を実行する場合
case "command1":
//引数と識別キーを取得する
string parameter = (string)ApplicationData.Current.LocalSettings.Values["parameter"];
string key = (string)ApplicationData.Current.LocalSettings.Values["key"];
//起動する外部プログラムの設定を行う
ProcessStartInfo processInfo = new ProcessStartInfo
{
FileName = rootPath + @"\Command1\Command1.exe",
Arguments = parameter,
CreateNoWindow = true,
RedirectStandardOutput = true,
UseShellExecute = false
};
//外部プログラムを起動して処理を待つ
Process process = Process.Start(processInfo);
process.WaitForExit();
//外部プログラムの処理結果を取得し、クリップボードに書き込む
string output = process.StandardOutput.ReadToEnd();
System.Windows.Forms.Clipboard.SetText(key+output);
break;
//「コマンド2」を実行する場合
case "command2":
parameter = (string)ApplicationData.Current.LocalSettings.Values["parameter"];
key = (string)ApplicationData.Current.LocalSettings.Values["key"];
processInfo = new ProcessStartInfo
{
FileName = rootPath + @"\Command2\Command2.exe",
Arguments = parameter,
CreateNoWindow = true,
RedirectStandardOutput = true,
UseShellExecute = false
};
process = Process.Start(processInfo);
process.WaitForExit();
output = process.StandardOutput.ReadToEnd();
System.Windows.Forms.Clipboard.SetText(key + output);
break;
}
}
}
}
}
続いてUWPアプリから起動するランチャープログラムの記述です。
14-15行目で今実行しているアプリのルートフォルダを取得して、ランチャーから同一パッケージ内の他のプログラムを起動するための実行ファイルのパスを取得できるようにしています。ランチャーから起動する外部プログラムは直接引数として指定されるので、19行目でその条件分岐を行っています。
24-48行目では「コマンド1」を実行するための処理を書いています。まずはUWPアプリ側から保存したApplicationData.LocalSettingsは同一パッケージ内の他のプログラムからもアクセスできるので、まずはここで起動する外部プログラムに渡す引数を取得しましょう。また同時に戻り値を識別するための識別キーも取得しておきます(27-28行目)。続いて起動するプログラムの設定を行い、それを起動しましょう。外部プログラムを実行してそのプロセスを取得する方法は以下の記事もご覧ください。
45-46行目で起動した外部プログラムの結果を取得し、それを識別キーを合わせてクリップボードに書き込みます。
なお、このコードでは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属性を適用させています。
以上でサンプルアプリの完成です。これで、UWPアプリの中に特定の処理を行う外部コマンドを組み込むことが可能になりました。最後に、ソリューションのプラットフォームをx86かx64のいずれかにして「SampleApp」を起動させてみましょう。
コメント