.NET MAUI ShellのFlyoutに区切り線と余白を入れる最適解(Windows対応の実装ガイド)

.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 を差し込めない代わりに、各行の中でボトム罫線と内側余白を描く方法です。これにより、パッと見で行が分離し、クリック範囲も適切に広がります。

&lt;Shell
  x:Class="SampleApp.AppShell"
  xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"&gt;

  &lt;Shell.Resources&gt;
    &lt;Style x:Key="FlyoutTextStyle" TargetType="Label"&gt;
      &lt;Setter Property="FontSize" Value="14" /&gt;
      &lt;Setter Property="Padding" Value="16,10" /&gt; &lt;!-- 行の内側余白 --&gt;
    &lt;/Style&gt;

    &lt;Color x:Key="SeparatorLight"&gt;#1F000000&lt;/Color&gt;
    &lt;Color x:Key="SeparatorDark"&gt;#1FFFFFFF&lt;/Color&gt;
    &lt;AppThemeColor x:Key="SeparatorColor"
                   Light="{StaticResource SeparatorLight}"
                   Dark="{StaticResource SeparatorDark}" /&gt;
  &lt;/Shell.Resources&gt;

  &lt;Shell.ItemTemplate&gt;
    &lt;DataTemplate&gt;
      &lt;Grid RowDefinitions="Auto,1"&gt;
        &lt;Label Grid.Row="0"
               Text="{Binding Title}"
               Style="{StaticResource FlyoutTextStyle}" /&gt;

        &lt;BoxView Grid.Row="1"
                 HeightRequest="1"
                 Margin="16,0" &gt;
          &lt;BoxView.Background&gt;
            &lt;SolidColorBrush Color="{StaticResource SeparatorColor}" /&gt;
          &lt;/BoxView.Background&gt;
        &lt;/BoxView&gt;
      &lt;/Grid&gt;
    &lt;/DataTemplate&gt;
  &lt;/Shell.ItemTemplate&gt;

  &lt;!-- FlyoutItem 群は前掲例と同様 --&gt;
&lt;/Shell&gt;

注意点

  • これは行の内側に線を描く方式で、項目と項目の“外側の間”に UI を挿入するわけではありません(標準 API の制約は維持)。
  • 選択ハイライトの色やフォーカス可視化はプラットフォーム依存です。テンプレートを凝り過ぎると OS 標準のアクセシビリティに逆らう可能性があるため、罫線は薄く、余白はほどほどに。
  • ダーク/ライトの切替には AppThemeColor を必ず使い、コントラスト比を確保します。

余白をもう少しだけ広げたい(最小限の調整)

読みやすさの観点では、ラベルの上下に 8~12px 程度の余白があれば十分です。触り心地(ヒット領域)を重視するなら 14~16px まで広げます。

&lt;Style x:Key="FlyoutTextStyle" TargetType="Label"&gt;
  &lt;Setter Property="FontSize" Value="14" /&gt;
  &lt;Setter Property="Padding" Value="20,12" /&gt; &lt;!-- ほどよく拡大 --&gt;
  &lt;Setter Property="LineBreakMode" Value="TailTruncation" /&gt;
&lt;/Style&gt;

ヘッダー/フッターで「大きな区切り」を作る

グローバルな区切りや告知、アカウント情報などは FlyoutHeader / FlyoutFooter(または各 Template)を活用します。ここに太めのボーダーや背景を与えると、メインリストと明確に分離できます。

&lt;Shell ...&gt;
  &lt;Shell.FlyoutHeaderTemplate&gt;
    &lt;DataTemplate&gt;
      &lt;Grid Padding="16" RowDefinitions="Auto,1"&gt;
        &lt;Label Text="{Binding UserName}" FontAttributes="Bold" /&gt;
        &lt;BoxView Grid.Row="1" HeightRequest="2" BackgroundColor="#33000000" /&gt;
      &lt;/Grid&gt;
    &lt;/DataTemplate&gt;
  &lt;/Shell.FlyoutHeaderTemplate&gt;

  &lt;Shell.FlyoutFooterTemplate&gt;
    &lt;DataTemplate&gt;
      &lt;Grid Padding="16" RowDefinitions="1"&gt;
        &lt;BoxView HeightRequest="2" BackgroundColor="#33000000" /&gt;
      &lt;/Grid&gt;
    &lt;/DataTemplate&gt;
  &lt;/Shell.FlyoutFooterTemplate&gt;
&lt;/Shell&gt;

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) =&gt;
        {
            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)中(設計次第)高い(ナビゲーション作り込み必須)△ 大規模案件で検討

