2012年3月19日月曜日

wpf : ListBox.ItemsPanelにWrapPanelを設定して幅を調整

ListBoxのItemsPanelにテンプレートを設定すれば、項目のレイアウトを大きく変えることができます。 ItemsPanelにはPanel系のコンテナを指定することが可能です。 当然、WrapPanelも使えます。

本来、WrapPanelは子コントロールを左から右へ順に配置し、ボックスの端で折り返します。 (javaでいうところのFlowLayoutですね。) しかし、ListBox.ItemsPanelにWrapPanelを使うとちょっと表示がおかしくなってしまいます。 スクロールバーが表示されて折り返しが効かなくなるのです。

どうやらListBoxは内部にScrollViewerを持っていて、その下にItemsPanelを配置するようですね。 WrapPanelのWidthに固定値を設定すればスクロールバーの表示は回避できます。 ですがそれだとレイアウトが制限されてしまいます。 自由にレイアウトするには、ListBox内のScrollViewerの幅にあわせてテンプレートのWrapPanelの幅が変わるような仕組みが必要です。

バインディングでWrapPanelの幅とScrollViewerの幅を連携させることができれば1番楽なんでしょうけれど、その方法は見つかりませんでした。 というわけで当面の回避策としてListBoxのSizeChangedイベントで幅を連携させる方法を取ってみました。

簡単なサンプルコードを書くと、こんな感じ。 まずはMainWindow.xamlは、

<Window x:Class="ListBoxItemsPanelTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="300" Width="300">
    <ListBox
            Name="listBox"
            HorizontalAlignment="Stretch"
            VerticalAlignment="Stretch"
            SizeChanged="OnListBoxSizeChanged"
    >
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel/>
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Border BorderThickness="1" BorderBrush="Gray">
                    <TextBlock Text="{Binding}"/>
                </Border>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Window>

MainWindow.xaml.csは、

using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace ListBoxItemsPanelTest
{
    public partial class MainWindow : Window
    {
        public ObservableCollection<string> Items { get; set; }

        public MainWindow()
        {
            InitializeComponent();

            Items = new ObservableCollection<string>();
            for (int i = 0; i < 40; i++)
                Items.Add(i % 5 == 0 ? "あいうえお" : "" + i);
            listBox.ItemsSource = Items;
        }

        private void OnListBoxSizeChanged(object sender, SizeChangedEventArgs e)
        {
            ScrollViewer itemsViewer = (ScrollViewer)FindControl(listBox, typeof(ScrollViewer));
            WrapPanel itemsPanel = (WrapPanel)FindControl(listBox, typeof(WrapPanel));
            itemsPanel.Width = itemsViewer.ActualWidth;
        }

        // 最初に見つかったコントロールを返す
        private DependencyObject FindControl(DependencyObject obj, Type controlType)
        {
            if (obj == null)
                return null;
            if (obj.GetType() == controlType)
                return obj;

            int childrenCount = VisualTreeHelper.GetChildrenCount(obj);
            for (int i = 0; i < childrenCount; i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(obj, i);
                DependencyObject descendant = FindControl(child, controlType);
                if (descendant != null && descendant.GetType() == controlType)
                {
                    return descendant;
                }
            }

            return null;
        }
    }
}

FindControlメソッドは前の投稿の使いまわしです。 ちょくちょく使いそうなメソッドなのでユーティリティクラスのstaticメソッドとかにしといた方がいいのかもしれませんね。