かずきのBlog@hatena

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

WPF や WinForms などで UWP のコントロールを使う XAML Island(プレビュー)

UWP のコントロールを WPF や WinForms などで使う XAML Island ですが現在プレビュー段階ですが試すことが出来るようになっています。

NuGet パッケージのソースとして https://dotnet.myget.org/F/uwpcommunitytoolkit/api/v3/index.json を追加することで各種 NuGet パッケージを入れることが出来るようになっています。

NuGet パッケージマネージャーの右上の歯車アイコンをクリックして設定するのが一番近道かな。

では試してみようと思います。

.NET Framework 4.7.2 をターゲットにして WPF のプロジェクトを追加して、以下の NuGet パッケージを追加します。

  • Microsoft.Toolkit.Wpf.UI.XamlHost

そして以下のページを参考にして UWP API を使用できるようにします。

Windows 10 向けのデスクトップ アプリを強化する - UWP app developer | Microsoft Docs

f:id:okazuki:20180926115444p:plain

参照に追加して満足しがちですが、追加で winmd ファイルはローカルコピーを False にする手順があるので忘れずにしましょう。

WindowsXamlHost コントロールを MainWindow.xaml に追加します。こんな感じで。ChildChanged イベントは後で初期化コードを書くので追加しておきます。そして、InitialTypeName に表示したいコントロールのクラス名を入れます。

<Window x:Class="WpfApp2.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:WpfApp2"
        xmlns:xamlhost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="450"
        Width="800">
    <Grid>
        <xamlhost:WindowsXamlHost ChildChanged="WindowsXamlHost_ChildChanged"
                                  InitialTypeName="Windows.UI.Xaml.Controls.Button" />
    </Grid>
</Window>

コードビハインドでは、以下のような感じでコードで色々設定します。

using Microsoft.Toolkit.Wpf.UI.XamlHost;
using System;
using System.Windows;
using Windows.UI.Popups;
using UWP = Windows.UI.Xaml.Controls; // いちいち打つのがだるいので UWP でアクセスできるようにしとく

namespace WpfApp2
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void WindowsXamlHost_ChildChanged(object sender, EventArgs e)
        {
            var host = (WindowsXamlHost)sender;
            var button = (UWP.Button)host.Child;
            button.Content = "Hello UWP button";
            button.Click += this.Button_Click;
        }

        private async void Button_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            var dlg = new MessageDialog("Hello from UWP button");
            await dlg.ShowAsync();
        }
    }
}

実行するとこんな感じで動きます。

f:id:okazuki:20180926120522p:plain

ばっちりですね!

カスタムコントロールも試してみよう

さくっといきます。UWP のクラスライブラリプロジェクトを追加します。 作成したクラスライブラリプロジェクトをアンロードして csproj を編集します。まず、Project タグのすぐ下の Import タグの下に以下の定義を追加します。

<PropertyGroup>
  <EnableTypeInfoReflection>false</EnableTypeInfoReflection>
  <EnableXBindDiagnostics>false</EnableXBindDiagnostics>
</PropertyGroup>

csproj の最後の Import タグの下らへんに以下の定義を追加します。HostFrameworkProject タグの値は WPF のアプリのプロジェクト名です。

<PropertyGroup>
  <HostFrameworkProject>TestWPFApp</HostFrameworkProject>
  <PostBuildEvent>md $(SolutionDir)\$(HostFrameworkProject)\bin\$(ConfigurationName)\$(ProjectName)
copy $(TargetDir)\*.* $(SolutionDir)\$(HostFrameworkProject)\bin\$(ConfigurationName)\$(ProjectName)
copy $(ProjectDir)\*.xaml $(SolutionDir)\$(HostFrameworkProject)\bin\$(ConfigurationName)\$(ProjectName)
copy $(ProjectDir)\*.xaml.cs $(SolutionDir)\$(HostFrameworkProject)\$(ProjectName)
copy $(ProjectDir)\obj\$(PlatformName)\$(ConfigurationName)\*.g.* $(SolutionDir)\$(HostFrameworkProject)\$(ProjectName)</PostBuildEvent>
</PropertyGroup>

