rksoftware

Visual Studio とか C# とかが好きです

XAML Islands で表示スケールに対応する

WPF で XAML Islands を使う際に表示スケールがまだサポートされていないことを以前書きました。

まだサポートされていないと言われているのでそれまで。サポートを待てばよいのですがどうにも辛抱たまらんので頑張ってみました。

■ 表示スケール

ハードウェアの性能向上に伴い、PC のディスプレイのピクセルの細かさも上がってきています。ピクセルの細かい環境でこれまでと同じように、例えば文字を表示しようとすると非常に小さな文字になってしまいます。そこで、Windows には全てを拡大して表示する機能があります。
最近はパソコンを買ってセットアップした直後に 150% ~ 200% で設定されていることも多いので気づかず恩恵を受けている方も多いと思います。

■ 表示スケールに対応していない例

次の例は、表示スケール 175% の Windows 上で XAML Islands を使っています。WindowsXamlHost コントロールとその内部の ProgressRing に同じサイズを設定していますが、ProgressRing が小さく表示されてしまっています。 f:id:rksoftware:20190114192214j:plain

■ 対策 Viewbox 先生

UWP には Viewbox という非常に強力なコントロールがあります。このコントロールは内部のコントロールを自分のサイズに合わせて自動的に拡大・縮小して表示してくれます。
つまり、を使っています。WindowsXamlHost の子供として ProgressRing を設定し、どうにかして Viewbox のサイズをうまい事すればそれらしく表示できます。

■ コード

どうにかしてうまい事したコードです。

<Window x:Class="WpfApp1.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:WpfApp1"
        xmlns:xamlhost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="400" Background="LightGray">
    <Grid>

        <xamlhost:WindowsXamlHost x:Name="windowsXamlHost1" Width="300" Height="200"/>

    </Grid>
</Window>
using System;
using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // ProgressRing 生成
            var progressRing = new Windows.UI.Xaml.Controls.ProgressRing
            {
                IsActive = true,
                Width = 300,
                Height = 200,
                Background = new Windows.UI.Xaml.Media.SolidColorBrush(Windows.UI.Colors.AliceBlue),
            };

            // Viewbox 生成
            var viewbox = new Windows.UI.Xaml.Controls.Viewbox
            {
                Stretch = Windows.UI.Xaml.Media.Stretch.Uniform,
            };

            // コントロールを配置
            viewbox.Child = progressRing;
            windowsXamlHost1.Child = viewbox;

            // UWP コントロールの Loaded イベントのタイミングでは Parent が設定されている
            // ※ Window の Loaded イベントのタイミングでは未設定
            viewbox.Loaded += (sender, e) =>
            {
                // Viewbox のサイズを実サイズに調整
                var parent = ((Windows.UI.Xaml.FrameworkElement)viewbox.Parent);
                (viewbox.Width, viewbox.Height) = (parent.Width, parent.Height);
            };
        }
    }
}

f:id:rksoftware:20190114192245j:plain

■ WindowsXamlHost の Child の Parent

WindowsXamlHost の Child の Parent というと何を言っているかよくわからないかと思いますが、WindowsXamlHost の Child の Parent は試してみると動作時に Border コントロールが設定されていました。
そしてこの Border のサイズは実サイズのようです。つまり、WindowsXamlHost の Child に設定したコントロール (Viewbox) から Parent (Border) を取り出して、Viewbox を同じサイズにすればうまい事でそうです。

■ viewbox の Loaded イベント

前述のサイズ設定ですが、今のところ viewbox の Loaded イベントで行うのが良さそうです。
というのも、Window の Loaded イベント時にはまだ UWP 側のコントロール達は準備できておらず Parent プロパティが null です。Window の Loaded イベントから遅延して処理を実行する方法もありますが、Parent プロパティが null でなくなるまでの時間が一定ではなかったので筋が悪いと判断しました。

改良

□ サイズ設定方法の問題

前述のコードはそれなりに動きますが、WindowsXamlHost のサイズ設定と ProgressRing のサイズ設定をうまい事合わせ続ける必要があります。
例えば、ProgressRing のサイズを変えたいなと思って次のようにサイズを設定したとします。

// ProgressRing 生成
var progressRing = new Windows.UI.Xaml.Controls.ProgressRing
{
    IsActive = true,
    Width = 100,
    Height = 100,
    Background = new Windows.UI.Xaml.Media.SolidColorBrush(Windows.UI.Colors.AliceBlue),
};

しかし Viewbox 先生は次のように構わず拡大してしまいます。 f:id:rksoftware:20190114192245j:plain

うまい事するには、WindowsXamlHost のサイズも同じように変更しなければなりませんし、逆もまた同じです。

<xamlhost:WindowsXamlHost x:Name="windowsXamlHost1" Width="100" Height="100"/>

f:id:rksoftware:20190114192325j:plain

□ 対策

Viewbox と ProgressRing の間に Grid などを挟み、挟んだ Grid などのサイズを WindowsXamlHost のサイズと同じにするコードを追加します。

// コントロールを配置
grid.Children.Add(progressRing);
viewbox.Child = grid;
windowsXamlHost1.Child = viewbox;
// UWP コントロールの Loaded イベントのタイミングでは Parent が設定されている
// ※ Window の Loaded イベントのタイミングでは未設定
viewbox.Loaded += (sender, e) =>
{
    // grid のサイズを WindowsXamlHost のサイズに調整
    (grid.Width, grid.Height) = (windowsXamlHost1.Width, windowsXamlHost1.Height);

コード全体は次の様になります。

using System;
using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // ProgressRing 生成
            var progressRing = new Windows.UI.Xaml.Controls.ProgressRing
            {
                IsActive = true,
                Width = 100,
                Height = 100,
                Background = new Windows.UI.Xaml.Media.SolidColorBrush(Windows.UI.Colors.AliceBlue),
            };

            // Grid 生成
            var grid = new Windows.UI.Xaml.Controls.Grid { };

            // Viewbox 生成
            var viewbox = new Windows.UI.Xaml.Controls.Viewbox
            {
                Stretch = Windows.UI.Xaml.Media.Stretch.Uniform,
            };

            // コントロールを配置
            grid.Children.Add(progressRing);
            viewbox.Child = grid;
            windowsXamlHost1.Child = viewbox;

            // UWP コントロールの Loaded イベントのタイミングでは Parent が設定されている
            // ※ Window の Loaded イベントのタイミングでは未設定
            viewbox.Loaded += (sender, e) =>
            {
                // grid のサイズを WindowsXamlHost のサイズに調整
                (grid.Width, grid.Height) = (windowsXamlHost1.Width, windowsXamlHost1.Height);

                // Viewbox のサイズを実サイズに調整
                var parent = ((Windows.UI.Xaml.FrameworkElement)viewbox.Parent);
                (viewbox.Width, viewbox.Height) = (parent.Width, parent.Height);
            };
        }
   }
}

実行結果

f:id:rksoftware:20190114192346j:plain

これで、常に意図したサイズで表示されます。

DPI の変更があっても大丈夫なそうです。

  • DPI の異なるディスプレイ間をウィンドウが移動した場合
  • 実行中に表示スケールが変更された場合

など。
特に問題なく表示崩れなども起きませんでした。