概要
WinUI3を使うとWindows標準UIの見た目でモダンな感じのアプリケーションを作ることができます。MVVMもかなり簡単に実装できます。細かいことをやろうとしなければかなりいい感じなのですが、ビルドやウインドウ関連の扱いが謎に難しく、開発前のお作法が多いように思います。しかもそれらお作法なしじゃまともに開発できません。
とりあえずわかる範囲でまとめていきます。筆者がWinUI3ビギナーなので、ある程度確立するまではちびちび更新していきます。。
exeを配布する
開発したアプリを配布する場合、WinUI3はインストーラを配布するパターンとexeやdllをそのまま配布するパターンがあります。業務システムの場合はexeで配布できるほうが便利かと思いますが、WinUI3はcsprojに1行足さないとそれができません。csprojの<PropertyGroup>内に以下のように追記します。
<PropertyGroup>
<!--省略-->
<WindowsPackageType>None</WindowsPackageType>
<!--省略-->
</PropertyGroup>
なお、デバッグするときにUnpackagedを選ぶ際もこの手順を行っておかないとエラーになります(なぜなのか...)。

参考にさせていただいた記事
ウインドウのサイズについて
ウインドウのサイズを直接変更できるプロパティが存在しません。また、ウインドウ内のコントロールは画面の倍率に追従するのにウインドウサイズは追従しません。これを追従させるために以下のような静的クラスを定義するとよいかと思います。
using System.Runtime.InteropServices;
using Microsoft.UI.Xaml;
public static partial class WindowExtensions
{
public static readonly double DefaultPixelsPerInch = 96D;
[DllImport("User32.dll")]
private static extern uint GetDpiForWindow(nint hwnd);
public static int DipToPhysical(double dip, uint dpi, double? pixelsParInch = null)
{
return (int)(dip * dpi / (pixelsParInch.HasValue ? pixelsParInch.Value : DefaultPixelsPerInch));
}
public static double PhysicalToDip(int px, uint dpi, double? pixelsParInch = null)
{
return (pixelsParInch.HasValue ? pixelsParInch.Value : DefaultPixelsPerInch) * px / dpi;
}
public static int GetWidth(this Window window)
{
var dpi = GetDpiForWindow((nint)window.AppWindow.Id.Value);
return (int)PhysicalToDip(window.AppWindow.Size.Width, dpi);
}
public static int GetHeight(this Window window)
{
var dpi = GetDpiForWindow((nint)window.AppWindow.Id.Value);
return (int)PhysicalToDip(window.AppWindow.Size.Height, dpi);
}
public static void SetWidth(this Window window, int width, double? pixelsParInch = null)
{
var dpi = GetDpiForWindow((nint)window.AppWindow.Id.Value);
window.AppWindow.ResizeClient(new Windows.Graphics.SizeInt32()
{
Height = window.AppWindow.Size.Height,
Width = DipToPhysical(width, dpi, pixelsParInch),
});
}
public static void SetHeight(this Window window, int height, double? pixelsParInch = null)
{
var dpi = GetDpiForWindow((nint)window.AppWindow.Id.Value);
window.AppWindow.ResizeClient(new Windows.Graphics.SizeInt32()
{
Height = DipToPhysical(height, dpi, pixelsParInch),
Width = window.AppWindow.Size.Width,
});
}
public static void SetSize(this Window window, int width, int height, double? pixelsParInch = null)
{
var dpi = GetDpiForWindow((nint)window.AppWindow.Id.Value);
window.AppWindow.ResizeClient(new Windows.Graphics.SizeInt32()
{
Height = DipToPhysical(height, dpi, pixelsParInch),
Width = DipToPhysical(width, dpi, pixelsParInch),
});
}
}
こちらはネット上に記事がたくさんあり、割とたどり着きやすかったですが、それでもかなり苦戦しました。
ちなみに.NET10以降はC#14の拡張プロパティも使用できるので、メソッドではなくプロパティとして拡張してあげるとより自然になるかと思います。
参考にさせていただいた記事
ウインドウの最大化・最小化
これもよく使うのになぜかそのまま呼び出せないです。以下のようにAppWindow.PresenterをPverlappedPresenterにキャストすれば呼び出せます。
((OverlappedPresenter)AppWindow.Presenter).Maximize();
((OverlappedPresenter)AppWindow.Presenter).Minimize();
参考にさせていただいた記事
公式は正義()
Titlebarについて
ウインドウのタイトルバーをカスタマイズできますが、デフォルトで存在する閉じるや最大化のボタンが非常に厄介です。タイトルバーのカスタマイズを行うためにはTitleBarというコントロールを使用しますが、これを使用すると閉じるボタンなどの高さが追従せず、タイトルバーの見た目が不自然になります。

かといってTitleBarを使用せずに全部独自でやろうとすると、閉じるボタンの上にコントロールが重なってしまったりして制御が非常に面倒です。
WinUI3のギャラリーアプリを見てみるとちゃんとタイトルバーと閉じるボタンたちの高さがちゃんと合っていたので、なにかやっているはずと思ってみてみたところ、コンストラクタに以下のような記述がありました。
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
これを書いてあげるだけで、ちゃんと高さが追従するようになりましたし、ボタンとコントロールが被ることも避けられました。

