添付プロパティを使ってGridやStackPanelだけで表現できないレイアウトを実現する


概要

WPFではStackPanelやGridといったパネルコントロールを使って、望みのレイアウトを表現します。
ほとんどのレイアウトはパネルコントロールの組み合わせで可能ですが、まれに出来ない場合もあります。
そのような場合に添付プロパティを使って、解決する方法を紹介します。

作りたいレイアウト

以下のように長い文字が入るTextBoxと別のコントロール、ここではButtonがいます。

このとき、以下のような動きをしたい、とします。
片方だけならそれぞれGridとStackPanelを使ってできますが、両方を同時に満たすことができません。

  • 長い文字が入ってもButtonが画面内に出ていかない、TextBoxの長さを制限したい
  • 短い文字の時はTextBoxを縮めてButtonを近くに置きたい

添付プロパティ

MaxWidth用のLimitMaxWidthMaxHeight用のLimitMaxHeightの2つのフォーマットを指定できる添付プロパティが定義されています。

class LimitSizeHelper
{
    #region LimitMaxHeight添付プロパティ
    public static double GetLimitMaxHeight(DependencyObject obj) => (double)obj.GetValue(LimitMaxHeightProperty);
    public static void SetLimitMaxHeight(DependencyObject obj, double value) => obj.SetValue(LimitMaxHeightProperty, value);
    public static readonly DependencyProperty LimitMaxHeightProperty =
        DependencyProperty.RegisterAttached("LimitMaxHeight", typeof(double), typeof(LimitSizeHelper),
                new PropertyMetadata(1d, (d, e) => AddLimitMaxSize(d, e, false)));
    #endregion

    #region LimitMaxWidth添付プロパティ
    public static double GetLimitMaxWidth(DependencyObject obj) => (double)obj.GetValue(LimitMaxWidthProperty);
    public static void SetLimitMaxWidth(DependencyObject obj, double value) => obj.SetValue(LimitMaxWidthProperty, value);
    public static readonly DependencyProperty LimitMaxWidthProperty =
        DependencyProperty.RegisterAttached("LimitMaxWidth", typeof(double), typeof(LimitSizeHelper),
                new PropertyMetadata(-1d, (d, e) => AddLimitMaxSize(d, e, true)));
    #endregion


    private static void AddLimitMaxSize(DependencyObject d, DependencyPropertyChangedEventArgs e, bool isWidth)
    {
        if (d is FrameworkElement targetObj
            && targetObj.Parent is Panel panel
            && e.NewValue is double newValue && newValue > 0)
        {
            panel.SizeChanged += (o, _) =>
                Parent_SizeChanged(targetObj, panel, isWidth, newValue);
        }
    }

    private static void Parent_SizeChanged(FrameworkElement targetObj, Panel panel, bool isWidth, double ratio)
    {
        var otherSumSize = panel.Children
            .Cast<FrameworkElement>()
            .Where(x => x != targetObj)
            .Sum(x => isWidth ? x.ActualWidth : x.ActualHeight);

        double maxSize = ((isWidth ? panel.ActualWidth : panel.ActualHeight) - otherSumSize) * ratio;

        if (isWidth)
            targetObj.MaxWidth = maxSize;
        else
            targetObj.MaxHeight = maxSize;
    }
}

使用方法

XAML上で親コントロールに対する比率を指定します。
local:LimitSizeHelper.LimitMaxWidth="0.99"

コードビハインドは使用していません。

MainWindow.xaml
<Window
   x:Class="LimitSizeControl.MainWindow"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:local="clr-namespace:LimitSizeControl"
   Width="300" Height="300">
   <!--  Width制限  -->
   <StackPanel
      Margin="10"
      Background="LightSlateGray"
      Orientation="Horizontal">
      <TextBox
         local:LimitSizeHelper.LimitMaxWidth="0.99"
         Text="LLLLLLLLLLLLLLLOOOOOOOOOOONNNNNNNNNGGGGGG"
         TextWrapping="Wrap" />
      <Button Content="BUTTON" />
   </StackPanel>
   <!--  Height制限  -->
   <!--<StackPanel Margin="10" Background="LightSlateGray">
      <TextBox
         Width="50"
         local:LimitSizeHelper.LimitMaxHeight="0.99"
         Text="LLLLLLLLLLLLLLLLLLLLLLOOOOOOOOOOOOOOOOOOOOOOOOOOOONNNNNNNNNNNNNNNNNNNNNNNNGHHHHHHHHHHHHHHHHGGGGGGGGG"
         TextWrapping="Wrap" />
      <Button Content="BUTTON" />
   </StackPanel>-->
</Window>

環境

VisualStudio 2019 Version 16.8.4
.NET 5
C#9

参考

https://qiita.com/YSRKEN/items/686068a359866f21f956
https://www.atmarkit.co.jp/ait/articles/1011/30/news116_2.html