.NET MAUIで非パッケージアプリを作る

5月後半に.NET MAUIがリリースされてそれなりに経過してると思うんですが、未だにドキュメントが英語ですら探しづらい、という事で残してみたり。
インストールについてはここを参照にするか、正式リリース版で作成したい場合はCLIから導入すると良いのではないかなと。

Visual Studio(Preview)から.NET MAUIアプリ、またはCLIからdotnet new mauiを実行するとビルド出来る環境が作成できるかと思います・・・が、お試しだからとプロジェクト名にMauiAppとか安易につけると名前が競合してビルドに失敗するので気を付けましょう。

まずはビルド

さて、ビルドを実行すると各プラットフォーム向けのバイナリが完成するわけなんですが、この時Windows向けにはWindowsアプリ(非デスクトップアプリ)としてビルドされます。
Visual Studioから実行する場合はメニューからWindows Machineで実行を開始すれば必要な追加手順を実行してくれるので特に問題はないんですが、CLIでビルド・実行したい場合や配布したい場合はこれだとちょっと面倒くさい(署名付きパッケージを作成しないといけない等)ので、バイナリを直接実行したい、というのが目的になります。
※あと個人的にはVisual Studioから実行した場合でもスタートメニューにアプリとして追加されてしまい、手動でアンインストールするまで居残るのが気に入らない、というのもあります

MAUIアプリはここで説明されている通り、WindowsではWinUI 3(Windows App SDK)を使用してビルドされるので、パッケージ化されていない WinUI 3 デスクトップ アプリの手順を参考にしてプロジェクトに「<WindowsPackageType>None</WindowsPackageType>」を追加してやります。

<Project Sdk="Microsoft.NET.Sdk">

(中略)

	<PropertyGroup>
		<WindowsPackageType>None</WindowsPackageType>
	</PropertyGroup>
</Project>

これでビルドしなおすと、exeを直接実行出来るようになります(実行PCにWindows App SDKランタイムのインストールは必要)。

WindowsPackageType = None を適用

が、操作してみるとすぐに気づきますが、タイトルバーに対する操作を受け付けてくれません(クリックしても反応はするものの実際に最小化・最大化・閉じる・移動は行われない)。
ウインドウのサイズ変更や、タスクバーからアプリを右クリックして「すべてのウインドウを閉じる」を選べば正常に終了するので、どうやらタイトルバーからのイベントが正しく処理出来なくなる様子。

前準備

Windowsアプリでビルドした場合は当然正しくタイトルバーが動作するので、デスクトップアプリとして動作する場合のみタイトルバーの動作を変更したい、というわけでプロジェクトをもう少し修正します。

<Project Sdk="Microsoft.NET.Sdk">

(中略)

	<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
		<WindowsPackageType>None</WindowsPackageType>
		<DefineConstants Condition="'$(WindowsPackageType)' == 'None'">WINAPP_NO_PACKAGE;$(DefineConstants)</DefineConstants>
	</PropertyGroup>
</Project>

WINAPP_NO_PACKAGEの部分はお好みの名前で。
PropertyGroupの方のCondition部分は別に指定しなくても影響はないはずですが個人的に指定。
これで今回の修正部分を#if ~ #endifで括ることでWindowsアプリにする時にもコードを再利用出来ます。

1.MauiWinUIWindowで対応

Microsoft.UI.Xaml.WindowクラスのWindows版実装である、Microsoft.Maui.MauiWinUIWindowExtendsContentIntoTitleBarプロパティがあるので、これを生成時に変更してやります。

まず、Createdイベントをサブスクライブする為に、Application.CreateWindow()をオーバーライドします。
この時デフォルトのままである場合、Applicationの実装は(アプリ名).AppクラスとしてApp.xaml及びApp.xaml.csが作成される為、プラットフォーム固有処理としてPlatform\Windows\にApp.xaml.csを作成・・・したいところなのですが、同フォルダには既に(アプリ名).WinUI.Appクラスとしてファイルが作成されている為、Platform\Windows以下に作成する時は別名(例えばApplication.xaml.cs)でファイルを作成する必要があります。

