.NET MAUI の Shell を Windows で使うと、Flyout(ハンバーガーメニュー)の各項目が「間隔ゼロ」で縦に並び、アプリの情報量が増えるほど読みづらくなりがちです。本稿では、標準 API の制約を踏まえつつ、区切り線と余白を“安全に”追加するための実践テクニックを、XAML テンプレート/グループ化/ハンドラー拡張まで段階的に解説します。
前提とゴール
対象は .NET MAUI Shell を採用した Windows デスクトップ(WinUI 3)アプリです。目標は次のとおりです。
- Flyout の項目間に区切り線と視認性の高い余白を持たせる。
- 可能な限り標準機能で実現し、将来的な MAUI の更新でも壊れにくい構成にする。
- どうしても必要な場合のみ、ネイティブ層(WinUI)のハンドラー拡張で補う。
結論(要点)
- 標準 API だけでは「項目間に任意の UI を直接挿入」できない。Flyout の内部レイアウトはフレームワーク管理であり、項目と項目の“間”に BoxView などを差し込むプロパティはありません。
- 最も実用的な回避策は、FlyoutItem を「グループ化」して擬似的に区切りを作ること。
FlyoutDisplayOptions="AsMultipleItems"を使うと、子要素が個別行で現れ、グループの上下に自動の区切りが入ります。 - 見た目の自由度を上げるなら、ItemTemplate で各行の内側に“擬似的な仕切り”と余白を描く。「間」に直接置けない制約は残るものの、各行のボトムに 1px ラインと内側余白を仕込めます。
- さらに細かい制御が必要なら、ハンドラー拡張で Windows の NavigationView を直接調整。保守コストとリスクは上がるため、最終手段として位置付けます。
なぜ「直接挿入できない」のか(仕組みの理解)
Shell の Flyout はプラットフォームごとに内部実装が異なり、Windows では WinUI の NavigationView を用いて描画されます。MAUI 側は Flyout の各項目を “リスト項目”として束ねており、アプリ開発者は基本的に「項目(行)」のテンプレートは差し替えられても、「項目と項目のあいだ」に独立した要素(区切り専用行や空白行)を直接挿入する API を持ちません。ゆえに、行の内側に下罫線を描く/グループ分割による自動区切りを利用するといった“擬似的な”アプローチが中心になります。
一番ラクな回避策:FlyoutItem をグループ化し、区切りを擬似的に作る
「機能単位でグループ」→「グループの子を個別行として表示」→「グループ境界に自動の区切り」という流れで、視覚的なブロッキングを簡単に得られます。次の例では「メイン」と「管理」を分けて、行の塊を作ります。
<Shell
x:Class="SampleApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SampleApp">
<FlyoutItem Title="メイン" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Home" Icon="home.png">
<ShellContent Title="ホーム"
ContentTemplate="{DataTemplate local:HomePage}" />
</Tab>
<Tab Title="Settings" Icon="settings.png">
<ShellContent Title="設定"
ContentTemplate="{DataTemplate local:SettingsPage}" />
</Tab>
</FlyoutItem>
<FlyoutItem Title="管理" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Users">
<ShellContent Title="ユーザー"
ContentTemplate="{DataTemplate local:UsersPage}" />
</Tab>
<Tab Title="Roles">
<ShellContent Title="権限"
ContentTemplate="{DataTemplate local:RolesPage}" />
</Tab>
</FlyoutItem>
<Shell.MenuItems>
<MenuItem Text="サインアウト"
Command="{Binding SignOutCommand}" />
</Shell.MenuItems>
</Shell>
ポイント
FlyoutDisplayOptions="AsMultipleItems"を付けたFlyoutItemの子(Tab/ShellContent)は個別の行として並びます。- グループの上端・下端には自動の区切りが入り、まとまりが視覚化されます。
- 区切り位置を変えたい場合は、
FlyoutItemを複数に分けて表示順序を調整します。 <Shell.MenuItems>は「メイン項目」とは別枠で並ぶため、自然な大区切りを作るのにも有効です(例:サインアウトやライセンス表示などアクション系)。
グループ化の設計ヒント
- ユーザーの仕事の流れ(ホーム → 操作 → 管理)に合わせてブロックを切る。
- 10 項目を 2~3 ブロックに分けるだけでも、走査時間と誤タップが減ります。
- 将来項目が増える前提で「成長するグループ」を用意しておくと保守が楽です。
テンプレートで「行の内側」に罫線と余白を仕込む
「間」に直接 UI を差し込めない代わりに、各行の中でボトム罫線と内側余白を描く方法です。これにより、パッと見で行が分離し、クリック範囲も適切に広がります。
<Shell
x:Class="SampleApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Shell.Resources>
<Style x:Key="FlyoutTextStyle" TargetType="Label">
<Setter Property="FontSize" Value="14" />
<Setter Property="Padding" Value="16,10" /> <!-- 行の内側余白 -->
</Style>
<Color x:Key="SeparatorLight">#1F000000</Color>
<Color x:Key="SeparatorDark">#1FFFFFFF</Color>
<AppThemeColor x:Key="SeparatorColor"
Light="{StaticResource SeparatorLight}"
Dark="{StaticResource SeparatorDark}" />
</Shell.Resources>
<Shell.ItemTemplate>
<DataTemplate>
<Grid RowDefinitions="Auto,1">
<Label Grid.Row="0"
Text="{Binding Title}"
Style="{StaticResource FlyoutTextStyle}" />
<BoxView Grid.Row="1"
HeightRequest="1"
Margin="16,0" >
<BoxView.Background>
<SolidColorBrush Color="{StaticResource SeparatorColor}" />
</BoxView.Background>
</BoxView>
</Grid>
</DataTemplate>
</Shell.ItemTemplate>
<!-- FlyoutItem 群は前掲例と同様 -->
</Shell>
注意点
- これは行の内側に線を描く方式で、項目と項目の“外側の間”に UI を挿入するわけではありません(標準 API の制約は維持)。
- 選択ハイライトの色やフォーカス可視化はプラットフォーム依存です。テンプレートを凝り過ぎると OS 標準のアクセシビリティに逆らう可能性があるため、罫線は薄く、余白はほどほどに。
- ダーク/ライトの切替には
AppThemeColorを必ず使い、コントラスト比を確保します。
余白をもう少しだけ広げたい(最小限の調整)
読みやすさの観点では、ラベルの上下に 8~12px 程度の余白があれば十分です。触り心地(ヒット領域)を重視するなら 14~16px まで広げます。
<Style x:Key="FlyoutTextStyle" TargetType="Label">
<Setter Property="FontSize" Value="14" />
<Setter Property="Padding" Value="20,12" /> <!-- ほどよく拡大 -->
<Setter Property="LineBreakMode" Value="TailTruncation" />
</Style>
ヘッダー/フッターで「大きな区切り」を作る
グローバルな区切りや告知、アカウント情報などは FlyoutHeader / FlyoutFooter(または各 Template)を活用します。ここに太めのボーダーや背景を与えると、メインリストと明確に分離できます。
<Shell ...>
<Shell.FlyoutHeaderTemplate>
<DataTemplate>
<Grid Padding="16" RowDefinitions="Auto,1">
<Label Text="{Binding UserName}" FontAttributes="Bold" />
<BoxView Grid.Row="1" HeightRequest="2" BackgroundColor="#33000000" />
</Grid>
</DataTemplate>
</Shell.FlyoutHeaderTemplate>
<Shell.FlyoutFooterTemplate>
<DataTemplate>
<Grid Padding="16" RowDefinitions="1">
<BoxView HeightRequest="2" BackgroundColor="#33000000" />
</Grid>
</DataTemplate>
</Shell.FlyoutFooterTemplate>
</Shell>
Windows 限定:ハンドラー拡張で NavigationView を直接さわる(上級)
どうしても「項目と項目の間」にセパレーターを挿入したい、行の高さ・ホバー厚みを OS レベルで変えたい、といった要件ではハンドラー拡張を検討します。以下は最低限の「接続時フック」例です。
// MauiProgram.cs
using Microsoft.Maui.Hosting;
using Microsoft.Maui.Handlers;
#if WINDOWS
using Microsoft.UI.Xaml.Controls;
#endif
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
#if WINDOWS
ShellHandler.Mapper.AppendToMapping("FlyoutTuning", (handler, view) =>
{
var nav = handler.PlatformView as NavigationView;
if (nav is null) return;
// 例:項目間に太めのセパレーターを差し込む(擬似)
// 注意:MAUI の仮想化や再構成で消える可能性があるためテスト必須
// nav.MenuItems.Insert( ... new NavigationViewItemSeparator() );
// 例:コンパクトモード時のアイテム高さを調整するスタイルを適用
// nav.Resources["NavigationViewItemMinHeight"] = 44d;
});
#endif
return builder.Build();
}
}
リスクと対策
- Flyout の再生成や仮想化で独自に挿した要素が消える可能性。ナビゲーション遷移・ウィンドウサイズ変更・テーマ切替など、再作成が起きやすい操作を回して検証しましょう。
- 将来の MAUI/WinUI 更新で内部構造が変わると壊れます。機能フラグ(オプトイン)とフェイルセーフを設け、動作異常時はデフォルト描画にフォールバックする実装に。
- 単体テストは難しいため、UI テスト(表示要素の数・順序・TabIndex・フォーカス移動)を用意すると安心です。
どの方法を選ぶべきか(比較表)
| 手法 | できること | 保守性 | 工数 | おすすめ度 |
|---|---|---|---|---|
| FlyoutItem のグループ化(AsMultipleItems) | 大きな塊ごとに自動区切り。順序を変えて柔軟に配置。 | 高い(標準機能のみ) | 低い | ◎ 最優先 |
| Shell.ItemTemplate で行内に罫線・余白 | 細かい見た目調整(各行のボトム線/内側余白) | 中(テンプレート変更の影響に注意) | 中 | ○ 併用推奨 |
| FlyoutHeader / Footer | 全体を分ける太めの区切り、アカウント表示など | 高い | 低い | ○ |
| ハンドラー拡張(NavigationView 直接操作) | 項目間の実セパレーター、行高・ホバーの OS レベル調整 | 低い(内部構造依存) | 高い | △ 最終手段 |
| Shell を使わない(メニュー自作) | 完全自由(CollectionView で好きな UI) | 中(設計次第) | 高い(ナビゲーション作り込み必須) | △ 大規模案件で検討 |
最短レシピ:現場で実用になる組み合わせ
- グループ化:機能単位で
FlyoutItemを分け、AsMultipleItemsを設定。 - テンプレート:
Shell.ItemTemplateに薄いボトム線(1px)と内側余白(上下 10–14px)を設定。 - ヘッダー/フッター:アカウント情報と太めの仕切りを置き、主リストと切り分ける。
- 動作確認:ダーク/ライト、ウィンドウ幅 800px 未満、スクリーンリーダー有効時、タッチ操作のすべてで視認性・操作性をチェック。
実装サンプル(まとめ版)
以下は上記の各要素を一つにまとめた XAML 例です。これをベースに調整していくと、初期コストを抑えつつ読みやすい Flyout を素早く構築できます。
<Shell
x:Class="SampleApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SampleApp"
Title="SampleApp">
<!-- 色・余白・罫線 -->
<Shell.Resources>
<AppThemeColor x:Key="SeparatorColor" Light="#1F000000" Dark="#1FFFFFFF" />
<Style x:Key="FlyoutTextStyle" TargetType="Label">
<Setter Property="FontSize" Value="14" />
<Setter Property="Padding" Value="20,12" />
</Style>
</Shell.Resources>
<!-- 行テンプレート(ボトム罫線+内側余白) -->
<Shell.ItemTemplate>
<DataTemplate>
<Grid RowDefinitions="Auto,1">
<Label Grid.Row="0" Text="{Binding Title}" Style="{StaticResource FlyoutTextStyle}" />
<BoxView Grid.Row="1" HeightRequest="1" Margin="16,0"
BackgroundColor="{StaticResource SeparatorColor}" />
</Grid>
</DataTemplate>
</Shell.ItemTemplate>
<!-- ヘッダー(大区切り) -->
<Shell.FlyoutHeaderTemplate>
<DataTemplate>
<Grid Padding="16" RowDefinitions="Auto,1">
<Label Text="SampleApp" FontAttributes="Bold" FontSize="18" />
<BoxView Grid.Row="1" HeightRequest="2" BackgroundColor="#33000000" />
</Grid>
</DataTemplate>
</Shell.FlyoutHeaderTemplate>
<!-- グループ1:メイン -->
<FlyoutItem Title="メイン" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Home" Icon="home.png">
<ShellContent Title="ホーム" ContentTemplate="{DataTemplate local:HomePage}" />
</Tab>
<Tab Title="Search" Icon="search.png">
<ShellContent Title="検索" ContentTemplate="{DataTemplate local:SearchPage}" />
</Tab>
<Tab Title="Settings" Icon="settings.png">
<ShellContent Title="設定" ContentTemplate="{DataTemplate local:SettingsPage}" />
</Tab>
</FlyoutItem>
<!-- グループ2:管理 -->
<FlyoutItem Title="管理" FlyoutDisplayOptions="AsMultipleItems">
<Tab Title="Users">
<ShellContent Title="ユーザー" ContentTemplate="{DataTemplate local:UsersPage}" />
</Tab>
<Tab Title="Roles">
<ShellContent Title="権限" ContentTemplate="{DataTemplate local:RolesPage}" />
</Tab>
</FlyoutItem>
<!-- アクション系(別枠) -->
<Shell.MenuItems>
<MenuItem Text="サポート"
Command="{Binding OpenSupportCommand}" />
<MenuItem Text="サインアウト"
Command="{Binding SignOutCommand}" />
</Shell.MenuItems>
</Shell>
アクセシビリティと可読性のベストプラクティス
- ヒット領域は十分に:行の内側余白は上下 10–16px、左右 16–20px を目安に。
- コントラスト比:罫線は主張しすぎない濃度(不透明度 10–20% 程度)が無難。テキストは背景に対して WCAG の最小コントラスト比を意識。
- キーボード操作:上下矢印・Enter で移動/決定できることを確認。フォーカス可視化を壊さないテンプレートにする。
- テーマ切替:
AppThemeColorを徹底。直値の#FFFFFF等は避け、ライト/ダークそれぞれで検証。 - 縮小時の折り返し:タイトルが長い場合は
LineBreakMode="TailTruncation"で末尾省略し、二行化は避ける。
パフォーマンスと保守
- テンプレートは軽く:各行に複雑なレイアウトを入れると描画コストが跳ねます。
Grid + Label + BoxView程度が最適。 - 資産の一元管理:罫線色・余白量は
Shell.Resourcesでキー化し、別ページでも再利用。 - 実機検証:高 DPI ディスプレイで 1px は細すぎることがあります。必要に応じて 2px の線+薄色へ。
よくある質問(FAQ)
Q. 本当に「項目の間」に BoxView などを差し込めませんか?
A. 標準 API ではできません。行の内側に線や余白を描くか、グループ境界の自動区切りを活用します。
Q. MenuItem で空行のようなものを作れますか?
A. クリック不能な“ダミー”行は推奨されません。見た目の空きはテンプレート側の余白で作り、意味のある行だけを表示しましょう。
Q. モバイル(Android/iOS)では同じ見た目になりますか?
A. Flyout の描画は OS 毎に異なります。Windows での視認性改善はそのままでも、モバイルで過度な余白に感じられる場合は、OnPlatform で余白量などを出し分けると良いです。
Q. どうしても「実セパレーター」が必要です。
A. ハンドラー拡張で NavigationView に NavigationViewItemSeparator を挿入する手がありますが、内部構造依存で壊れやすく、再生成時の維持が課題です。十分なテストとフォールバックを前提に検討してください。
設計チェックリスト
- 情報のまとまりごとに FlyoutItem を分割したか。
- 各行の 内側余白は 10–16px の範囲か。
- 罫線は AppThemeColor でライト/ダーク両対応か。
- ヘッダー/フッターで 大区切りを設けたか。
- キーボード、スクリーンリーダー、タッチで 操作確認したか。
まとめ
Shell の Flyout は、標準 API だけでは「項目と項目の間」に自由な UI を差し込めません。そこで、グループ化(AsMultipleItems)で自動区切りを得つつ、ItemTemplate で各行の内側に罫線と余白を仕込むのが、手堅く保守性の高い解です。より大胆な見た目が必要ならヘッダー/フッターを併用し、最後の一線としてハンドラー拡張を検討しましょう。これらを組み合わせれば、Windows の Flyout でも読みやすく、迷いにくいナビゲーションを実現できます。

コメント