ちなみにXAMLはこんな感じ。ギャラリーアプリからそのまま引っ張ってきてイベントハンドラを消しただけですが。
<Grid x:Name="RootGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar
x:Name="titleBar"
Title="WinUI 3 Gallery"
IsPaneToggleButtonVisible="True">
<TitleBar.LeftHeader>
<ImageIcon
Height="16"
Margin="0,0,8,0"
Source="/Assets/Tiles/GalleryIcon.ico" />
</TitleBar.LeftHeader>
<TitleBar.Content>
<AutoSuggestBox
x:Name="controlsSearchBox"
Width="320"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
x:FieldModifier="public"
KeyboardAcceleratorPlacementMode="Hidden"
PlaceholderText="Search controls and samples..."
QueryIcon="Find">
</AutoSuggestBox>
</TitleBar.Content>
</TitleBar>
<Grid x:Name="AutomationHelpersPanel" Grid.Row="1">
<TextBlock Text="テストページです"/>
</Grid>
</Grid>
参考にさせていただいた記事
公式は正義(n回目)
WinUI3のクラスライブラリをnugetに公開するときの罠
WinUI3のクラスライブラリを作ってnugetに公開する際、いくつか罠があるようです。まだ1つしかぶち当たっていないので1項目しか書きませんが、もしほかにもあればどんどん書いていく予定です。
アイコンファイルを起因としたエラーが発生してしまう
csprojでパッケージのアイコン用にpngファイルを指定できます。以下のような感じです。
<PropertyGroup>
<!-- 省略 -->
<PackageIcon>アイコン.png</PackageIcon>
<!-- 省略 -->
</PropertyGroup>
<ItemGroup>
<None Include="アイコン.png">
<PackagePath>\</PackagePath>
<Pack>True</Pack>
</None>
</ItemGroup>
普通のクラスライブラリならこれで問題なくパッケージングしてnugetに公開して参照することができるようになります。ただ、WinUI3の場合はなぜかこれがうまくいきません。というのも、nugetへの公開と他プロジェクトからの参照まではうまくいくのですが、参照元のプロジェクトをビルドしようとすると、以下のようなエラーが発生します。
エラー (アクティブ) MSB3030 ファイル "~\.nuget\パッケージ名\バージョン\lib\net8.0-windows10.0.19041\パッケージ名\アイコン.png" は見つからなかったためコピーできません。
実際見に行くとアイコンファイルは見つかりません。なのに、MSBuildが勝手にそのアイコンファイルをコピーしようとしてしまうのです。おそらくWinUI3の場合はpngファイルがアセットとして扱われてしまうようです。これを回避する根本的な策としては、csprojに以下のような記述を追加します。
<PropertyGroup>
<!-- 省略 -->
<PackageIcon>アイコン.png</PackageIcon>
<!-- 省略 -->
</PropertyGroup>
<ItemGroup>
<None Include="アイコン.png">
<PackagePath>\</PackagePath>
<Pack>True</Pack>
</None>
</ItemGroup>
<!-- 追加 -->
<ItemGroup>
<Content Remove="アイコン.png" />
</ItemGroup>
<!-- ここまで -->
このように、Content Removeを追加することで、ビルドからpngファイルを明確に外すことができ、うまくいくようになります。当たり前といえば当たり前ですが、他のパッケージと同じノリでやろうとするとうまくいかない上に、ネット上に全然情報が落ちていないので、かなりハマってしまいました。
ちなみに、裏技(?)として、アイコンファイル名をPackageIcon.pngにしておくと、VisualStudioが自動でContent Removeを書いてくれます。
MAUIのほうにも似たようなIssueが立っていたので、もしかしたら同じ方法で避けることができるかもしれませんね。
DIコンテナについて
XAMLでデザインを行う以上、デフォルトコンストラクタに引数を受け取ることができないので、いろいろと工夫が必要になります。
ウインドウやページに関しては、それらをDIコンテナに入れておけばいいだけなので大丈夫ですが、コントロールはDIコンテナに入れてしまうとXAMLを使えなくなってしまいますので...。
これに関しては妥協点を模索中です。。ぜひこうやるといいよ的な方法があれば教えていただきたいです。
MVVMについて
CommunityToolkit.Mvvmを使った方法を書いていきたいですが、DIコンテナも絡めて書いておきたいので、DIの施策が決まったらこちらも書きます。
とりあえずは先人の非常にありがたい記事を貼っておきます。
https://zenn.dev/y_a_y/articles/winui_mvvm_b9685efa3cc1eb
ひとこと
主は普段から仕事とかでもWinFormsやBlazor、ASP.NET gRPC、MagicOnionなどいろいろ触ってますが、まあとにかくC#が流行っていないかなしみ。ほかにもReactやVue、django、express、electron、phpなどC#以外のフレームワークも割と触っているほうですが、やはりC#が一番素晴らしいかなという印象です。
たぶん世間的にはとにかく.NETFrameworkのWinFormsのイメージが強すぎて、かなり損しているかなと思います。私も古いC#や.NETFrameworkのことをそんなに好んではいませんが、普通に考えて今流行っている「イケてる」フレームワークよりはかなりいいですし、その上後方互換も強力で他のフレームワークに引けを取らない先進的な機能を有しています。
肌感としてはMAUIはまだ早すぎる感じがするので、今のところはWinUI3、Blazor、MagicOnion、Dapperがあればいいかなぁという感じです。WinUI3とWPFを比べるとWPFのほうが良いという記事が多いのでもしかしたらWPFに乗り換えるかもしれませんが、静的バインドがあまりにデカいのでWinUI3が流行ってくれると嬉しいなぁと思っています。
ぜひ皆さん良いC#ライフを...。