そして、WPF プロジェクトの下に先ほど作った UWP のクラスライブラリプロジェクトと同じ名前のフォルダを作成します。

f:id:okazuki:20180926121906p:plain

クラスライブラリプロジェクトを再読込したあとに適当にコントロールを自分で作ります。私は ListView にデータを表示してみたいなと思ったので、追加で 1 つ .NET Standard のクラスライブラリプロジェクトも追加して表示するデータのクラスを追加したりしました。

// .NET Standard ライブラリに追加した Data.cs
namespace ClassLibrary2
{
    public class Data
    {
        public string Text { get; set; }
    }
}

UWP のクラスライブラリと WPF のアプリのほうに上記 Data クラスを含んだ .NET Standard ライブラリの参照を追加して、以下のようなユーザーコントロールを UWP のクラスライブラリに追加します。

// MyUserControl1.xaml
<UserControl x:Class="ClassLibrary1.MyUserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="using:ClassLibrary1"
             xmlns:data="using:ClassLibrary2"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             d:DesignHeight="300"
             d:DesignWidth="400">

    <Grid>
        <ListView ItemsSource="{x:Bind ItemsSource}"
                  SelectionChanged="ListView_SelectionChanged">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="data:Data">
                    <TextBlock Text="{x:Bind Text}"
                               Style="{ThemeResource BodyTextBlockStyle}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</UserControl>
// MyUserControl1.xaml.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// ユーザー コントロールの項目テンプレートについては、https://go.microsoft.com/fwlink/?LinkId=234236 を参照してください

namespace ClassLibrary1
{
    public sealed partial class MyUserControl1 : UserControl
    {


        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ItemsSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(MyUserControl1), new PropertyMetadata(null));

        public event EventHandler<SelectionChangedEventArgs> SelectionChanged;

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

        private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            SelectionChanged?.Invoke(this, e);
        }
    }
}

ビルドのターゲットを x86 か x64 にしてビルドをして、WPF のプロジェクトですべてのファイルを表示を選択するとクラスライブラリと同じ名前のフォルダに 4 つファイルが出来てると思いますので、それをプロジェクトに含めます。

f:id:okazuki:20180926123038p:plain

この方法だと WPF アプリより先にUWP のクラスライブラリがビルド出来てないと不都合がありそうなのでソリューションのプロパティでプロジェクトの依存関係をよしなに設定しておくとトラブルが少ないとおもいます。

そして、MainWindow.xaml を編集して WindowsXamlHost でホストするコントロールを先ほど作成したユーザーコントロールにします。

<Window x:Class="WpfApp2.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:WpfApp2"
        xmlns:xamlhost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="450"
        Width="800">
    <Grid>
        <xamlhost:WindowsXamlHost ChildChanged="WindowsXamlHost_ChildChanged"
                                  InitialTypeName="ClassLibrary1.MyUserControl1" />
    </Grid>
</Window>

コードビハインドはこんな感じ。

// MainWindow.xaml.cs
using ClassLibrary2;
using Microsoft.Toolkit.Wpf.UI.XamlHost;
using System;
using System.Linq;
using System.Windows;
using Windows.UI.Popups;

namespace WpfApp2
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void WindowsXamlHost_ChildChanged(object sender, EventArgs e)
        {
            var host = (WindowsXamlHost)sender;
            var userControl = (ClassLibrary1.MyUserControl1)host.Child;
            if (userControl == null)
            {
                return;
            }

            userControl.ItemsSource = Enumerable.Range(1, 100).Select(x => new Data
            {
                Text = $"Data: {x}",
            });
            userControl.SelectionChanged += this.UserControl_SelectionChanged;
        }

        private async void UserControl_SelectionChanged(object sender, Windows.UI.Xaml.Controls.SelectionChangedEventArgs e)
        {
            var data = e.AddedItems.OfType<Data>().FirstOrDefault();
            if (data == null)
            {
                return;
            }

            var dlg = new MessageDialog($"{data.Text} selected");
            await dlg.ShowAsync();
        }
    }
}

動いた動いた。

f:id:okazuki:20180926124737p:plain

公式ドキュメント

基本的に手順は全てここに書いてあります。

docs.microsoft.com