CreatedイベントではMicrosoft.Maui.Controls.Windowがsenderとして渡されるので、senderから((Window)sender).Handler.PlatformViewとしてMicrosoft.Maui.MauiWinUIWindowを取り出す事が出来ます。

WinUI的にはタイトルバーをユーザでカスタムする時には、ExtendsContentIntoTitleBarをtureに設定するのですが、MAUIではfalseに設定します。

namespace (アプリ名);

public partial class App : Application
{
#if WINAPP_NO_PACKAGE
    protected override Window CreateWindow(IActivationState activationState)
    {
        var result = base.CreateWindow(activationState);
        result.Created += (sender, e) =>
        {
            var window = sender as Window;
            var view = window.Handler.PlatformView as Microsoft.Maui.MauiWinUIWindow;
            view.ExtendsContentIntoTitleBar = false;
        };
        return result;
    }
#endif
}

これでビルドするとそっけない感じの表示ですが正しく動作するタイトルバーで実行されます。
因みにタイトルバーのテキストはwindow.Titleで設定したものが反映されます。

ExtendsContentIntoTitleBar = false を適用

ファイル名とクラス名が一致して気持ち悪い、という場合は(アプリ名).Appクラス名及びファイル名を変更した上で、MauiProgram.csで「.UseMauiApp<App>()」となっている部分のAppを変更したクラス名に変更するのが良いかと思います。

2.AppWindowで対応(Windows11のみ)

Windows 11のみで実行する場合、Microsoft.UI.Windowing.AppWindowクラスのTitleBarプロパティが提供される(Windows 10だとnullになっているっぽい)ので、これを利用すると少しだけ見栄えの良いタイトルバーが表示されます。
こちらはWinUI 3そのものなので、trueに設定します。

namespace (アプリ名);

public partial class App : Application
{
#if WINAPP_NO_PACKAGE
    protected override Window CreateWindow(IActivationState activationState)
    {
        var result = base.CreateWindow(activationState);
        result.Created += (sender, e) =>
        {
            var window = sender as Window;
            var view = window.Handler.PlatformView as Microsoft.Maui.MauiWinUIWindow;

            Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(view.WindowHandle);
            var ApplicationWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);
            var TitleBar = ApplicationWindow.TitleBar;
            TitleBar.ExtendsContentIntoTitleBar = true;
        };
        return result;
    }
#endif
}

タイトルバーの左側アイコンは表示されていませんが、クリックするとシステムメニューが表示されます。
また、この方法でタイトルバーを表示した場合はwindow.Titleの内容はタイトルに反映されません。

AppWindowクラス経由で設定した場合

3.併用してみる

次のように上記2つの方法を併用した場合(当然Windows 11のみで有効)。

namespace (アプリ名);

public partial class App : Application
{
#if WINAPP_NO_PACKAGE
    protected override Window CreateWindow(IActivationState activationState)
    {
        var result = base.CreateWindow(activationState);
        result.Created += (sender, e) =>
        {
            var window = sender as Window;
            var view = window.Handler.PlatformView as Microsoft.Maui.MauiWinUIWindow;
            view.ExtendsContentIntoTitleBar = false;

            Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(view.WindowHandle);
            var ApplicationWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);
            var TitleBar = ApplicationWindow.TitleBar;
            TitleBar.ExtendsContentIntoTitleBar = true;
        };
        return result;
    }
#endif
}

タイトルバーがアプリ内に取り込まれて、モバイルプラットフォーム向けの表示っぽくなります。
この場合でもウインドウの左上辺りをクリックするとシステムメニューが表示されます。

個人的にはこの表示が一番マルチプラットフォームUIっぽいかな、と思ったり。

試していない事

操作出来るようにしたタイトルバーのデザインは多分このへんを参考にすればきちんと表示出来る気がしないでもない・・・けどもgithubでissueも出てるので、そのうち正式対応が出るんじゃないかなー&とりあえず自分で使う分には問題なさそうなのでいったん放置。

Leave a Reply

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください