子要素をレンガのようにタイル状に敷き詰める

 
WPF や Silverlight でもカスタム コントロールを作成することができます。
今回は例として、子要素をレンガのようにタイル状に敷き詰めるためのレイアウト パネルを
カスタム コントロールとして作成します。
 
レイアウトをカスタマイズするためには、「2 段階レイアウト モデル」を理解しておく必要があります。
レイアウトの内部処理は、測定 (Measure) と配置 (Arrange) の 2 段階で実行されます。
  • 測定段階: 各要素が必要とするサイズを測定します。
  • 配置段階: 測定結果をもとに各要素が実際に配置されます。
 
具体的には、MeasureOverride メソッドおよび ArrangeOverride メソッドをオーバーライドして、
子要素を測定および配置するためのコードを記述します。
今回作成したいコントロールはレイアウト パネルなので、Panel を継承してクラスを作成します。
(UniformGrid に似た構造になります。)
 
BrickTile.cs

public class BrickTile : Panel
{
   
private int rows;

    public static readonly DependencyProperty ColumnsProperty
       
= DependencyProperty.Register("Columns", typeof(int), typeof(BrickTile),
           
new FrameworkPropertyMetadata(2, FrameworkPropertyMetadataOptions.AffectsMeasure),
            o
=> (int)o >= 2);

    public int Columns
    {
       
get { return (int)base.GetValue(ColumnsProperty); }
       
set { base.SetValue(ColumnsProperty, value); }
    }

    // 子要素のレイアウトに必要なサイズを測定します。
    protected override Size MeasureOverride(Size availableSize)
    {
        ComputeRows();
       
Size childAvailableSize = GetChildSize(availableSize);

        var children = InternalChildren.Cast<UIElement>().ToArray();
       
foreach (var child in children)
        {
            child
.Measure(childAvailableSize);
        }
       
double maxWidth = children.Length == 0 ? 0 : children.Max(e => e.DesiredSize.Width);
       
double maxHeight = children.Length == 0 ? 0 : children.Max(e => e.DesiredSize.Height);

        return new Size(Columns * maxWidth, rows * maxHeight);
    }

    // 子要素を配置します。
    protected override Size ArrangeOverride(Size finalSize)
    {
       
Size childFinalSize = GetChildSize(finalSize);

        int actualIndex = 0;
       
foreach (UIElement child in InternalChildren)
        {
           
var position = GetPosition(actualIndex);
           
Point childPoint = new Point
            {
                X
= (position.Item2 + (position.Item1 % 2 == 0 ? 0 : 0.5)) * childFinalSize.Width,
                Y
= position.Item1 * childFinalSize.Height,
            };
           
Rect childRect = new Rect(childPoint, childFinalSize);

            child.Arrange(childRect);

            if (child.Visibility != Visibility.Collapsed)
            {
                actualIndex
++;
            }
        }

        return finalSize;
    }

    // 行数を算出します。
    private void ComputeRows()
    {
       
int actualChildrenCount = InternalChildren.Cast<UIElement>()
           
.Count(e => e.Visibility != Visibility.Collapsed);

        if (actualChildrenCount == 0)
        {
            rows
= 0;
        }
       
else
        {
           
var lastChildPosition = GetPosition(actualChildrenCount  1);
            rows
= lastChildPosition.Item1 + 1;
        }
    }

    // 指定されたインデックスの子要素の (行番号, 列番号) を求めます。
    private Tuple<int, int> GetPosition(int index)
    {
       
// 2 行分を 1 グループとして、 (グループ番号, グループ内の位置) を求めます。
        int groupChildrenCount = 2 * Columns  1;
       
var groupedPosition = Tuple.Create(index / groupChildrenCount, index % groupChildrenCount);

        return Tuple.Create(2 * groupedPosition.Item1 + groupedPosition.Item2 / Columns,
            groupedPosition
.Item2 % Columns);
    }

    // 子要素のサイズを求めます。
    private Size GetChildSize(Size parentSize)
    {
       
return new Size(parentSize.Width / Columns, parentSize.Height / (rows == 0 ? 1 : rows));
    }
}


 
これで、子要素がレンガのようにタイル状に並ぶようになります。
 
Button (Expression Blend)
 
せっかくなので、これを使って九州の県を選択させるための UI を考えてみます。
まず、ListBox に ListBoxItem を追加して県名を設定します。
 
九州 (スタイル設定なし)
 
この何の変哲もない ListBox も、
次のように ItemsPanel プロパティを BrickTile に変更するだけで視認性の高い UI になります。
この XAML は Expression Blend で生成できます。

<Window x:Class="BrickTileTest.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml&quot;
   xmlns:s="http://schemas.saka-pon.net/sample1&quot;
   Title="MainWindow" Height="300" Width="300">
    <Window.Resources>
        <Style TargetType="{x:Type ListBox}">
            <Setter Property="FontSize" Value="16"/>
            <Setter Property="ItemsPanel" Value="{DynamicResource ItemsPanelTemplate1}"/>
        </Style>
        <ItemsPanelTemplate x:Key="ItemsPanelTemplate1">
            <s:BrickTile IsItemsHost="True" Columns="3"/>
        </ItemsPanelTemplate>
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="Background" Value="#FFC0F0B0"/>
            <Setter Property="BorderBrush" Value="#FF106020"/>
            <Setter Property="BorderThickness" Value="2"/>
            <Setter Property="Margin" Value="2"/>
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
            <Setter Property="VerticalContentAlignment" Value="Stretch"/>
            <Setter Property="ContentTemplate" Value="{DynamicResource DataTemplate1}"/>
        </Style>
        <DataTemplate x:Key="DataTemplate1">
            <TextBlock Text="{Binding Mode=OneWay}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </DataTemplate>
    </Window.Resources>
    <ListBox>
        <ListBoxItem Content="長崎"/>
        <ListBoxItem Content="佐賀"/>
        <ListBoxItem Content="福岡"/>
        <ListBoxItem Content="熊本"/>
        <ListBoxItem Content="大分"/>
        <ListBoxItem Content="鹿児島"/>
        <ListBoxItem Content="宮崎"/>
    </ListBox>
</
Window>

 
九州 (スタイル設定あり)
 
また、この UI 上では矢印キーや PageUp / PageDown キーでフォーカスを移動できます。
[長崎] が選択されている状態で [↓] キーを押すと [熊本] に移動し、PageDown キーを押すと [鹿児島] に移動します。
 
他にもいろいろと応用できそうです。
 
東京 23 区
 
関八州
 
バージョン情報
.NET Framework 4
Silverlight 4
 
参照

コメントを残す