クリップボードを介してUWPアプリと外部プログラムを連携させる【UWP】

UWPアプリと外部プログラムを連携させる方法としてはAppServiceを使うのが標準的ですが、外部プログラム側でAppServiceに対応できないケースもあります。そのような場合の連携の方法としてここではクリップボードを介して連携する方法を説明します。

概要

Desktop Bridgeを使えば、UWPアプリからFullTrustProcessLauncherを用いて外部プログラムを起動することができます。(詳細は以下の記事をご覧ください)

これを使えばUWPアプリでは実現できないような機能を外部スクリプトにして実行させることもできます。UWPアプリと外部プログラムとはAppServiceを用いることで双方向の連携をすることができますが、この方法では外部プログラム側からもAppServiceに接続する必要があります。しかし、外部プログラム側の制限でAppServiceに接続できないケースもあるかもしれません。そもそも、アプリの処理の一部を外部プログラムに行わせるだけであれば、その処理の入力と出力だけやり取りできれば十分なのでAppServiceを用いた連携までは不要かもしれません。ここではその場合の連携の方法として、アプリ間の汎用的なデータ共有法であるクリップボードを用いた方法を考えてみましょう。

想定シナリオ
ここでは「UWPアプリの処理の一部を外部スクリプトで実行する」というようなケースを考えていきます。

サンプルアプリ:文字数カウンター

ここでは入力した文字数をカウントするという簡単なアプリ(文字数カウンター)を作成してみましょう。

処理の流れは以下のようになります。

  1. UWPアプリのテキストボックスに文字を入力して、「文字数カウント」ボタンを押す
  2. 入力された文字列が「文字数カウント」用の外部プログラムに渡される
  3. 外部プログラムによる文字数カウントの結果が再びUWPアプリに返される
  4. UWPアプリで文字数カウントの結果を表示する

ここでは、2と3のデータのやり取りにクリップボードを用いています。では、このアプリの作成方法について見ていきましょう。

「文字数カウンター」を作成する

プロジェクトを作成する

以下の記事に従ってDesktop Bridge用のプロジェクトを作成していきます。

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

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

AppLauncherプロジェクトの「アプリケーション」に上記の2つのプロジェクトを追加しておき、UWPAppの方をエントリポイントに設定します。続いて、AppLauncherプロジェクトの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="文字数カウンター"
        Description="AppLauncher"
        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="ConsoleApp\ConsoleApp.exe" />
      </Extensions>
    </Application>
  </Applications>

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

</Package>

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

なお、実行する際はソリューションのプラットフォームをx86かx64のいずれかにする必要があります。

UWPアプリのコーディング

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

    <StackPanel>
        <TextBox x:Name="txtBox"/>
        <Button Content="文字数カウント" Click="Button_Click"/>
        <TextBlock x:Name="txtBlock"/>
    </StackPanel>
</Page>
MainPage.xaml.cs
using System;
using System.IO;
using Windows.ApplicationModel;
using Windows.ApplicationModel.DataTransfer;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace UWPApp
{
    public sealed partial class MainPage : Page
    {
        //識別キー
        private string key;

        public MainPage()
        {
            this.InitializeComponent();
        }

        //「文字数カウント」ボタンがクリックされたときの処理
        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            //識別キーを生成する
            key = Path.GetRandomFileName();

            //テキストボックスに入力された文字列を取得し、識別キーを加えてクリップボードに保存する
            string txt = txtBox.Text;
            DataPackage data = new DataPackage();
            data.SetText(key + txt);
            Clipboard.SetContent(data);

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

            //「文字数カウント」プログラムを起動する
            await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
        }

        //クリップボードの監視
        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();

                    //クリップボードに保存されたテキストデータが、識別キーで始まる場合
                    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アプリ側のコーディングです。

C#側のコードは、「文字数カウント」ボタンがクリックされると、27行目でその文字列を取得して、28-30行目でそれをクリップボードに保存し、36行目でLaunchFullTrustProcessForCurrentAppAsyncメソッドを用いて「文字数カウント」プログラムを起動するというのが基本的な流れです。すると、この「文字数カウント」プログラムがクリップボードの内容を取得して処理を行い、文字数カウント結果を再びクリップボードに返すので、「文字数カウント」プログラムが終了したらその結果を受け取ればOKです。

しかし実はここで大きな問題があります。このLaunchFullTrustProcessForCurrentAppAsyncメソッドは外部プログラムの起動が完了したかどうかは受け取れても、そのプログラムが終了したかどうかは受け取ることはできないのです。つまり、UWPアプリ側から外部プログラムの処理が完了したがどうかが分からないのです。そこで、ここではクリップボードを監視することで外部プログラムの処理の結果がクリップボードに書き込まれたかどうかを把握しています。33行目でクリップボードの変更を検知するContentChangedイベントに処理を追加しています。

あとは、クリップボードの内容が変更されたらそれを取得すればいいでしょうか?もちろんそれでもいいのですが、外部プログラムの処理には少し時間がかかるのでその間に他のプログラムによってクリップボードが変更されてしまう可能性もありますよね?そこで、クリップボードの変更が「文字数カウント」プログラムによるものかどうかを識別できるようにもうひと手間加えましょう。ここではランダムに生成した文字列を識別キーとして用意して、テキストボックスに入力された文字列に付け加えてクリップボードに保存されるようにしましょう(29行目)。ランダムな文字列の取得は任意の方法で構いませんが、ここではGetRandomFileNameメソッドを用いています。外部プログラム側でもこの識別キーを戻り値に付け加えるようにしておきます。これによりクリップボードに保存された文字列にこの識別キーが含まれていれば「文字数カウント」プログラムの戻り値だと判断することができます(56行目)。これで「文字数カウント」プログラムの結果を取得できれば識別キーを除いてそれを表示させ(58行目)、クリップボードの監視を終了させましょう(59行目)。

外部プログラム(「文字数カウント」プログラム)のコーディング

Program.cs
using System;
using System.Windows.Forms;

namespace ConsoleApp
{
    class Program
    {
        [STAThreadAttribute]
        static void Main(string[] args)
        {
            //識別キーの長さを予め登録しておく
            int keyLength = 12;

            //クリップボードのテキストを取得して、識別キーと文字列を分離する
            string arg = Clipboard.GetText();
            string key = arg.Substring(0, keyLength);
            string txt = arg.Substring(keyLength);

            //文字列の長さをカウントする
            int l = txt.Length;

            //カウント結果と識別キーを再び結合して、クリップボードに保存する
            Clipboard.SetText(key + "文字数は" + l + "文字です。");
        }
    }
}

文字数カウントを行う外部プログラムはクリップボードから取得した文字列の文字数をカウントし、それを再びクリップボードに保存するだけのプログラムです。ただし、クリップボード上で外部プログラムの結果を識別できるように識別キーを用いているので、それを分離する操作が必要です。ここで、この識別キーはGetRandomFileNameメソッドで生成されており、これは12文字のランダムな文字列になるので、12行目でそれを登録しています。

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

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
  </PropertyGroup>

</Project>

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

まとめ

以上で、UWPアプリから実行する外部プログラムに対してクリップボードを介してデータのやり取りをする方法を説明しました。

AppServiceを使う必要もないので、外部プログラム側の改修を最小限に抑えられるのが一番のメリットですが、やり取りするデータがクリップボードで筒抜けになるので、機密性の高い情報のやり取りには向いていません。また、クリップボードに対するアクセスが重なるとエラーになる可能性もあるので、実際のアプリでは例外処理をしっかりと書いておく必要もあります。

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

コメントを残す

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

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