こんにちは、働くC#プログラマーのさんさめです。
WPFで領域をドラッグで広げられるようにするには
Gridと、GridSplitterを使います。
ただこれ、コンテンツを折り畳める
Expanderと併用すると、
なんとも残念なことになります。
というのも、
Expanderを閉じても
広げた領域がそのまま残ってしまうのです。
こういう時にやりたいのは、
「開いている時だけ領域サイズが調整できて、
閉じたらヘッダー部分だけ残して縮むこと」
です。
というわけで本記事では、
この挙動を改善するために必要な実装を解説し、
最後にxamlにちょっと書き足すだけで、
良い感じの挙動になる添付プロパティを紹介します。
Expanderの開閉に合わせてGridの幅を再設定すると良い
改善の話に進む前に、
どうして領域が残る挙動になってしまうのか、
その原因を確認します。
GridSplitterは、
Gridに配置した
ColumnDefinitionのWidth ( / RowDefinitionのHeight)
を変更するコントロールです。
つまり、そこに置いたコントロール
(この場合はExpander)の高さや幅がいくつか、
など知ったこっちゃないのです。
これを改善するためには、
Expanderの開閉に合わせて、
Expanderが配置されている
ColumnDefinitionのWidth( / RowDefinitionのHeight)
をAutoに戻す必要があります。
コードビハインドに書くとしたら、
以下のようなコードになります。
private GridLength lastRowHeight = GridLength.Auto;
private void MyExpander_Expanded(object sender, RoutedEventArgs e)
{
// 前に閉じたときの高さ値が残っていたらそれを復元
MyRowDefinition.Height = lastRowHeight;
// GridSplitterを可視化
MyGridSplitter.Visibility = Visibility.Visible;
}
private void MyExpander_Collapsed(object sender, RoutedEventArgs e)
{
// 閉じる前の高さを保存し
// 高さをAutoに戻す
lastRowHeight = MyRowDefinition.Height;
MyRowDefinition.Height = GridLength.Auto;
// GridSplitterを非表示に
MyGridSplitter.Visibility = Visibility.Collapsed;
}
Expanderが閉じているときは、
GridSplitterも非表示にして、
ドラッグできないようにしておくと、
気が利いていますね。
念のため、xaml側も載せておきます。
<Window x:Class="GridSnapExpander.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="250" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto" x:Name="MyRowDefinition"/>
</Grid.RowDefinitions>
<TextBox Text="メイン画面のつもり"/>
<GridSplitter Height="4"
Background="Gray"
x:Name="MyGridSplitter"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Visibility="Collapsed"/>
<Expander Header="サブ画面"
x:Name="MyExpander"
Expanded="MyExpander_Expanded"
Collapsed="MyExpander_Collapsed"
Grid.Row="1">
<TextBox Text="サブ画面のつもり"/>
</Expander>
</Grid>
</Window>
実行結果を見てみましょう。
Expanderを閉じたときに、
領域が引っ込むようになりました。
また、Expanderが展開しているときだけ、
GridSplitterが現れています。
相性を改善する添付プロパティを作ってみた
しかし、
この挙動を実現させるために、
上記のようなコードを毎回書くのは
はっかり言って面倒です。
というわけで、
上記のような振る舞いをExpanderにさせるための
添付プロパティを作ってみました。
コードは以下の通りです。
…と、言いつつ長いので閉じておきます。
興味のある方は実装を眺めてみてください。
WPFのプロジェクトなら、
全文コピペするだけでビルド通ります。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace GridSnapExpander
{
public static class ExpanderAttachment
{
public enum GridSnapMode
{
None,
Auto,
Explicit,
}
private static BooleanToVisibilityConverter BoolToVisible { get; }
= new BooleanToVisibilityConverter();
public static GridSnapMode GetGridSnap(Expander obj)
{
return (GridSnapMode)obj.GetValue(GridSnapProperty);
}
public static void SetGridSnap(Expander obj, GridSnapMode value)
{
obj.SetValue(GridSnapProperty, value);
}
// Using a DependencyProperty as the backing store for GridSnap. This enables animation, styling, binding, etc...
public static readonly DependencyProperty GridSnapProperty =
DependencyProperty.RegisterAttached("GridSnap",
typeof(GridSnapMode),
typeof(ExpanderAttachment),
new PropertyMetadata(GridSnapMode.None,
(d, e) =>
{
if (!(e.NewValue is GridSnapMode mode)) { return; }
if (!(d is Expander expander)) { return; }
// コントロールを取得したいため少し待ってから処理開始
expander.Dispatcher.BeginInvoke((Action)(async () =>
{
await Task.Delay(500);
// 横開きモードか縦開きモードかで分岐
switch (expander.ExpandDirection)
{
case ExpandDirection.Down:
case ExpandDirection.Up:
expander.AttachMode_Vertical(mode);
break;
case ExpandDirection.Left:
case ExpandDirection.Right:
expander.AttachMode_Horizontal(mode);
break;
default:
break;
}
}));
}));
public static GridSplitter GetTargetGridSplitter(DependencyObject obj)
{
return (GridSplitter)obj.GetValue(TargetGridSplitterProperty);
}
public static void SetTargetGridSplitter(DependencyObject obj, GridSplitter value)
{
obj.SetValue(TargetGridSplitterProperty, value);
}
// Using a DependencyProperty as the backing store for TargetGridSplitter. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TargetGridSplitterProperty =
DependencyProperty.RegisterAttached("TargetGridSplitter", typeof(GridSplitter), typeof(ExpanderAttachment), new PropertyMetadata(null));
private static void AttachMode_Vertical(this Expander expander, GridSnapMode mode)
{
var targetGrid = expander.FindAncestor<Grid>();
if (targetGrid == null) { return; }
expander.Expanded -= Expanded_Vertical;
expander.Collapsed -= Collapsed_Vertical;
if (mode == GridSnapMode.None)
{
return;
}
// ExpanderのGrid.Rowから
// 高さを変更したいRowDefinitionを取得
var gridRow = Grid.GetRow(expander);
var targetRowDefinition = targetGrid.RowDefinitions[gridRow];
SetTargetRowDefinition(expander, targetRowDefinition);
SetLastGridLength(expander, targetRowDefinition.Height);
// GridSplitterを取得
var gridSplitter = mode == GridSnapMode.Auto
? targetGrid.FindDescendant<GridSplitter>()
: GetTargetGridSplitter(expander);
if (mode == GridSnapMode.Explicit && gridSplitter == null)
{
throw new ArgumentException("Mode 'Explicit' requires 'TargetGridSplitter'", "TargetGridSplitter");
}
// 表示切替はBindingで行う
gridSplitter.SetBinding(UIElement.VisibilityProperty, new Binding(nameof(Expander.IsExpanded))
{
Mode = BindingMode.OneWay,
Converter = BoolToVisible,
Source = expander,
});
expander.Expanded += Expanded_Vertical;
expander.Collapsed += Collapsed_Vertical;
}
private static void Collapsed_Vertical(object sender, RoutedEventArgs e)
{
if (!(sender is Expander expander)) { return; }
// 閉じる前の高さを保存し
// 高さをAutoに戻す
var targetRowDefinition = GetTargetRowDefinition(expander);
SetLastGridLength(expander, targetRowDefinition.Height);
targetRowDefinition.Height = GridLength.Auto;
}
private static void Expanded_Vertical(object sender, RoutedEventArgs e)
{
if (!(sender is Expander expander)) { return; }
// 前に閉じたときの高さ値が残っていたらそれを復元
GetTargetRowDefinition(expander).Height = GetLastGridLength(expander);
}
private static void AttachMode_Horizontal(this Expander expander, GridSnapMode mode)
{
var targetGrid = expander.FindAncestor<Grid>();
if (targetGrid == null) { return; }
expander.Expanded -= Expanded_Horizontal;
expander.Collapsed -= Collapsed_Horizontal;
if (mode == GridSnapMode.None)
{
return;
}
// ExpanderのGrid.Columnから
// 幅を変更したいColumnDefinitionを取得
var gridColumn = Grid.GetColumn(expander);
var targetColumnDefinition = targetGrid.ColumnDefinitions[gridColumn];
SetTargetColumnDefinition(expander, targetColumnDefinition);
SetLastGridLength(expander, targetColumnDefinition.Width);
// GridSplitterを取得
var gridSplitter = mode == GridSnapMode.Auto
? targetGrid.FindDescendant<GridSplitter>()
: GetTargetGridSplitter(expander);
if (mode == GridSnapMode.Explicit && gridSplitter == null)
{
throw new ArgumentException("Mode 'Explicit' requires 'TargetGridSplitter'", "TargetGridSplitter");
}
// 表示切替はBindingで行う
gridSplitter.SetBinding(UIElement.VisibilityProperty, new Binding(nameof(Expander.IsExpanded))
{
Mode = BindingMode.OneWay,
Converter = BoolToVisible,
Source = expander,
});
expander.Expanded += Expanded_Horizontal;
expander.Collapsed += Collapsed_Horizontal;
}
private static void Collapsed_Horizontal(object sender, RoutedEventArgs e)
{
if (!(sender is Expander expander)) { return; }
// 閉じる前の幅を保存し
// 幅をAutoに戻す
var targetColumnDefinition = GetTargetColumnDefinition(expander);
SetLastGridLength(expander, targetColumnDefinition.Width);
targetColumnDefinition.Width = GridLength.Auto;
}
private static void Expanded_Horizontal(object sender, RoutedEventArgs e)
{
if (!(sender is Expander expander)) { return; }
// 前に閉じたときの高さ値が残っていたらそれを復元
GetTargetColumnDefinition(expander).Width = GetLastGridLength(expander);
}
private static GridLength GetLastGridLength(Expander obj)
{
return (GridLength)obj.GetValue(LastGridLengthProperty);
}
private static void SetLastGridLength(Expander obj, GridLength value)
{
obj.SetValue(LastGridLengthProperty, value);
}
// Using a DependencyProperty as the backing store for LastGridLength. This enables animation, styling, binding, etc...
private static readonly DependencyProperty LastGridLengthProperty =
DependencyProperty.RegisterAttached("LastGridLength", typeof(GridLength), typeof(ExpanderAttachment), new PropertyMetadata(GridLength.Auto));
private static RowDefinition GetTargetRowDefinition(Expander obj)
{
return (RowDefinition)obj.GetValue(TargetRowDefinitionProperty);
}
private static void SetTargetRowDefinition(Expander obj, RowDefinition value)
{
obj.SetValue(TargetRowDefinitionProperty, value);
}
// Using a DependencyProperty as the backing store for TargetRowDefinition. This enables animation, styling, binding, etc...
private static readonly DependencyProperty TargetRowDefinitionProperty =
DependencyProperty.RegisterAttached("TargetRowDefinition", typeof(RowDefinition), typeof(ExpanderAttachment), new PropertyMetadata(null));
private static ColumnDefinition GetTargetColumnDefinition(Expander obj)
{
return (ColumnDefinition)obj.GetValue(TargetColumnDefinitionProperty);
}
private static void SetTargetColumnDefinition(Expander obj, ColumnDefinition value)
{
obj.SetValue(TargetColumnDefinitionProperty, value);
}
// Using a DependencyProperty as the backing store for TargetColumnDefinition. This enables animation, styling, binding, etc...
private static readonly DependencyProperty TargetColumnDefinitionProperty =
DependencyProperty.RegisterAttached("TargetColumnDefinition", typeof(ColumnDefinition), typeof(ExpanderAttachment), new PropertyMetadata(null));
public static T FindAncestor<T>(this DependencyObject depObj)
where T : DependencyObject
{
while (depObj != null)
{
if (depObj is T target)
{
return target;
}
depObj = VisualTreeHelper.GetParent(depObj);
}
return null;
}
public static T FindDescendant<T>(this DependencyObject depObj)
where T : DependencyObject
{
if (depObj == null) { return null; }
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? FindDescendant<T>(child);
if (result != null) { return result; }
}
return null;
}
}
}
xamlで使うときは、
以下のように設定します。
簡単ですね。
コードビハインドや各所のx:Nameが消えて
だいぶすっきりしました。
Autoを設定しておけば、
自身の配置されているRowDefinitionを、
自動的に検知して高さを調整してくれます。
また、GridSplitterの表示も切り替えてくれます。
実行結果を見てみましょう。
ちゃんとExpanderを閉じたときに、
Gridが縮んでいますね。
ExpandDirectionが横向きの時も、
ちゃんと動作しますのでご安心を。
(両対応のせいでコードが長くなってます…)
GridSplitterを複数置きたい場合はExplicitモードを使う
ちなみに、
もしGridSplitterが複数置かれている場合は、
Autoでは表示切替がうまくいかない可能性があります。
その場合は、モードをExplicitに変更して、
TargetGridSplitterを以下のようにBindingで指定します。
これを使えば、
指定したGridSplitterの表示状態が
変更されるようになります。
実行結果は次のようになります。
指定したGridSplitterの表示状態のみが、
Expanderの開閉に連動していますね。
まとめ
まとめです。
- ExpanderとGridSplitterを併用すると、
閉じたときに残念な見た目になる - Expanderの開閉に合わせて、
幅や高さをAutoに再設定すると改善する - xamlの設定1つで相性を改善する
添付プロパティを紹介
最後までお読みいただき、ありがとうございました。
関連記事
添付プロパティの中では、
ビューの祖先や子孫から、
特定のコントロールを取得するコードを使用しています。
それぞれ、取得するコード例を紹介しています。
コメント