WinUI3のウインドウのタイトルバーを自由にカスタムしたい

もくじ
https://tera1707.com/entry/2022/02/06/144447#WinUI3

やりたいこと

以前書いた「WinUI3のウインドウの最大化ボタン無効化や、ウインドウサイズ変更をしたい」で、ウインドウについている最大化ボタン、最小化ボタンの無効化をどうやるか調べたが、今回はタイトルバーを好きに改造したい。
イメージは、「wpfで、×ボタンをなくしたウインドウを作る」の記事で調べた、WPFでタイトルバーを好きに改造するやり方と同じことをやりたい。

前提

  • VisualStudio2022 Community 17.1.4
  • WinUI3.0
  • Windows App SDK 1.0
  • 2022年4月の時点で調査実施

今回できたこと

まず結論として、今回調べた限りでは、
WPFの時にやったWindowChromeのような自由さでタイトルバーをカスタムする方法は見つけられなかった。
(標準のキャプションボタン(最小化、最大化、閉じるボタンのこと)を全部見えなくして、自作の閉じるボタンを付ける、などができなかった)

今回できたのは、

  • 好きなアイコンを表示
  • タイトルバーの太さや色を自由に変える
  • 標準のキャプションボタン(最小化、最大化、閉じるボタン)をそのまま使う
    (無効にはできなかった)

というところまで。
できたこと、できなかったことをまとめたのが下図。

※WinUI3は、2022年4月の時点でまだどんどん変わっていっている、枯れてないフレームワークのようなので、今後のバージョンUP(WinUI3.1以降)では動きがガラッと変わる可能性もあると思われる。うまくいかなかった部分については今後に期待。

以下、調べたことのメモ。

やったこと

分岐点:win11だけに絞るか、win10でも動かしたいか

タイトルバーを自由にいじる方法は、

  • win11からのみ使える簡単なやり方
  • win10でも使えるやり方

がある様子。

https://docs.microsoft.com/ja-jp/windows/apps/develop/title-bar?tabs=winui3

今やりたいのは、タイトルバーのカスタムをして、Win10でも11でも動かすことだったので、win10でも使えるやり方を使う。
(win11のみのやり方のほうが、簡単かついろいろできるっぽい。未調査。)

msドキュメントでは、win11のみで使えるやり方にWindows App SDK、win10でも使えるやり方に「WinUI3」と表記されて説明されていた。(UWP/WinUI2というくくりもあったが今回はタッチせず。)

分岐点:シンプルにやるか、フルカスタムするか

さらに、win11のみの方法、win10でも使える方法のどちらにも、シンプルにカスタムする方法と、フルにカスタムする方法があるらしい。

win10でも使える方法の「シンプルにカスタムする方法」のほうを見ると、このやり方ではタイトルバーの色は変えられないとある。

またざっと試したところ、できるのはタイトルの文言を好きな文字列にすることくらいっぽいので、今回はシンプルな方法は使わず、フルにカスタムする方法を調べる。

win10でも使える、フルカスタムする方法

メインウインドウ(Microsoft.UI.Xaml.Windowクラス)のExtendsContentIntoTitleBarプロパティをtrueにして、
SetTitleBar()の引数に、タイトルバーにしたいxamlに書いたコントロールを渡してやれば、その部分がタイトルバーになる。

下記で、「titlebar」という名前にしたGridを、タイトルバーにしてみた。

<Window
    x:Class="WinUI3WindowTitleBarJikken.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3WindowTitleBarJikken"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="100"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0" Background="Blue" Name="titlebar">
            <TextBlock Text="タイトルバーに出したい文言" />
        </Grid>

        <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
        </StackPanel>
    </Grid>
</Window>
using Microsoft.UI.Xaml;

namespace WinUI3WindowTitleBarJikken
{
    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();

            ExtendsContentIntoTitleBar = true;
            SetTitleBar(titlebar);
        }
        private void myButton_Click(object sender, RoutedEventArgs e)
        {
            myButton.Content = "Clicked";
        }
    }
}

これを実行すると、下図のようなウインドウが出てくる。

青のGridがタイトルバー扱いになっていて、例えばそこをドラッグすればウインドウの移動ができし、ダブルクリックすればウインドウが最大化する。

上のメインウインドウでは、タイトルバーの色をxaml内で直接指定しているが、MSのドキュメントにもあるように、リソースディクショナリに下記のように書くことでも背景色指定ができる。

<Application
    x:Class="WinUI3WindowTitleBarJikken.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinUI3WindowTitleBarJikken">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
                <!-- Other merged dictionaries here -->
            </ResourceDictionary.MergedDictionaries>
            <!-- Other app resources here -->
            <SolidColorBrush x:Key="WindowCaptionBackground">Green</SolidColorBrush>
            <SolidColorBrush x:Key="WindowCaptionBackgroundDisabled">LightGreen</SolidColorBrush>
            <SolidColorBrush x:Key="WindowCaptionForeground">Red</SolidColorBrush>
            <SolidColorBrush x:Key="WindowCaptionForegroundDisabled">Pink</SolidColorBrush>
        </ResourceDictionary>
    </Application.Resources>
</Application>

これで、MSのドキュメントに書いてある、タイトルバーのフルカスタムのさわりの部分はできた。

ここまではうまくできた

ここまで、MSのドキュメントどおりにやってみて、
タイトルバーのキャプションボタンの部分の見た目は自由にできたのだが、今回はここからキャプションボタンの一部(今回は最大化ボタン)を無効化したい。

が、ここからがどうにもうまくいかず、試行錯誤というか、あれこれ試した部分。 結果としては、100%思っていたような動き(タイトルバーの見た目(色)を変えたうえで、最大化ボタンを無効にする)をさせることはできなかったので妥協点を見つけたのだが、そこに行くまでの経過をメモしておく。

