WinUI 3×ItemsRepeaterで非均一タイルを整列する実装ガイド|LinedFlowLayoutとElementPreparedで幅2倍を安定適用

WinUI 3 の ItemsRepeater と LinedFlowLayout で Pinterest 風の“ときどき大きい”タイルを実現したい──そんなときに最小手数で確実に動く実装手順を、仮想化の落とし穴まで含めて徹底解説します。インデックス 2 と 6 を幅 2 倍にする例を軸に、拡張しやすいコード構成と運用のコツをまとめました。

目次

前提とゴールの整理

本記事のゴールは、WinUI 3 の ItemsRepeater+LinedFlowLayout を使ったタイル表示で、「一部のアイテムだけ幅(や高さ)を 2 倍にする」非均一レイアウトを、仮想化と再利用に強いかたちで安定実装することです。

  • 課題:通常のタイルは 100px。インデックス 2 と 6 だけ 200px(2 倍)にしたい。
  • 要件:スクロール時の仮想化(要素の生成・再利用)でもサイズが勝手に戻らないこと。
  • 使うレイアウト:LinedFlowLayout(横幅が変わると自動的に折り返し位置が調整される)

全体像:最小実装の考え方

最もシンプルで堅牢な方針は、ItemsRepeater.ElementPrepared イベントで毎回サイズをセットすることです。ElementPrepared は仮想化による要素の生成・再利用のたびに呼ばれるため、ここで “対象インデックスなら 2 倍、それ以外なら標準” を必ず適用し直します。これにより、スクロールでアイテムが再利用されてもサイズがリセットされる問題を防げます。

最小構成の XAML

まずはページに ItemsRepeater を配置し、LinedFlowLayout を設定。タイルのルートは Border とし、後で名前で参照できるよう x:Name="ItemBorder" を付けます。

<Page
    x:Class="SampleApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="using:Microsoft.UI.Xaml.Controls"
    xmlns:local="using:SampleApp">

  <Grid Padding="16">
    <controls:ItemsRepeater x:Name="ir1"
                            ItemsSource="{x:Bind Items, Mode=OneWay}"
                            ElementPrepared="ir1_ElementPrepared">
      <controls:ItemsRepeater.Layout>
        <controls:LinedFlowLayout />
      </controls:ItemsRepeater.Layout>

      <controls:ItemsRepeater.ItemTemplate>
        <DataTemplate x:DataType="local:TileItem">
          <Border x:Name="ItemBorder"
                  Width="100"
                  Height="100"
                  CornerRadius="8"
                  Margin="4"
                  Background="{Binding BaseColor}">
            <TextBlock Text="{x:Bind Title}"
                       FontSize="20"
                       FontWeight="SemiBold"
                       VerticalAlignment="Center"
                       HorizontalAlignment="Center"/>
          </Border>
        </DataTemplate>
      </controls:ItemsRepeater.ItemTemplate>
    </controls:ItemsRepeater>
  </Grid>
</Page>

データモデルと初期データ

標準幅のタイル 10 個を用意します。タイトルだけの素朴なモデルから始め、必要なら色などを後で足します。

using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System.Collections.ObjectModel;
using System.Collections.Generic;

namespace SampleApp;

public sealed partial class MainPage : Page
{
    public ObservableCollection<TileItem> Items { get; } = new();

    // 今回は「2 と 6」を 2 倍にするルール
    private readonly HashSet<int> _wideIndices = new() { 2, 6 };

    public MainPage()
    {
        this.InitializeComponent();

        for (int i = 0; i < 10; i++)
        {
            Items.Add(new TileItem
            {
                Title = $"Item {i:D2}",
                BaseColor = new SolidColorBrush(Colors.LightGray)
            });
        }
    }

    private void ir1_ElementPrepared(ItemsRepeater sender,
                                     ItemsRepeaterElementPreparedEventArgs args)
    {
        int index = sender.GetElementIndex(args.Element);

        if (args.Element is FrameworkElement fe
            && fe.FindName("ItemBorder") is Border border)
        {
            bool isWide = _wideIndices.Contains(index);

            // 幅 2 倍(標準 100 の想定)
            border.Width = isWide ? 200 : 100;

            // 高さも変えたい場合はここで設定可能(例:縦 2 倍)
            // border.Height = isWide ? 200 : 100;

            // 視覚確認用の色(本番不要なら削除)
            border.Background = new SolidColorBrush(
                isWide ? Colors.IndianRed : Colors.SteelBlue);
        }
    }
}

