かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

DesktopBridge アプリで UWP アプリと Win32 アプリの連携方法

1つ前の記事で起動方法は紹介しました。

blog.okazuki.jp

起動は出来たら次したいことは連携ですよね。ということでしてみましょう。

仕組み

UWP の AppService を使ってやるのがお勧めです。

docs.microsoft.com

UWP 側で AppService を作っておいて、それを介して WPF などの Win32 アプリと UWP のアプリが連携するようにします。 なのでこんな感じで動くような雰囲気ですね。

  • UWP アプリ起動
  • Win32 アプリ起動
  • Win32 アプリから AppService へ接続
    • UWP アプリ側で AppService が起動する
  • 接続を介してやり取り

やってみよう

私が最後に UWP を勉強したときは別プロセスで AppService が起動してたような気がするのですが最近は UWP アプリと同じプロセスで可能になっててびっくりしました。

docs.microsoft.com

App.xaml.cs の編集

OnBackgroundActivated をオーバーライドして AppServiceTriggerDetails が来てたら AppServiceConnection を取得して各種イベントを初期化します。 メッセージが飛んで来たら RequestReceived イベントが来るのでここで色んな処理をします。

今回は MainPage のテキストを更新するための処理を呼び出しています。 注意点は必ずしも UI スレッドでイベントが発生するわけではないので UI にダイレクトにアクセスするのは NG です。

private AppServiceConnection _appServiceConnection;
private BackgroundTaskDeferral _appServiceDeferral;

protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
    base.OnBackgroundActivated(args);

    if (args.TaskInstance.TriggerDetails is AppServiceTriggerDetails appService)
    {
        _appServiceDeferral = args.TaskInstance.GetDeferral();
        args.TaskInstance.Canceled += TaskInstance_Canceled;
        _appServiceConnection = appService.AppServiceConnection;
        _appServiceConnection.RequestReceived += AppServiceConnection_RequestReceived;
        _appServiceConnection.ServiceClosed += AppServiceConnection_ServiceClosed;
    }
}

private void AppServiceConnection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
{
    _appServiceDeferral?.Complete();
}

private async void AppServiceConnection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
    var d = args.GetDeferral();

    var message = args.Request.Message;
    var input = message["Input"] as string;

    await MainPage.Current?.SetTextAsync(input);
    await args.Request.SendResponseAsync(new ValueSet
    {
        ["Result"] = $"Accept: {DateTime.Now}"
    });
    d.Complete();
}

private void TaskInstance_Canceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
    _appServiceDeferral?.Complete();
}

MainPage 側ではこんな感じ。

<Page x:Class="LaunchApp.UWP.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:LaunchApp.UWP"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock x:Name="textBlock"
                   Style="{ThemeResource HeaderTextBlockStyle}" />
        <Button Content="Launch"
                Click="Button_Click" 
                HorizontalAlignment="Center"
                VerticalAlignment="Center"/>
    </Grid>
</Page>

コードビハインドでは、グローバルにページのインスタンスにアクセスできるような static 変数へのインスタンスへの格納と、テキストを設定する処理を書いています。 テキストを設定する処理では UI スレッド以外からのアクセスにも対応する処理を入れてます。

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.ApplicationModel;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace LaunchApp.UWP
{
    public sealed partial class MainPage : Page
    {
        public static MainPage Current { get; private set; }

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

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
        }

        public async Task SetTextAsync(string text)
        {
            void setText()
            {
                textBlock.Text = text;
            }

            if (Dispatcher.HasThreadAccess)
            {
                setText();
            }
            else
            {
                await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                {
                    setText();
                });
            }
        }
    }
}

そして、マニフェストに AppService があることを定義します。このとき定義するのは UWP アプリ(今回の場合はLaunchApp.UWP)ではなく Windows アプリケーション パッケージ プロジェクト(今回の場合は LaunchApp) に定義します。

LaunchApp の Package.appxmanifest を開いて以下のように AppService を定義します。

f:id:okazuki:20180210123054p:plain

これで具体的には、以下のようなタグが Application タグの下に追加されます。

<Extensions>
  <!-- ここから -->
  <uap:Extension Category="windows.appService">
    <uap:AppService Name="InProcessAppService" />
  </uap:Extension>
  <!-- ここまで -->
  <desktop:Extension Category="windows.fullTrustProcess" Executable="LaunchApp.WPF\LaunchApp.WPF.exe" />
</Extensions>

WPF 側の実装

あとは MainWindow でクリックされたときに AppServiceConnection を作って繋ぎに行ってます。

<Window x:Class="LaunchApp.WPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:LaunchApp.WPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <TextBox x:Name="inputTextBox" />
        <Button Content="Send"
                Click="Button_Click" />
        <TextBlock x:Name="logTextBlock" />
    </StackPanel>
</Window>

コードビハインドで実際の通信処理をしてます。

using System;
using System.Diagnostics;
using System.Windows;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;

namespace LaunchApp.WPF
{
    public partial class MainWindow : Window
    {
        private AppServiceConnection _appServiceConnection;
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            if (_appServiceConnection == null)
            {
                _appServiceConnection = new AppServiceConnection();
                _appServiceConnection.AppServiceName = "InProcessAppService";
                _appServiceConnection.PackageFamilyName = Package.Current.Id.FamilyName;
                var r = await _appServiceConnection.OpenAsync();
                if (r != AppServiceConnectionStatus.Success)
                {
                    MessageBox.Show($"Failed: {r}");
                    _appServiceConnection = null;
                    return;
                }
            }

            var res = await _appServiceConnection.SendMessageAsync(new ValueSet
            {
                ["Input"] = inputTextBox.Text,
            });
            logTextBlock.Text = res.Message["Result"] as string;
        }
    }
}

実行して動作確認

こんな感じで動けば成功です。

f:id:okazuki:20180210131023g:plain

コード

プロジェクトは GitHub に公開しています。GitHub のほうのコードは、ここでのコードをちょっと改造して UWP -> WPF 方向の通信も追加しています。

github.com