WinUI 3 でタイトルバーを整えるコツ: NavigationView との統合パターン
WinUI 3 でタイトルバーを整えるコツ: NavigationView との統合パターン
WinUI 3 でモダンな Windows アプリケーションを開発する際、カスタムタイトルバーと NavigationView の組み合わせは非常に一般的なパターンです。しかし、この組み合わせにはいくつかの落とし穴があり、適切に実装しないとレイアウトが崩れてしまいます。
この記事では、実際の開発プロジェクトで遭遇した問題と、その解決策を通じて、WinUI 3 のタイトルバーを整えるコツを紹介します。
目次
- WinUI 3 のタイトルバーの基本
- NavigationView との統合における問題
- 解決策: NavigationViewContentMargin のオーバーライド
- 完全な実装例
- デバッグのコツ
- まとめ
WinUI 3 のタイトルバーの基本
TitleBar コントロールの登場
WinUI 3 (Windows App SDK 1.4 以降) では、新しい TitleBar コントロールが導入されました。これにより、カスタムタイトルバーの実装が大幅に簡素化されました。
従来の方法 (Window.ExtendsContentIntoTitleBar と Window.SetTitleBar()) と比較して、以下のメリットがあります:
- 宣言的な記述: XAML で直接タイトルバーを定義可能
- NavigationView との統合: バックボタンやペイントグルボタンを簡単に連携
- アクセシビリティの向上: 標準的なタイトルバーの動作を自動的に継承
基本的な実装
<Page>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar
x:Name="AppTitleBar"
Title="My App"
Grid.Row="0" />
<!-- コンテンツエリア -->
<Grid Grid.Row="1">
<!-- アプリのメインコンテンツ -->
</Grid>
</Grid>
</Page>
ウィンドウの設定
MainWindow.xaml.cs で、コンテンツをタイトルバー領域に拡張する設定を行います:
public MainWindow()
{
InitializeComponent();
ExtendsContentIntoTitleBar = true;
}
これにより、カスタムタイトルバーがウィンドウの最上部に配置されます。
NavigationView との統合における問題
典型的なレイアウト構造
多くの WinUI 3 アプリケーションでは、以下のような構造を採用しています:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- TitleBar -->
<RowDefinition Height="*" /> <!-- NavigationView -->
</Grid.RowDefinitions>
<TitleBar Grid.Row="0" ... />
<NavigationView Grid.Row="1" ...>
<!-- ナビゲーションメニューとコンテンツ -->
</NavigationView>
</Grid>
問題: 垂直方向のレイアウトずれ
この構造では、以下のような問題が発生します:
![レイアウトずれの図]
- 左ペイン (ナビゲーションメニュー): タイトルバーの直下から正しく開始される
- 右ペイン (コンテンツエリア): タイトルバーの下に余白が入り、下にずれて表示される
原因
この問題は、NavigationView の内部実装に起因しています:
- NavigationView は、左ペイン (Pane) と右ペイン (Content) を異なるレイアウトロジックで管理
- コンテンツエリアには、デフォルトで
NavigationViewContentMarginというリソースが適用される - このマージンは、通常のアプリケーションでは適切だが、
ExtendsContentIntoTitleBarを使用している場合には不要な余白を生成してしまう
この問題は WinUI 3 の既知の問題として、GitHub で報告されています:
解決策: NavigationViewContentMargin のオーバーライド
シンプルで効果的な方法
NavigationView のリソースディクショナリで、デフォルトのマージンをオーバーライドします:
<NavigationView x:Name="NavigationViewControl" Grid.Row="1" ...>
<NavigationView.Resources>
<Thickness x:Key="NavigationViewContentMargin">0,0,0,0</Thickness>
<Thickness x:Key="NavigationViewContentGridBorderThickness">0</Thickness>
<Thickness x:Key="NavigationViewMinimalContentGridBorderThickness">0</Thickness>
</NavigationView.Resources>
<NavigationView.MenuItems>
<!-- メニュー項目 -->
</NavigationView.MenuItems>
<Frame x:Name="ContentFrame" />
</NavigationView>
なぜこれで解決するのか
- NavigationViewContentMargin: コンテンツエリアに適用される上部マージンを 0 に設定
- NavigationViewContentGridBorderThickness: コンテンツグリッドの境界線の太さを 0 に設定
- NavigationViewMinimalContentGridBorderThickness: 最小表示モードでの境界線の太さを 0 に設定
これらのリソースをオーバーライドすることで、NavigationView の内部レイアウトが調整され、左ペインと右ペインが同じ垂直位置から開始されるようになります。
ページレベルでのパディング調整
コンテンツエリアがタイトルバーの直下から始まるようになったため、各ページで適切な上部パディングを設定します:
<Page>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 上部ボタンバー -->
<StackPanel Grid.Row="0" Padding="24,24,24,8">
<!-- コントロール -->
</StackPanel>
<!-- メインコンテンツ -->
<GridView Grid.Row="1" Padding="16,0,16,16">
<!-- アイテム -->
</GridView>
</Grid>
</Page>
通常、24px の上部パディングが適切です。これにより、タイトルバーとコンテンツの間に適度な余白が確保されます。
完全な実装例
ShellPage.xaml
<Page
x:Class="MyApp.Views.ShellPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- カスタムタイトルバー -->
<TitleBar
x:Name="AppTitleBar"
Grid.Row="0"
Title="My Application"
BackRequested="TitleBar_BackRequested"
IsBackButtonVisible="{x:Bind ContentFrame.CanGoBack, Mode=OneWay}"
IsPaneToggleButtonVisible="True"
PaneToggleRequested="TitleBar_PaneToggleRequested">
<!-- アイコンの追加(オプション) -->
<TitleBar.LeftHeader>
<ImageIcon
Height="16"
Margin="0,0,8,0"
Source="ms-appx:///Assets/AppIcon.png" />
</TitleBar.LeftHeader>
</TitleBar>
<!-- NavigationView -->
<NavigationView
x:Name="NavigationViewControl"
Grid.Row="1"
PaneDisplayMode="Auto"
IsPaneOpen="True"
IsPaneToggleButtonVisible="False"
IsBackButtonVisible="Collapsed"
IsSettingsVisible="True"
OpenPaneLength="200"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
ItemInvoked="NavigationView_ItemInvoked">
<!-- 重要:デフォルトマージンのオーバーライド -->
<NavigationView.Resources>
<Thickness x:Key="NavigationViewContentMargin">0,0,0,0</Thickness>
<Thickness x:Key="NavigationViewContentGridBorderThickness">0</Thickness>
<Thickness x:Key="NavigationViewMinimalContentGridBorderThickness">0</Thickness>
</NavigationView.Resources>
<!-- メニュー項目 -->
<NavigationView.MenuItems>
<NavigationViewItem
Content="ホーム"
Tag="main">
<NavigationViewItem.Icon>
<SymbolIcon Symbol="Home" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem
Content="ドキュメント"
Tag="documents">
<NavigationViewItem.Icon>
<SymbolIcon Symbol="Document" />
</NavigationViewItem.Icon>
</NavigationViewItem>
</NavigationView.MenuItems>
<!-- コンテンツフレーム -->
<Frame x:Name="ContentFrame" />
</NavigationView>
</Grid>
</Page>
ShellPage.xaml.cs
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace MyApp.Views;
public sealed partial class ShellPage : Page
{
public ShellPage()
{
InitializeComponent();
}
private void TitleBar_BackRequested(TitleBar sender, object args)
{
if (ContentFrame.CanGoBack)
{
ContentFrame.GoBack();
}
}
private void TitleBar_PaneToggleRequested(TitleBar sender, object args)
{
NavigationViewControl.IsPaneOpen = !NavigationViewControl.IsPaneOpen;
}
private void NavigationView_ItemInvoked(NavigationView sender,
NavigationViewItemInvokedEventArgs args)
{
if (args.IsSettingsInvoked)
{
ContentFrame.Navigate(typeof(SettingsPage));
}
else if (args.InvokedItemContainer is NavigationViewItem item)
{
var tag = item.Tag?.ToString();
Type? pageType = tag switch
{
"main" => typeof(MainPage),
"documents" => typeof(DocumentsPage),
_ => null
};
if (pageType != null && ContentFrame.CurrentSourcePageType != pageType)
{
ContentFrame.Navigate(pageType);
}
}
}
}
MainWindow.xaml.cs
using Microsoft.UI.Xaml;
namespace MyApp;
public sealed partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// コンテンツをタイトルバー領域に拡張
ExtendsContentIntoTitleBar = true;
}
}
コンテンツページの例 (MainPage.xaml)
<Page
x:Class="MyApp.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Background="{ThemeResource SolidBackgroundFillColorBaseBrush}"
Padding="0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 上部コントロールエリア (パディング 24px) -->
<StackPanel Grid.Row="0" Padding="24,24,24,8" Spacing="12">
<TextBlock
Text="ホーム"
Style="{StaticResource TitleTextBlockStyle}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="更新" />
<ToggleButton Content="フィルター" />
</StackPanel>
</StackPanel>
<!-- メインコンテンツエリア -->
<GridView Grid.Row="1" Padding="16,0,16,16">
<!-- アイテム -->
</GridView>
</Grid>
</Page>
デバッグのコツ
1. 背景色を使った可視化
レイアウトの問題をデバッグする際は、背景色を設定して領域を可視化します:
<Page Background="#FF2D2D30">
<!-- コンテンツ -->
</Page>
または、各 Grid 要素に異なる背景色を設定:
<Grid Background="Red"> <!-- タイトルバー領域 -->
<Grid Background="Blue"> <!-- NavigationView 領域 -->
<Grid Background="Green"> <!-- コンテンツエリア -->
これにより、どの要素がどこに配置されているか、どこにマージンやパディングが適用されているかが一目瞭然になります。
2. Live Visual Tree の活用
Visual Studio のデバッグ機能「Live Visual Tree」を使用します:
- アプリケーションをデバッグ実行 (F5)
- メニューから「Debug」→「Windows」→「Live Visual Tree」を選択
- ビジュアルツリーで要素を選択し、プロパティを確認
特に以下のプロパティに注目:
MarginPaddingActualHeight/ActualWidthVerticalAlignment/HorizontalAlignment
3. XAML Hot Reload
XAML Hot Reload を活用して、リアルタイムで変更を確認します:
- マージンやパディングの値を変更して、すぐに結果を確認
- 背景色を変更して、領域を確認
- デバッグ実行を停止せずに調整可能
4. タイトルバーの高さを確認
タイトルバーの実際の高さをコードで確認します:
AppTitleBar.Loaded += (s, e) =>
{
System.Diagnostics.Debug.WriteLine($"TitleBar Height: {AppTitleBar.ActualHeight}");
};
通常、タイトルバーの高さは 48px ですが、DPI スケーリングやシステム設定によって変わる可能性があります。
よくある間違いと対処法
間違い 1: 負のマージンで調整しようとする
<!-- ❌ 避けるべき方法 -->
<NavigationView Grid.Row="1" Margin="0,-48,0,0">
問題点:
- DPI スケーリングで崩れる可能性
- タイトルバーの高さが変わると破綻
- 保守性が低い
正しい方法:
- リソースのオーバーライドを使用 (本記事の解決策)
間違い 2: 各ページで個別に調整
<!-- ❌ 避けるべき方法 -->
<Page Margin="0,-48,0,0">
問題点:
- すべてのページで同じ調整が必要
- DRY 原則に反する
- バグの温床
正しい方法:
- ShellPage レベルで一度だけ調整
間違い 3: Grid.RowSpan で無理やり重ねる
<!-- ❌ 避けるべき方法 -->
<NavigationView Grid.Row="0" Grid.RowSpan="2">
問題点:
- タイトルバーとコンテンツが重なる
- z-index の管理が複雑化
- クリック領域が競合
正しい方法:
- 適切な Grid 構造を使用
他のアプローチ
アプローチ 1: TitleBar を NavigationView の中に配置
<NavigationView>
<NavigationView.PaneCustomContent>
<TitleBar ... />
</NavigationView.PaneCustomContent>
</NavigationView>
メリット:
- レイアウトが一体化
デメリット:
- TitleBar の機能が制限される可能性
- ペインを閉じた時の挙動が複雑
アプローチ 2: 旧来の Window.SetTitleBar() 方式
// MainWindow.xaml.cs
ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar);
<!-- カスタムタイトルバー要素 -->
<Grid x:Name="AppTitleBar" Height="48">
<!-- タイトルバーのコンテンツ -->
</Grid>
メリット:
- 完全にカスタマイズ可能
デメリット:
- 実装が複雑
- アクセシビリティ対応が必要
- バックボタンなどの統合が手動
推奨: Windows App SDK 1.4 以降では、TitleBar コントロールの使用を推奨
参考リソース
公式ドキュメント
- Title bar - Windows apps | Microsoft Learn
- NavigationView - Windows apps | Microsoft Learn
- WinUI 3 Gallery - 公式サンプルアプリ
関連する問題とディスカッション
- NavigationView on WinUI 3 is not aware of Window.ExtendsContentIntoTitleBar value
- How to extend NavigationView back button properly into Title Bar
まとめ
WinUI 3 でタイトルバーと NavigationView を統合する際のポイント:
- TitleBar コントロールを使用: Windows App SDK 1.4 以降の新しい TitleBar コントロールを活用
- NavigationViewContentMargin をオーバーライド: デフォルトのマージンを 0 に設定
- 適切な Grid 構造: TitleBar と NavigationView を別の Row に配置
- ページレベルでパディング調整: 各ページで適切な上部パディング (通常 24px) を設定
- デバッグ時は可視化: 背景色や Live Visual Tree を活用
この方法により、左ペイン (ナビゲーションメニュー) と右ペイン (コンテンツエリア) が同じ垂直位置から開始され、統一感のある美しい UI を実現できます。
WinUI 3 はまだ進化中のフレームワークであり、今後のアップデートでこれらの問題が改善される可能性もあります。しかし、現時点では本記事で紹介した方法が最もシンプルで効果的な解決策です。
著者について
この記事は、実際の WinUI 3 プロジェクトの開発経験に基づいて執筆されました。同様の問題に直面している開発者の助けになれば幸いです。
🤖 この記事は Claude Code の支援を受けて作成されました。