public class TileItem
{
    public string Title { get; set; } = "";
    public Brush BaseColor { get; set; } = new SolidColorBrush(Colors.LightGray);
}

キモは「毎回セット」:仮想化に強い書き方

ItemsRepeater はパフォーマンスのために要素を再利用(リサイクル)します。つまり、一度 200px にした Border が、スクロール後に別インデックスのアイテムに再割り当てされることがあります。だからこそ、ElementPrepared のたびに “正しいサイズ” を必ずセットし直す必要があります。

  • ダメな例:ロード時に一度だけ Width を変更する。
  • 良い例:ElementPrepared 内で index を判定して、毎回 Width/Height を設定。

この「毎回セット」さえ守れば、スクロールやデータの増減があっても、サイズの取り違えは起きません。

インデックス以外のルールへ広げる

「2 と 6 を広く」のような固定インデックスから始めても、実務ではさまざまなルールが欲しくなります。以下のように柔軟に拡張できます。

モデル側のフラグで制御(推奨)

データ駆動にしたい場合、モデルに IsWide を追加し、ElementPrepared で参照します。並べ替えやフィルタにも強く、テストもしやすい構成です。

public class TileItem
{
    public string Title { get; set; } = "";
    public bool IsWide { get; set; }
}

private void ir1_ElementPrepared(ItemsRepeater sender,
                                 ItemsRepeaterElementPreparedEventArgs args)
{
    int index = sender.GetElementIndex(args.Element);

    if (args.Element is FrameworkElement fe
        && fe.DataContext is TileItem vm
        && fe.FindName("ItemBorder") is Border border)
    {
        bool isWide = vm.IsWide; // データが真理
        border.Width = isWide ? 200 : 100;
    }
}

周期ルール(例:3 枚ごとに 2 枚目を 2 倍)

bool isWide = (index % 3) == 1; // 0-based: 1, 4, 7, ...
border.Width = isWide ? 200 : 100;

条件を関数化してテスト容易に

private bool ShouldBeWide(int index) => index == 2 || index == 6;

...
border.Width = ShouldBeWide(index) ? 200 : 100; 

高さも自在:横 2 倍+縦 2 倍の長方形にも

縦方向も伸ばしたいなら Height を同じ場所で設定します。LinedFlowLayout は幅や高さの違いに応じて折り返し・整列を再計算してくれます。

bool isWide = _wideIndices.Contains(index);

// 横だけ 2 倍
border.Width  = isWide ? 200 : 100;
border.Height = 100;

// たとえば縦も 2 倍にしたい場合
// border.Height = isWide ? 200 : 100;

スタイル切替で見通しを良くする

Width/Height や角丸など、複数の外観をまとめて切り替えたいなら、スタイルを 2 つ用意して ElementPrepared で適用するのが整然です。

<Page.Resources>
  <Style x:Key="TileNormal" TargetType="Border">
    <Setter Property="Width"  Value="100"/>
    <Setter Property="Height" Value="100"/>
    <Setter Property="CornerRadius" Value="8"/>
  </Style>

  <Style x:Key="TileWide" TargetType="Border">
    <Setter Property="Width"  Value="200"/>
    <Setter Property="Height" Value="100"/>
    <Setter Property="CornerRadius" Value="8"/>
  </Style>
</Page.Resources>
border.Style = ShouldBeWide(index) ? (Style)Resources["TileWide"]
                                   : (Style)Resources["TileNormal"];

LinedFlowLayout と折り返しの挙動

LinedFlowLayout は「行(line)」の概念を持ち、アイテム幅が異なっても自然に折り返し・整列されます。横幅 200 のタイルが途中に混ざると、その分だけスペースの使い方が変わり、次アイテムの配置位置も調整されます。特別なアルゴリズムを実装しなくても、見栄えのよいタイル列を得やすいのが利点です。

ただし、常に「最適充填」が得られるわけではありません。見た目をさらに整えたい場合は、アイテム間の余白やコンテナ幅(親 Grid やウィンドウ幅)を調整したり、広いタイルの出現位置を規則化する(例:行頭に必ず広いタイルが来るようインデックスを選ぶ)と崩れが減ります。

よくある落とし穴と対策

症状原因対処
スクロール後にサイズが戻る/崩れる仮想化で要素が再利用。サイズを一度しか設定していないElementPrepared のたびに必ず Width/Height をセット
配置が瞬時に反映されない測定/配置のスケジューリング待ち大量変更時は最後に ir1.InvalidateMeasure() を呼ぶ
アクセシビリティでフォーカス移動が直感的でない不均一グリッドはフォーカスナビゲーションが難しい方向キーでの移動(XY フォーカス)を手当。必要に応じてカスタム
行頭・行末の余白が気になる幅 2 倍タイルの出現位置と親の幅の組合せ広いタイルの出現規則を固定化/ウィンドウ幅に応じルールを切替

