かずきのBlog@hatena

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

WPF などの .NET Framework のアプリから UWP の API を呼ぶ

同じチームの Matteo さんが書いてくれてた記事に ConditionalAttribute が使ってあって、あぁこういう機能あったなぁと思ったのでメモがてら記事をなぞってやってみました。

Desktop Bridge – Enhancing a desktop application with the UWP APIs – App Consult Team

WPF や Windows Forms とかで書かれたアプリから UWP の API は呼べます。UwpDesktop という NuGet パッケージを追加するのがお手軽ではあります。 まぁでも UwpDesktop が何をやってくれるかというのを確認するためにも手動の方法を確認してみましょう。

因みに、今回の方法を使っても全 API が呼べるわけではなく以下にある API がサポートされている API です。

パッケージ デスクトップ アプリで利用可能な UWP API (デスクトップ ブリッジ) - UWP app developer | Microsoft Docs

全部がサポートされてないのは、例えばですが UWP のコントロールを WPF でそもそも出せないとか、そういう色んな制約のせいですね。

手動でやる方法

C:\Program Files (x86)\Windows Kits\10\UnionMetadata\Windows.winmd を参照に追加します。 ただ、デフォルトで dll などの拡張子しか表示しないようになっているので「すべてのファイル」を選択しないと winmd の拡張子のファイルは表示されないので気を付けてください。

f:id:okazuki:20180329091724p:plain

参照先の Blog 記事がトーストを表示してみようという内容だったのでそれにならってやってみましょう。名前空間を省略せずに書くとこのようになります。

public void ShowNotification()
{
    var xml = @"<toast>
            <visual>
                <binding template='ToastGeneric'>
                    <text>Desktop Bridge</text>
                    <text>The file has been created</text>
                </binding>
            </visual>
        </toast>";

    Windows.Data.Xml.Dom.XmlDocument doc = new Windows.Data.Xml.Dom.XmlDocument();
    doc.LoadXml(xml);

    Windows.UI.Notifications.ToastNotification toast = new Windows.UI.Notifications.ToastNotification(doc);
    Windows.UI.Notifications.ToastNotificationManager.CreateToastNotifier().Show(toast);
}

そして、このメソッドを画面に適当に置いたボタンのクリックイベントから呼び出すとエラーになります。(元記事もそうですね)

f:id:okazuki:20180329093141p:plain

UWP の機能の中でも、いくつかは appx という UWP の形式にパッケージングしないと呼べないものがあります。 ということで、以下のような記事の手順で appx にする必要があります。

Windows アプリケーション パッケージ プロジェクトを使って UWP として実行すると無事トーストが出ました。

f:id:okazuki:20180329094220p:plain

appx じゃないバージョンもあるのですが…

これがメモしたかった内容なのですが(機能自体は知ってたけど使ってなかった)appx にパッケージングする方法でないと動かない機能があると困りますよね。既存の exe 配る方法などで。

そんな時は「ビルド→構成マネージャー」でUWP用の構成を作ります。

f:id:okazuki:20180329094746p:plain

プロジェクトのプロパティで条件付きコンパイルシンボルに DesktopUWP のようなものを定義しておきます。

あとは #if ディレクティブでくくるでもいいですし、見通しがいいのは元記事でも行われている ConditionalAttribute を使って UWP に依存する機能はメソッドにまとめておいて、DesktopUWP が定義されてないときは無視するようにするといいです。

private void Button_Click(object sender, RoutedEventArgs e)
{
    ShowNotification();
}

[Conditional("DesktopUWP")]
public void ShowNotification()
{
    var xml = @"<toast>
            <visual>
                <binding template='ToastGeneric'>
                    <text>Desktop Bridge</text>
                    <text>The file has been created</text>
                </binding>
            </visual>
        </toast>";

    Windows.Data.Xml.Dom.XmlDocument doc = new Windows.Data.Xml.Dom.XmlDocument();
    doc.LoadXml(xml);

    Windows.UI.Notifications.ToastNotification toast = new Windows.UI.Notifications.ToastNotification(doc);
    Windows.UI.Notifications.ToastNotificationManager.CreateToastNotifier().Show(toast);
}

こうすると通常の Debug / Release でビルドしたときはトーストの処理が呼ばれなくなります。 DesktopUWP - Debug や Release をもとに同じ手順で DesktopUWP - Reelase とかを作っておくと appx にパッケージングするときに、その構成を選んで行うとトーストが出るようになります。

非同期メソッドを呼ぶには

UWP の API の多くは非同期な API です。そのため await をすることが多いです。 ただ、UWP の API の戻り値は Task ではありません。IAsyncOperator<T> などになります。 そのため、何も考えずに以下のようなコードを書くと…。

public async Task GenerateWavFileAsync()
{
    var speech = new SpeechSynthesizer();
    var result = await speech.SynthesizeTextToStreamAsync("こんにちは世界");

    var fileName = System.IO.Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
        "speech.wav");
    using (var fs = File.Create(fileName))
    {
        await result.AsStreamForRead().CopyToAsync(fs);
        await fs.FlushAsync();
    }
}

エラーになります。

f:id:okazuki:20180329100854p:plain

await するためにはいくつかの条件を満たす必要があるのですが、これを満たしてないということです。 それらは拡張メソッドとして定義されているのですが、その拡張メソッドが定義されているのは以下の dll になるので、それを参照に追加してやる必要があります。

  • C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\System.Runtime.WindowsRuntime.dll

これでエラーが消えます。この SpeechSynthesizer クラスは appx にしなくても呼べるので ConditionalAttribute はいりません。

まとめ

最近は Windows 10 じゃないと使えない API も増えてきました。 そして Pure UWP じゃなくても UWP の API を使わないといけないケースがあると思います。

そんな時はここの手順を試してみてください。Desktop Bridge を使えば、トーストとかにも対応できます。