WinUI3でPackaged/Unpackagedの両方に対応するための方策
はじめに
WinUI3のアプリケーションには、MSIXでアプリを配布するPackaged方式と、直接起動できる.exe形式で配布可能なUnpackaged方式の2つの方式があります。
アプリをどちらの方式にも対応させる場合、Packaged/Unpackaged形式を切り替えてビルドやテストを行う必要が出てきます。また、それぞれの形式によって処理を分岐させたいケースもあるでしょう。
本記事ではこれらを実現する方策を、WinUI-GalleryやTemplateStudioのソースコードを参考にしながら紹介します。
なお、説明で使用しているプロジェクトは、Visual Studioが用意している下記のテンプレートプロジェクトから作成した直後のものです。

本記事で作成したプロジェクトを確認したい場合は、GitHubリポジトリを参照してください。
(前知識) Unpackaged形式にするには
前提として、WinUI3の新規プロジェクトを作成すると既定でPackaged形式になっています。これをUnpackaged形式にするためには.csprojに以下のプロパティ設定が最低限必要になります。
<WindowsPackageType>None</WindowsPackageType>
これを設定すると、launchSettings.jsonにUnpackaged形式向けの起動プロファイルも自動的に追記されます。
{
"profiles": {
"PackageTypeCheck (Package)": {
"commandName": "MsixPackage"
},
"PackageTypeCheck (Unpackaged)": {
"commandName": "Project"
}
}
}
このプロファイルを切り替えるだけで両形式とも起動できると楽なのですが、実際には先ほどのWindowsPackageTypeが効いてくるので、Packaged形式で実行しようとしてもエラーが出ます。
パッケージ形式のビルド切り替え
実際に両形式でパブリッシュされている良例が、MSがリリースしているWinUI-Gallery(WinUI3コンポーネントのギャラリー)です。
.csprojを見てみると、構成(DebugやRelease等)にUnpackaged版も追加することでPropertyGroupを切り替えていることが分かります。
オリジナルの.csprojでは他にも色々とコンディショナルなPropertyGroupが定義されていますが、ここではWindowsPackageTypeの付け替え一点に絞って紹介します。
ソリューション構成の追加
Visual Studioのメニューバーで構成マネージャーを選択します。

アクティブソリューション構成のドロップダウンから新規作成を選択し、設定のコピー元をDebugとした状態でDebug-Unpackagedという構成を作成します。
Releaseについても同様にRelease-Unpackagedを作成します。

プロジェクトファイルにPropertyGroupを追加
作成した構成に基づいて、.csprojで下記ロジックを実現できるようにします。
- Packaged形式かどうかを定義する
Packagedというプロパティ(任意名で可)をtrueで追加する。 -
ConfigurationがDebug-UnpackagedまたはRelease-Unpackagedの場合、Packagedをfalseにする。 -
Packagedがtrueの場合-
EnableMsixToolingをtrueにする。
-
-
Packagedがfalseの場合-
EnableMsixToolingをfalseにする。 -
WindowsPackageTypeをNoneにする。
-
実際の.csprojは下記のようになります。
なお、ConfigurationsやPublish関連のプロパティは構成追加の時点で自動追記されているはずです。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
- <EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
+ <Configurations>Debug;Release;Debug-Unpackaged;Release-Unpackaged</Configurations>
+ <Packaged>true</Packaged>
+ <Packaged Condition="'$(Configuration)' == 'Debug-Unpackaged' Or '$(Configuration)' == 'Release-Unpackaged'">false</Packaged>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Packaged)' != 'true'">
+ <EnableMsixTooling>false</EnableMsixTooling>
+ <WindowsPackageType>None</WindowsPackageType>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Packaged)' == 'true'">
+ <EnableMsixTooling>true</EnableMsixTooling>
</PropertyGroup>
<ItemGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
+ <PublishReadyToRun Condition="'$(Configuration)'=='Debug-Unpackaged'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
+ <PublishTrimmed Condition="'$(Configuration)'=='Debug-Unpackaged'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
</PropertyGroup>
</Project>
構成を切り替えて実行
以上の設定後、起動プロファイルに合わせて構成マネージャーを選択することで、Packaged/Unpackagedを切り替えて実行できるようになります。

Unpackaged形式

Packaged形式
パッケージ形式に基づく実装分岐1【プリプロセッサ】
WinUIEx(WinUI3のWindowを拡張したライブラリ)のサンプルコードなんかではこの方法が取られています。
パッケージ形式に基づいて.csprojで特定の定数を定義し、#ifディレクティブで分岐させる方法です。
.csprojで定数を定義
ここでは例としてPackaged形式の時にPACKAGEDという定数を定義します。
<PropertyGroup Condition="'$(Packaged)' == 'true'">
<EnableMsixTooling>true</EnableMsixTooling>
+ <DefineConstants>PACKAGED</DefineConstants>
</PropertyGroup>
WinUIExのExampleコードの場合、どんな定義か
WindowsPackageTypeがNoneの時にUNPACKAGED定数を定義しています。
<DefineConstants Condition="'$(WindowsPackageType)'=='None'">UNPACKAGED;$(DefineConstants)</DefineConstants>
#ifで分岐
下記の具合で分岐させることができます。Visual Studioでは実行されない#ifブロックはグレイの文字列で表示されますが、構成を切り替えると自動的にエディタの表示も切り替わります。
using Microsoft.UI.Xaml;
namespace PackageTypeCheck;
public sealed partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
#if PACKAGED
// Packaged app
MyText.Text = "Packaged App";
#else
// Unpackaged app
MyText.Text = "Unpackaged App";
#endif
}
}
パッケージ形式に基づく実装分岐2【ランタイム】
WinUI-GalleryとTemplateStudioどちらもこの方式を採用しており、ヘルパクラスとして提供されています。本項ではこれらの実装は行わず、それぞれのプロジェクトの実装を簡単に紹介するにとどめます。
動作原理
Windows APIには呼び出し元のパッケージ情報を取得する関数(後述)が定義されています。この関数は、非パッケージのプロセスから呼び出された場合、APPMODEL_ERROR_NO_PACKAGE(15700)というリターンコードを返す仕様になっています[1]。
このリターンコードを確認することで、ランタイムでもパッケージ形式を判断できるようになります。
WinUI-Gallery
api-ms-win-appmodel-runtime-l1-1-1.dllを介してGetCurrentPackageId関数を呼び出し、返り値が15700か判断しています。
TemplateStudio
kernel32.dllを介してGetCurrentPackageFullName関数を呼び出し、返り値が15700か判断しています。
Discussion