大量データ時のパフォーマンス指針

  • サイズ計算を O(1) に:HashSet<int> などで即判定。重いロジックは避ける。
  • 見た目変更はまとめて:多数の IsWide 更新後に InvalidateMeasure() を 1 度。
  • 色やスタイルの計算をキャッシュ:Style 切替で再計算を減らす。
  • 画像読み込みは遅延:タイル内に画像を置く場合は遅延読み込みや低解像度プレビューを併用。

応用:動的ルールの切替(レスポンシブ)

ウィンドウ幅が狭いときは全アイテムを等幅に、広いときは「2 と 6 を 2 倍」にするなど、ルールの切替も簡単です。サイズクラスの変化やウィンドウ幅の変更イベントで内部フラグを切り替え、ItemsRepeater を再測定すれば OK です。

bool _useWideRule = true;

private void OnWindowSizeChanged(object sender, WindowSizeChangedEventArgs e)
{
    _useWideRule = e.Size.Width &gt;= 800;
    ir1.InvalidateMeasure(); // 全体の再レイアウト
}

private void ir1_ElementPrepared(ItemsRepeater sender,
                                 ItemsRepeaterElementPreparedEventArgs args)
{
    int index = sender.GetElementIndex(args.Element);

    if (args.Element is FrameworkElement fe
        &amp;&amp; fe.FindName("ItemBorder") is Border border)
    {
        bool isWide = _useWideRule &amp;&amp; (index == 2 || index == 6);
        border.Width = isWide ? 200 : 100;
    }
}

品質向上のためのチェックリスト

  • キーボード操作で各タイルに到達できるか(フォーカス可視化・順序)
  • スクリーンリーダーで読み上げ順が視覚順と矛盾していないか
  • マウス/タッチで広いタイルと狭いタイルの操作領域に差が出すぎていないか
  • 表示密度が高いときにもラベルが欠けないか(省略記号・ツールチップ)

「本格 Masonry(マス目)風」を目指す場合

WinUI 3 には Pinterest のような完全 Masonry を実現するレイアウトが標準では用意されていません。もっと自由度の高い配置が必要なら、次のアプローチを検討します。

  • UniformGridLayout をベースに疑似スパン:アイテムの幅/高さを倍数で統一し、出現位置のルールを工夫する。
  • サードパーティ:CommunityToolkit for WinUI 3 などで Masonry 風のレイアウトが提供されていないか確認。
  • 独自の VirtualizingLayout を実装:MeasureOverride/ArrangeOverride 相当で列ごとの最短列に積むアルゴリズムを記述。

まずは本記事の「幅/高さを個別に変える」アプローチで要件を満たせるかを見極め、足りなければ上記に進むのがおすすめです。

実運用に効くテスト観点

  • スクロール往復テスト:最上部→最下部→最上部を繰り返し、サイズブレが起きないかを検証。
  • 動的変更テスト:対象インデックスの追加/削除後に InvalidateMeasure() を呼び、配置が即時反映されるか。
  • リサイズテスト:ウィンドウの横幅を境界値(例:799px/800px)で往復させ、折り返しが不自然にならないか。

よくある質問(FAQ)

Q. XAML だけでインデックス判定して幅を変えられますか?
A. 直接インデックスにアクセスする仕組みはないため、ElementPrepared での制御がシンプルで堅牢です。データ駆動にしたい場合はモデルに IsWide を持たせてください。

Q. ElementClearing は使うべき?
A. 追加の後片付けが必要な場合に有用ですが、サイズは ElementPrepared で毎回セットしておけば、ElementClearing でリセットする必要は基本的にありません。

Q. LinedFlowLayout と UniformGridLayout の違いは?
A. 前者は幅/高さがばらつくアイテムを“行”の単位で自然に整列するのが得意。後者は等間隔のグリッドで見通しが良い反面、非均一サイズは苦手です。

レイアウト選択の目安(比較表)

レイアウト得意な配置非均一サイズ主な用途
LinedFlowLayout横方向に流れるタイル、行ベースの整列◎(本記事の方式と相性良)カード一覧、ギャラリー、ニュース面
UniformGridLayout規則正しい等間隔グリッド△(疑似スパンなら可)写真サムネ、ダッシュボード
StackLayout縦/横 1 列のリスト○(幅は親次第)チャット、設定リスト

