📦

WinUI3でPackaged/Unpackagedの両方に対応するための方策

に公開

はじめに

WinUI3のアプリケーションには、MSIXでアプリを配布するPackaged方式と、直接起動できる.exe形式で配布可能なUnpackaged方式の2つの方式があります。

アプリをどちらの方式にも対応させる場合、Packaged/Unpackaged形式を切り替えてビルドやテストを行う必要が出てきます。また、それぞれの形式によって処理を分岐させたいケースもあるでしょう。
本記事ではこれらを実現する方策を、WinUI-GalleryTemplateStudioのソースコードを参考にしながら紹介します。

なお、説明で使用しているプロジェクトは、Visual Studioが用意している下記のテンプレートプロジェクトから作成した直後のものです。
winui3-template-project
本記事で作成したプロジェクトを確認したい場合は、GitHubリポジトリを参照してください。

https://github.com/voltaney/Sample-WinUI3-PackageTypeCheck

(前知識) Unpackaged形式にするには

前提として、WinUI3の新規プロジェクトを作成すると既定でPackaged形式になっています。これをUnpackaged形式にするためには.csprojに以下のプロパティ設定が最低限必要になります。

.csproj
<WindowsPackageType>None</WindowsPackageType>

これを設定すると、launchSettings.jsonにUnpackaged形式向けの起動プロファイルも自動的に追記されます。

launchSettings.json
{
  "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を作成します。

add-configration

プロジェクトファイルにPropertyGroupを追加

作成した構成に基づいて、.csprojで下記ロジックを実現できるようにします。

  1. Packaged形式かどうかを定義するPackagedというプロパティ(任意名で可)をtrueで追加する。
  2. ConfigurationDebug-UnpackagedまたはRelease-Unpackagedの場合、Packagedfalseにする。
  3. Packagedtrueの場合
    • EnableMsixToolingtrueにする。
  4. Packagedfalseの場合
    • EnableMsixToolingfalseにする。
    • WindowsPackageTypeNoneにする。

実際の.csprojは下記のようになります。
なお、ConfigurationsPublish関連のプロパティは構成追加の時点で自動追記されているはずです。

.csproj
 <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-example
Unpackaged形式

packaged-example
Packaged形式

パッケージ形式による実装分岐(プリプロセッサ)

WinUIEx(WinUI3のWindowを拡張したライブラリ)のサンプルコードなんかではこの方法が取られています。
パッケージ形式に基づいて.csprojで特定の定数を定義し、#ifディレクティブで分岐させる方法です。

.csprojで定数を定義

ここでは例としてPackaged形式の時にPACKAGEDという定数を定義します。

.csproj
 <PropertyGroup Condition="'$(Packaged)' == 'true'">
  <EnableMsixTooling>true</EnableMsixTooling>
+  <DefineConstants>PACKAGED</DefineConstants>
 </PropertyGroup>
WinUIExのExampleコードの場合、どんな定義か

WindowsPackageTypeNoneの時にUNPACKAGED定数を定義しています。

.csproj
<DefineConstants Condition="'$(WindowsPackageType)'=='None'">UNPACKAGED;$(DefineConstants)</DefineConstants>

https://github.com/dotMorten/WinUIEx/blob/v2.5.1/src/WinUIExSample/WinUIExSample.csproj

#ifで分岐

下記の具合で分岐させることができます。Visual Studioでは実行されない#ifブロックはグレイの文字列で表示されますが、構成を切り替えると自動的にエディタの表示も切り替わります。

MainWindow.xaml.cs
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
    }
}

パッケージ形式による実装分岐(ランタイム)

WinUI-GalleryTemplateStudioどちらもこの方式を採用しており、ヘルパクラスとして提供されています。本項ではこれらの実装は行わず、それぞれのプロジェクトの実装を簡単に紹介するにとどめます。

動作原理

Windows APIには呼び出し元のパッケージ情報を取得する関数(後述)が定義されています。この関数は、非パッケージのプロセスから呼び出された場合、APPMODEL_ERROR_NO_PACKAGE15700)というリターンコードを返す仕様になっています[1]
このリターンコードを確認することで、ランタイムでもパッケージ形式を判断できるようになります。

api-ms-win-appmodel-runtime-l1-1-1.dllを介してGetCurrentPackageId関数を呼び出し、返り値が15700か判断しています。

https://github.com/microsoft/WinUI-Gallery/blob/v2.5.1/WinUIGallery/Helper/NativeHelper.cs#L20-L46

TemplateStudio

kernel32.dllを介してGetCurrentPackageFullName関数を呼び出し、返り値が15700か判断しています。

https://github.com/microsoft/TemplateStudio/blob/v5.5/code/TemplateStudioForWinUICs/Templates/Proj/Default/Param_ProjectName/Helpers/RuntimeHelper.cs#L6-L20

脚注
  1. https://learn.microsoft.com/ja-jp/windows/win32/api/appmodel/nf-appmodel-getcurrentpackagefullname#return-value ↩︎

GitHubで編集を提案

Discussion