🪟

XCopyでデプロイ可能なWinUI3のミニマムプロジェクトを作る

2023/03/18に公開

最近業務で利用する社内ツールをWinUI3で実装してみたりしました。なかなか勝手がわからないところもあり苦戦しましたが、新しいフレームワークを触るのは楽しいですね。

さてWinUI3のプロジェクトを新規作成手順ですが、通常はVisual Studioにテンプレートをインストールして、それを選択し作成しましょうと案内されます。いろいろいい塩梅に調整されたプロジェクトが作成されるのですが、いかんせん情報量が多すぎて何が何やらとなります。
社内ツールを作る程度なら、発行したバイナリをフォルダごとコピペする配布形式で十分です。そこでどこまで構成すればコピペでデプロイ可能な最小のWinUI3プロジェクトになるのか確認してみました。

まずはHelloWorld

最小のC#プロジェクトといえばHelloWorldするだけのコンソールアプリでしょう。というわけで以下のようなMinWinUI.csprojProgram.csを作成します。

MinWinUI.cpsroj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
Program.cs
namespace MinWinUI;
static class Program
{
    public static void Main() => Console.WriteLine("Hello world!");
}

当然ながらこのまま実行すればHelloWorldが表示されます。ここからいろいろパーツを足してWinUI3プロジェクトに仕立てていきましょう。

WinUI利用をプロジェクトファイルに明示

WPF、WinFormsでは各GUIフレームワークの利用を明示するUseWPFUseWindowsForms要素をtrueに指定しますが、WinUI3でも同様にUseWinUI要素を指定します。
あとGUIアプリなので、OutputTypeもWPFなどと同様WinExeに変更しておきます。

MinWinUI.cpsroj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    (中略)
    <UseWinUI>true</UseWinUI>
  </PropertyGroup>
</Project>

この時点でコンソールがアタッチされなくなるため、ビルドはできてもHelloWorldは表示されません。

Windows App SDKの参照をプロジェクトファイルに明示

前述の作業ではWinUI使いますと宣言しただけで、必要なアセンブリは一切指定していません。次はWinUI3に必要な各ファイルをNugetから参照します。やることととしては以下のようにMicrosoft.WindowsAppSDKを参照に指定するだけです。

MinWinUI.cpsroj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    (中略)
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.230313.1" />
  </ItemGroup>
</Project>

Visual StudioのテンプレートからWinUI3プロジェクトを新規作成すると、Microsoft.Windows.SDK.BuildToolsというパッケージも参照していますが、これはMicrosoft.WindowsAppSDKも参照しているため、プロジェクトファイルに明示しなくても自動的に依存関係から参照されます。

TFMとPlatformの指定

このままビルドをするとTargetPlatformが~といった旨のエラーが表示されるので、それを指定しましょう。TargetFramework要素をnet7.0-windows10.0.19041.0に変更し、Platforms要素も追加します。対象はx86,x64,arm64から適当なのをチョイスしましょう。
これでVisual Studioをでデバッグを実行しようとすると、最初に作成したコンソールプロジェクトではPlatform=AnyCPUのデバッグ構成しか作成されていないため、構成マネージャーが~と言われてエラーがでると思います。指示通り構成マネージャーから追加したPlatformsに対応した構成を作成追加してください。

MinWinUI.cpsroj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    (中略)
    <TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
    <Platforms>x64</Platforms>
    (中略)
  </PropertyGroup>
</Project>

Appクラスを作成

ここまでプロジェクトの設定だけでしたが、ようやくコードを書きます。
WPFやってる人にはおなじみのApplicationクラスを継承したAppクラスを定義します。今回は最小を目指すということでXAML側の定義を書きません。なのでそのままApplicatonクラスを継承したC#のコードだけを書きます。OnLaunchedメソッドのオーバーライドでWindowクラスのインスタンスを作成し、Activateメソッドで表示させるように指定します。WPFなどではウインドウを表示させるときはその名の通りShowメソッドを呼び出していましたが、WinUI3ではActivateメソッドのようです。

App.cs
using Microsoft.UI.Xaml;
namespace MinWinUI;
public class App : Application
{
    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        base.OnLaunched(args);
        new Window { Title = "Hello world!" }.Activate();
    }
}

定義した上記のAppクラスをMainメソッドから呼び出します。
MainメソッドにSTA属性を付与し、Application.Startメソッドからメインスレッドのコンテキストを確保しておいてから、Appクラスをnewするだけです。
今回はAppクラスのXAML定義を省略していますが、実はXAML定義を作成するとWinUI3プロジェクトでは自動的にMainメソッドが生成されます(WPFとWinFormsではユーザーが自分で作ったエントリーポイントを明示することができましたが、WinUIではできないようです)。このコードはその自動的に生成されたエントリーポイントをそのまま写経しただけです。
自動生成されたエントリーポイントだと必要なアセンブリのチェックなども行ってくれるので、実際のコードではAppクラスのXAML定義をちゃんとやりましょう。

Program.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Dispatching;
using WinRT;
namespace MinWinUI;
static class Program
{
    [STAThread]
    public static void Main()
    {
        ComWrappersSupport.InitializeComWrappers();
        Application.Start(p =>
        {
            var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
            SynchronizationContext.SetSynchronizationContext(context);
            new App();
        });
    }
}

最後の調整

さてこれでデバッグを実行するとようやくWinUIのウインドウが表示され……ません。TypeInitializationExceptionが発生します。必要なDLLが不足しているようなので、WindowsAppSDKSelfContainedを有効にして、DLLも吐き出してもらうようにします。

MinWinUI.cpsroj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
  </PropertyGroup>
</Project>

これでようやくデバッグ実行できたと思います。HelloWorldなウインドウが表示されました(私のPCがダークテーマなのでBackgroundが真っ黒なウインドウ)。

あとはコピペで配布できるように発行できるかどうか確認しておきます。以下のコマンドで必要なファイル全部入りの発行を行えます。

dotnet publish -c Release -r win-x64 --sc -p:Platform=x64

で、実行すると必要なファイルを吐き出すためのビルドタスクがないといった旨のエラーがでると思います。というわけでEnableMsixToolingを指定します。

MinWinUI.cpsroj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <EnableMsixTooling>true</EnableMsixTooling>
  </PropertyGroup>
</Project>

これでようやく発行も通りました。出力されたpublishフォルダをサンドボックス環境で実行したところ無事ウインドウが表示されました。

最終的にビルドを通したプロジェクトファイルを以下に書いておきます。コメントアウトしていますが、MSのドキュメントによるとWindowsPackageTypeの指定も必要なはず……なのですがWindowクラスを表示するだけなら未指定でも動いてしまいました。ちゃんと確認していないのですが、実際には必要なはず……です。

MinWinUI.cpsroj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
    <Platforms>x64</Platforms>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <UseWinUI>true</UseWinUI>
    <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
    <EnableMsixTooling>true</EnableMsixTooling>
    <!--<WindowsPackageType>None</WindowsPackageType>-->
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.230313.1" />
  </ItemGroup>
</Project>
App.cs
using Microsoft.UI.Xaml;
namespace MinWinUI;
public class App : Application
{
    protected override void OnLaunched(LaunchActivatedEventArgs args)
    {
        base.OnLaunched(args);
        new Window { Title = "Hello world!" }.Activate();
    }
}
Program.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Dispatching;
using WinRT;
namespace MinWinUI;
static class Program
{
    [STAThread]
    public static void Main()
    {
        ComWrappersSupport.InitializeComWrappers();
        Application.Start(p =>
        {
            var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
            SynchronizationContext.SetSynchronizationContext(context);
            new App();
        });
    }
}

Discussion