完全版サンプル(まとめて貼れる)

以下をそのままコピペすれば、インデックス 2 と 6 が幅 2 倍で表示される最小アプリが動きます。

MainPage.xaml

<Page
    x:Class="SampleApp.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="using:Microsoft.UI.Xaml.Controls"
    xmlns:local="using:SampleApp">









```
  &lt;controls:ItemsRepeater.Layout&gt;
    &lt;controls:LinedFlowLayout /&gt;
  &lt;/controls:ItemsRepeater.Layout&gt;

  &lt;controls:ItemsRepeater.ItemTemplate&gt;
    &lt;DataTemplate x:DataType="local:TileItem"&gt;
      &lt;Border x:Name="ItemBorder"
              Style="{StaticResource TileNormal}"
              Margin="4"
              Background="{Binding BaseColor}"&gt;
        &lt;StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center"&gt;
          &lt;TextBlock Text="{x:Bind Title}"
                     FontSize="20"
                     FontWeight="SemiBold"
                     HorizontalAlignment="Center"/&gt;
          &lt;TextBlock Text="{x:Bind Description}"
                     Opacity="0.7"
                     FontSize="12"
                     HorizontalAlignment="Center"/&gt;
        &lt;/StackPanel&gt;
      &lt;/Border&gt;
    &lt;/DataTemplate&gt;
  &lt;/controls:ItemsRepeater.ItemTemplate&gt;
&lt;/controls:ItemsRepeater&gt;
```


 

MainPage.xaml.cs

using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace SampleApp;

public sealed partial class MainPage : Page
{
public ObservableCollection Items { get; } = new();

```
private readonly HashSet&lt;int&gt; _wideIndices = new() { 2, 6 };

public MainPage()
{
    this.InitializeComponent();

    for (int i = 0; i &lt; 10; i++)
    {
        Items.Add(new TileItem
        {
            Title = $"Item {i:D2}",
            Description = (i == 2 || i == 6) ? "2x Width" : "Normal",
            BaseColor = new SolidColorBrush(Colors.LightGray)
        });
    }
}

private void ir1_ElementPrepared(ItemsRepeater sender,
                                 ItemsRepeaterElementPreparedEventArgs args)
{
    int index = sender.GetElementIndex(args.Element);

    if (args.Element is FrameworkElement fe
        &amp;&amp; fe.FindName("ItemBorder") is Border border)
    {
        bool isWide = _wideIndices.Contains(index);

        // スタイル切替(見通しが良い)
        border.Style = (Style)Resources[isWide ? "TileWide" : "TileNormal"];

        // デバッグ用の色分け(任意)
        border.Background = new SolidColorBrush(
            isWide ? Colors.IndianRed : Colors.SteelBlue);
    }
}
```

}

public class TileItem
{
public string Title { get; set; } = "";
public string Description { get; set; } = "";
public Brush BaseColor { get; set; } = new SolidColorBrush(Colors.LightGray);
} 

変更に強い設計のポイント

  • ルールの局所化:ShouldBeWide(index) のように関数に隔離。仕様変更に強い。
  • データの真理化:UI ロジックではなくモデルの IsWide に寄せると、並べ替え・検索と相性が良い。
  • 視覚と意味の分離:色はデバッグ専用。見た目はスタイル、意味はデータで表現。

運用チートシート

目的やることメモ
特定のタイルを 2 倍ElementPrepared で index 判定 → Width セット「毎回セット」を厳守
縦横どちらも 2 倍Height も同所で設定レイアウトは自動で折り返し
ルールを動的切替フラグ更新 → InvalidateMeasure()サイズクラスと連動させると良い
読みやすいコードスタイル 2 種+切替見通しと保守性が向上

まとめ

  • やることはシンプル:ElementPrepared でインデックスを判定し、対象だけ Width(必要なら Height)を変更。
  • 仮想化に注意:サイズは毎回設定。これで再利用時の崩れを未然に防止。
  • 拡張は容易:モデルの IsWide、周期ルール、スタイル切替、レスポンシブ対応などに素直に広げられる。
  • もっと自由度が必要なら:CommunityToolkit の活用や独自 VirtualizingLayout を検討。

以上で、WinUI 3 × ItemsRepeater × LinedFlowLayout による非均一タイル整列の実装は完成です。まずは最小構成で体験し、プロダクト要件に合わせて「ルール」「スタイル」「データ駆動」の 3 点を段階的に強化していきましょう。

この記事を書いた人

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

コメント

コメントする

目次