LoginSignup
13
9

More than 3 years have passed since last update.

[C#] デスクトップアプリ (WPF) から手軽にWinRT APIを活用しよう

Posted at

はじめに

WinRT (Windowsランタイム) API は Windows 8 以降で実装されている新しいAPIです。
モダンUIになっていて、Win32APIでサポートされていなかった様々な機能が使えるようにもなっています。

↓UIに関していえば、メッセージボックス一つとってもこの違い。(上: WinRT, 下: Win32APIベース (MessageBox.Show()))
image.png
image.png

しかし、WinRTはUWP (Universal Windows Platform) 向け(いわゆるストアアプリ用)な節があるものの、実際にはUWPだと実装しにくい機能も多く、普通のWindowsデスクトップアプリ (WPFなど) からWinRTの機能を「つまみ食い」したいこともあると思います。

C++/WinRTを介して呼び出せる

C++/WinRTでライブラリを作成し、それをC#側から参照することによってWinRT APIの機能を使うことができます。
普通のデスクトップアプリとしてビルドできるので(もちろんWindowsのバージョンが古いと実行できませんが)、Windowsの設定(サイドローディングとか)を変えないと開発できないとか、スタートメニューからしか起動できないといった悩みから解放されます。
[C#] C++/WinRTのブリッジを作ってC#から呼び出す方法 - Qiita

以下のMicrosoftの公式サンプルもこの方法です。
microsoft/RegFree_WinRT: Sample Code showing use of Registration-free WinRT to access a 3rd party native WinRT Component from a non-packaged Desktop app

でももっとお手軽に

とはいえ、ちょっとつまみ食いだけの割には少々ハードルが高いのも事実です。
というわけで、この記事ではもっと手軽にWinRTの機能を呼び出す方法をまとめてみます。

検証環境

  • Windows 10 Home 1903
  • Visual Studio Community 2019

事前準備

C#の WPF App (.NET Core) プロジェクトを作成しておきます。
NuGetパッケージマネージャーから、Microsoft.Windows.SDK.Contracts を追加します。このときターゲットにするWindowsのバージョン (1903とか) に合わせてパッケージのバージョンを指定します。1
今回は1903をターゲットにするために 10.0.18362.2005 を選びました。

プロジェクトのプロパティ「対象のフレームワーク」はデフォルトの .NET Core 3.1 のままにしました。

WPF アプリ (.NET Framework) を選んで .NET Framework (4.5以上) をターゲットにすることも可能ですが、その場合はさらに

  • C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.18362.0\Facade\windows.winmd
  • C:\Program Files (x86)\Windows Kits\10\References\10.0.18362.0\Windows.Foundation.UniversalApiContract\8.0.0.0\Windows.Foundation.UniversalApiContract.winmd
  • C:\Program Files (x86)\Windows Kits\10\References\10.0.18362.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd

への参照を手動で追加する必要がありました。2

コーディング

Microsoft.Windows.SDK.Contracts パッケージを追加しておくと、UWPアプリと同様、Windows 名前空間の下にあるWinRTの機能にアクセスできるようになります。
例えば、こんな感じで MessageDialog クラスにアクセスできるようになります。

MainWindow.xaml.cs
using System;
using System.Windows;
using Windows.UI.Popups;

namespace WpfApp1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            var dlg = new MessageDialog("Hello World!");
            await dlg.ShowAsync();
        }
    }
}

ただし、このプログラムはビルドはできますが、実行してみると例外が発生してしまいます。

System.Exception: 'ウィンドウ ハンドルが無効です。

CoreWindow を使用してこの API をスレッドから呼び出すか、ウィンドウを明示的に設定しておく必要があります。'

この問題を解決するためには、作成した MessageDialog のインスタンスについて、事前に IInitializeWithWindow インタフェースにキャストした上で Initialize() メソッドを呼び出しておきます。このとき、親ウィンドウのハンドルを取得して指定する必要があります。3
ウィンドウハンドルは Process.GetCurrentProcess().MainWindowHandle で取得しているサンプルが多いようですが、メインウィンドウではなく特定のウィンドウを指定したいこともあると思うので、応用が利くように以下では new WindowInteropHelper(this).Handle を使って取得しています。4
Windowsフォーム (WinForms) ベースで開発している方は this.Handle でウィンドウハンドルが取れます。

IInitializeWithWindow の定義はおまじない。5

MainWindow.xaml.cs
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using Windows.UI.Popups;

namespace WpfApp1
{
    [ComImport]
    [Guid("3E68D4BD-7135-4D10-8018-9FB6D9F33FA1")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IInitializeWithWindow
    {
        void Initialize(IntPtr hwnd);
    }

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            var dlg = new MessageDialog("Hello World!");
            ((IInitializeWithWindow)(object)dlg).Initialize(new WindowInteropHelper(this).Handle);
            await dlg.ShowAsync();
        }
    }
}

image.png
昔ながらのUIではなく、Windows 10風のUIでメッセージが表示されたでしょうか。
普通のデスクトップアプリなので、ビルドしたexeから直接起動できます。他のマシンへの配布も簡単です。(配布先のマシンへの .NET Core のインストールは必要ですが)

UIに関係しないAPI

何も考えずにUWPと同じように呼び出せます。
例えば、Windows.Data.Json クラスを使ってJSON文字列を解析できます。6

var obj = Windows.Data.Json.JsonObject.Parse("{\"a\": 1, \"b\": 2}");
System.Diagnostics.Debug.WriteLine(obj["a"].GetNumber());

クリップボードが更新されたときに、コピーされた内容を使って何らかの処理を実行する例。7

Windows.ApplicationModel.DataTransfer.Clipboard.ContentChanged += async (object? sender, object e) =>
{
    Windows.ApplicationModel.DataTransfer.DataPackageView content = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent();
    if (content.Contains(Windows.ApplicationModel.DataTransfer.StandardDataFormats.Text))
    {
        var text = await content.GetTextAsync();
        System.Diagnostics.Debug.WriteLine(text);
    }
};

日本語の形態素解析(文章を単語に分割して読みをつける)も標準機能でできます。8

var result = Windows.Globalization.JapanesePhoneticAnalyzer.GetWords("本日は晴天なり");
foreach (var word in result)
{
    System.Diagnostics.Debug.WriteLine("{0},{1}", word.DisplayText, word.YomiText);
}

UIが関係するAPI

MessageDialog のようにダイアログを表示するような機能については、作成したインスタンスに対して IInitializeWithWindow::Initialize() を一度呼び出しておく必要があります。あとは同じです。

FileOpenPicker であればこんな感じ。9

var openPicker = new FileOpenPicker();
((IInitializeWithWindow)(object)openPicker).Initialize(new WindowInteropHelper(this).Handle);
openPicker.ViewMode = PickerViewMode.Thumbnail;
openPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
openPicker.FileTypeFilter.Add(".jpg");
StorageFile file = await openPicker.PickSingleFileAsync();
if (file != null)
{
    System.Diagnostics.Debug.WriteLine(file.Path);
}

最後に

.NET 5 以降だともっと手軽に呼び出せるようになっているらしい。
.NET 5 から Windows Runtime API を呼ぶのが凄い楽になってる - Qiita

13
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
9