WPF の GridSplitter を使う
Windows Presentation Foundation (WPF) の GridSplitter を使う機会があったので、使い方について調べた内容をまとめます。
GridSplitter はグリッドの行の高さや列の幅を変更可能にするコントロールです。何となく簡単そうな印象があったのですが、実際に使ってみると意外に難しく、苦労しました。GridSplitter に関する説明やソースコードは以下にあります。
- GridSplitter Class (System.Windows.Controls) | Microsoft Docs
- GridSplitter に関する「方法」トピック | Microsoft Docs
- Reference Source - GridSplitter.cs
GridSplitter の配置方法には、グリッド内のコンテンツとセルを共有して重ねる方式と、GridSplitter 専用のセルに配置する方式の二通りがあります。以下に示すコードは、コンテンツに重ねて GridSplitter を配置する例です。この例では、グリッド内の二つのセルに LightBlue と LightPink の長方形を配置し、LightBlue の長方形に重ねて GridSplitter を配置しています。
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Rectangle Grid.Column="0" Fill="LightBlue"/> <GridSplitter Grid.Column="0" VerticalAlignment="Stretch" HorizontalAlignment="Right" Width="4" Background="Black"/> <Rectangle Grid.Column="1" Fill="LightPink"/> </Grid>
次のコードは、GridSplitter 専用のセルに配置する例です。グリッドに三つのカラムを用意し、左から順に LighBlue の長方形、GridSplitter, LightPink の長方形を配置します。
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Rectangle Grid.Column="0" Fill="LightBlue"/> <GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Center" Width="4" Background="Black"/> <Rectangle Grid.Column="2" Fill="LightPink"/> </Grid>
二つの方式で、GridSplitter の HorizontalAlignment の指定が異なっていることに注意してください。セルを共有する場合は Right または Left を指定するのに対して、GridSplitter 専用のセルに配置する場合は Center を指定します。GridSplitter は VerticalAlignment と HorizontalAlignment の指定によって、縦横どちらの方向に動くか (ResizeDirection)、左右または自分自身のどのセルをリサイズするか (ResizeBehavior) を自動的に決定します*1。VerticalAlignment と HorizontalAlignment を正しく指定しないと、意図したとおりの動作にならない可能性があります。
私が GridSplitter を使っていて困ったのは、グリッドの列の幅 (または行の高さ) を固定値や Auto にするとグリッドの領域を超えて GridSplitter を動かせてしまうことです。次のコードは、最初に示したコードから先頭カラムの Width を 200 に変えたものです。Width = "*" の場合には GridSplitter はグリッドの右端で止まりますが、このコードでは GridSplitter を右側にどこまでも動かせます。
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="200"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Rectangle Grid.Column="0" Fill="LightBlue"/> <GridSplitter Grid.Column="0" VerticalAlignment="Stretch" HorizontalAlignment="Right" Width="4" Background="Black"/> <Rectangle Grid.Column="1" Fill="LightPink"/> </Grid>
セルを共有する方式の場合には、次のように MaxWidth をグリッドの ActualWidth にバインドすることで解決します。
<Grid Name="Grid"> <Grid.ColumnDefinitions> <ColumnDefinition Width="200" MaxWidth="{Binding ElementName=Grid, Path=ActualWidth}"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Rectangle Grid.Column="0" Fill="LightBlue"/> <GridSplitter Grid.Column="0" VerticalAlignment="Stretch" HorizontalAlignment="Right" Width="4" Background="Black"/> <Rectangle Grid.Column="1" Fill="LightPink"/> </Grid>
一方、専用のセルに配置する方式では上記の解決方法を使えません。たとえば、次のようにコードを書いても GridSplitter はグリッドの右端で止まりません。先頭カラムの MaxWidth をグリッドの ActualWidth にバインドしても、第二カラムに存在する GridSplitter の幅の分だけグリッドの ActualWidth 自体が大きくなってしまうためです。問題を解決するには、グリッドの ActualWidth から GridSplitter の ActualWidth を引いた値にバインドする必要があります。
<Grid Name="Grid"> <Grid.ColumnDefinitions> <ColumnDefinition Width="200" MaxWidth="{Binding ElementName=Grid, Path=ActualWidth}"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Rectangle Grid.Column="0" Fill="LightBlue"/> <GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Center" Width="4" Background="Black"/> <Rectangle Grid.Column="2" Fill="LightPink"/> </Grid>
これは二つのバインディングソースを利用するため、IMultiValueConverter を実装するコンバータを作成します。Convert メソッドに必要な計算を実装します。今回の用途では ConvertBack メソッドは使いません。
internal class MaxWidthConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values.Length == 2 && values[0] is double gridActualWidth && values[1] is double splitterActualWidth) { return gridActualWidth - splitterActualWidth; } return DependencyProperty.UnsetValue; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotSupportedException(); } }
作成したコンバータを利用して、次のように問題を解決できます*2。
<Grid Name="Grid"> <Grid.Resources> <local:MaxWidthConverter x:Key="MaxWidthConverter"/> </Grid.Resources> <Grid.ColumnDefinitions> <ColumnDefinition Width="200"> <ColumnDefinition.MaxWidth> <MultiBinding Converter="{StaticResource MaxWidthConverter}"> <Binding ElementName="Grid" Path="ActualWidth"/> <Binding ElementName="Splitter" Path="ActualWidth"/> </MultiBinding> </ColumnDefinition.MaxWidth> </ColumnDefinition> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Rectangle Grid.Column="0" Fill="LightBlue"/> <GridSplitter Name="Splitter" Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Center" Width="4" Background="Black"/> <Rectangle Grid.Column="2" Fill="LightPink"/> </Grid>