うまくいかず、いろいろ試した部分

フルカスタムしたタイトルバーの最大化ボタンを無効化したい

以前、WinUI3で「最大化ボタンを無効化する」やり方を調べた。
https://tera1707.com/entry/2022/04/24/220519

その時、ウインドウハンドルからAppWindowクラスを取得して、そのAppWindowクラスに、最大化ボタンを無効にするよう設定したAppWindowPresenterクラスをセットする、というやり方をしたので、今回それと組み合わせて、

  • ExtendsContentIntoTitleBarプロパティをtrueにして、
  • 最大化ボタンを無効にするよう設定したAppWindowPresenterクラスをセットする

ということを試したが、ExtendsContentIntoTitleBarプロパティをtrueにしていると、AppWindowPresenterでIsMaximizableをfalseにして無効にしたはずの最大化ボタンが有効になってしまう。

using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using System;

namespace WinUI3WindowTitleBarJikken
{
    public sealed partial class MainWindow : Window
    {
        private AppWindow m_AppWindow;

        public MainWindow()
        {
            this.InitializeComponent();

            ExtendsContentIntoTitleBar = true;
            SetTitleBar(titlebar);

            IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
            Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd);
            m_AppWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId);

            // OverlappedPresenterを作成、カスタムしてウインドウにset
            var op = OverlappedPresenter.Create();
            op.IsMaximizable = false;
            op.IsMinimizable = true;
            op.IsResizable = false;
            op.IsAlwaysOnTop = false;
            m_AppWindow.SetPresenter(op); // →IsMaximizable = falseをセットしても、最大化ボタンは表示されてしまう
        }
    }
}

どうも、ExtendsContentIntoTitleBarがtrueのときに表示されているキャプションボタン(最大化、最小化、閉じるボタン)は「フォールバックタイトルバー」というものの一部らしく、見た目は同じでも、実は標準とは異なるものらしい。

試しに、下記のようにExtendsContentIntoTitleBar = trueだけを行いSetTitleBar()は行わないようにしてみると、

ExtendsContentIntoTitleBar = true;
//SetTitleBar(titlebar);

こういう画面が表示された。

右上に、キャプションボタン一式と、ちょっとつまめる領域がある。
どうもこれが、フォールバックタイトルバーというものらしい。
(つまんだり、ダブルクリックしたら最大化するなどのいわゆるタイトルバーの動きは、この部分のちょっとつまめる領域で行える)

イメージとしては、ExtendsContentIntoTitleBar =trueをすると、このフォールバックタイトルバーが出て、SetTitleBar()をすると、そこで指定した部分の領域に「ちょっとつまめる領域」が広がる、という感じ。

話を戻して、

そのフォールバックタイトルバーには、AppWindowPresenterのIsMaximizableIsMinimizableは効果がなく、これによる最大化ボタン、最小化ボタンの無効化はできなかった。(おそらく、IsMaximizableは裏で「標準の」キャプションボタンには効いてるが、ExtendsContentIntoTitleBar =trueのときに目に見えているフォールバックタイトルバーの中のキャプションボタンには聞いてない)

※ただ、IsResizable(サイズ変更の可/不可)やIsAlwaysOnTop(常時最前面表示するかどうか)のプロパティは聞いているようで、設定が反映される。
(ただし、IsResizableをfalseにすると、ウインドウの上下左右の枠付近にマウスを持って行っても矢印型にならないはずが、上部の枠にもっていくと、マウスが矢印型になってしまう。(でも実際にサイズ変更はできない) → WinUI3のバグか?)

まとめると、

  • ExtendsContentIntoTitleBar =true にしていると、
  • IsMaximizableをfalseにしたAppWindowPresenterクラスをセットしても、
  • 最大化ボタンを無効にできなかった

その時どうしたか

上記、最大化ボタンを無効にできないのをどうしても解決できなかった。
また、見方によってはWinUI3のバグとも、WinUI3が開発途中でまだ動きが不安定なだけ、とも取れる動きだったため、 今は‘ExtendsContentIntoTitleBar =true‘ と AppWindowPresenterクラス は一緒に使わない方がよいと感じた。

なので、

  1. タイトルバーの見た目(バーの色など)を変えるために‘ExtendsContentIntoTitleBar =true‘ にするなら、AppWindowPresenterクラス は使わない
  2. AppWindowPresenterクラス を使って最大化ボタンなどを無効にするなら、タイトルバーは標準のままを使う

が、今はよいのでは、と思う。
今回は、②を採用。

フルカスタムしたタイトルバーの最大化ボタンを無効化したいのがあきらめきれない

でもやっぱりタイトルバーの見た目を変えつつ最大化ボタンを無効にしたい。

MSの公式情報で、WinUI3のウインドウハンドルを取る方法があり、ウインドウハンドルを使ってWin32APIを使っていろいろやるのもアリ、という記載もあるようなので、その方向で何かできないかを調査。

やってみた結果、(コードは保存してなかったのだが)やろうとしていた最大化ボタンの無効化(というか、完全にキャプションボタンを表示されないようにして、自前xamlでキャプションボタンを作成)はできたのだが、それと引き換えに、ウインドウの標準の動作が消えてしまった。(win11のウインドウの周りの影とか、角が丸いとか)
(確か、SetWindoLong()でスタイルをPOPUPウインドウにした、とかだったと思う)

そのほか、Win32APIでいろいろやってみたがうまくいかなかったので、いったんこの方法はあきらめた。
(なにかよい方法ないか、継続的に調べたい...)

参考

msdocs タイトルバーのカスタマイズの仕方

https://docs.microsoft.com/en-us/windows/apps/develop/title-bar?tabs=wasdk