最短レシピ:現場で実用になる組み合わせ

  1. グループ化:機能単位で FlyoutItem を分け、AsMultipleItems を設定。
  2. テンプレート:Shell.ItemTemplate に薄いボトム線(1px)と内側余白(上下 10–14px)を設定。
  3. ヘッダー/フッター:アカウント情報と太めの仕切りを置き、主リストと切り分ける。
  4. 動作確認:ダーク/ライト、ウィンドウ幅 800px 未満、スクリーンリーダー有効時、タッチ操作のすべてで視認性・操作性をチェック。

実装サンプル(まとめ版)

以下は上記の各要素を一つにまとめた XAML 例です。これをベースに調整していくと、初期コストを抑えつつ読みやすい Flyout を素早く構築できます。

&lt;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"&gt;

  &lt;!-- 色・余白・罫線 --&gt;
  &lt;Shell.Resources&gt;
    &lt;AppThemeColor x:Key="SeparatorColor" Light="#1F000000" Dark="#1FFFFFFF" /&gt;
    &lt;Style x:Key="FlyoutTextStyle" TargetType="Label"&gt;
      &lt;Setter Property="FontSize" Value="14" /&gt;
      &lt;Setter Property="Padding" Value="20,12" /&gt;
    &lt;/Style&gt;
  &lt;/Shell.Resources&gt;

  &lt;!-- 行テンプレート(ボトム罫線+内側余白) --&gt;
  &lt;Shell.ItemTemplate&gt;
    &lt;DataTemplate&gt;
      &lt;Grid RowDefinitions="Auto,1"&gt;
        &lt;Label Grid.Row="0" Text="{Binding Title}" Style="{StaticResource FlyoutTextStyle}" /&gt;
        &lt;BoxView Grid.Row="1" HeightRequest="1" Margin="16,0"
                 BackgroundColor="{StaticResource SeparatorColor}" /&gt;
      &lt;/Grid&gt;
    &lt;/DataTemplate&gt;
  &lt;/Shell.ItemTemplate&gt;

  &lt;!-- ヘッダー(大区切り) --&gt;
  &lt;Shell.FlyoutHeaderTemplate&gt;
    &lt;DataTemplate&gt;
      &lt;Grid Padding="16" RowDefinitions="Auto,1"&gt;
        &lt;Label Text="SampleApp" FontAttributes="Bold" FontSize="18" /&gt;
        &lt;BoxView Grid.Row="1" HeightRequest="2" BackgroundColor="#33000000" /&gt;
      &lt;/Grid&gt;
    &lt;/DataTemplate&gt;
  &lt;/Shell.FlyoutHeaderTemplate&gt;

  &lt;!-- グループ1:メイン --&gt;
  &lt;FlyoutItem Title="メイン" FlyoutDisplayOptions="AsMultipleItems"&gt;
    &lt;Tab Title="Home" Icon="home.png"&gt;
      &lt;ShellContent Title="ホーム" ContentTemplate="{DataTemplate local:HomePage}" /&gt;
    &lt;/Tab&gt;
    &lt;Tab Title="Search" Icon="search.png"&gt;
      &lt;ShellContent Title="検索" ContentTemplate="{DataTemplate local:SearchPage}" /&gt;
    &lt;/Tab&gt;
    &lt;Tab Title="Settings" Icon="settings.png"&gt;
      &lt;ShellContent Title="設定" ContentTemplate="{DataTemplate local:SettingsPage}" /&gt;
    &lt;/Tab&gt;
  &lt;/FlyoutItem&gt;

  &lt;!-- グループ2:管理 --&gt;
  &lt;FlyoutItem Title="管理" FlyoutDisplayOptions="AsMultipleItems"&gt;
    &lt;Tab Title="Users"&gt;
      &lt;ShellContent Title="ユーザー" ContentTemplate="{DataTemplate local:UsersPage}" /&gt;
    &lt;/Tab&gt;
    &lt;Tab Title="Roles"&gt;
      &lt;ShellContent Title="権限" ContentTemplate="{DataTemplate local:RolesPage}" /&gt;
    &lt;/Tab&gt;
  &lt;/FlyoutItem&gt;

  &lt;!-- アクション系(別枠) --&gt;
  &lt;Shell.MenuItems&gt;
    &lt;MenuItem Text="サポート"
              Command="{Binding OpenSupportCommand}" /&gt;
    &lt;MenuItem Text="サインアウト"
              Command="{Binding SignOutCommand}" /&gt;
  &lt;/Shell.MenuItems&gt;

&lt;/Shell&gt;

アクセシビリティと可読性のベストプラクティス

  • ヒット領域は十分に:行の内側余白は上下 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 でも読みやすく、迷いにくいナビゲーションを実現できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次