午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

WPF、自動サイズ調整するCanvas、子要素のサイズや位置の変更で更新

動作確認、通常のCanvasと比較

動作確認
上が今回のCanvas、下が普通のCanvas

  • ExCanvas 背景色

    • TextBlock.Text="子要素11"
    • TextBlock.Text="子要素12"
  • Canvas 背景色

    • TextBlock.Text="子要素21"
    • TextBlock.Text="子要素22"

比較
どちらのCanvasにも背景色はGoldを指定している
通常のCanvasは子要素を配置してもサイズを指定しない限りはサイズが0なので、背景色は表示されていない
ExCanvasは子要素が収まるサイズに更新されるので背景色が表示されている
子要素11の左上がぴったりじゃなくて空いているのは、子要素11の配置座標が(20,10)に指定されているからなので、これで正しい

子要素のサイズと位置を変更したとき

子要素のサイズと位置変更
フォントサイズなどでサイズが変更されたときや、表示位置の変更で
ExCanvasはサイズが更新される
通常のCanvasのサイズは0のまま

自身のサイズを指定したとき

自身のサイズを指定したとき
どちらのCanvasも指定したサイズで表示される

作成環境

コード

github.com

xamlMainWindow.xaml

<Window x:Class="_20221222_ExCanvas.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20221222_ExCanvas"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="500">
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition Width="200"/>
    </Grid.ColumnDefinitions>

    <Canvas>
      <Canvas.Resources>
        <Style TargetType="TextBlock">
          <Setter Property="Background" Value="DodgerBlue"/>
          <Setter Property="Foreground" Value="White"/>
        </Style>
      </Canvas.Resources>
      <local:ExCanvas x:Name="MyExCanvas" Background="Gold">
        <TextBlock Text="子要素11" FontSize="30" Canvas.Left="20" Canvas.Top="10"/>
        <TextBlock x:Name="My子要素12" Text="子要素12" FontSize="30" Canvas.Left="50" Canvas.Top="80"/>
      </local:ExCanvas>
      <Canvas x:Name="MyCanvas" Canvas.Top="200" Background="Gold">
        <TextBlock Text="子要素21" FontSize="30" Canvas.Left="20" Canvas.Top="10"/>
        <TextBlock x:Name="My子要素22" Text="子要素22" FontSize="30" Canvas.Left="50" Canvas.Top="80"/>
      </Canvas>
    </Canvas>

    <StackPanel Grid.Column="1">
      <StackPanel.Resources>
        <Style TargetType="Slider">
          <Setter Property="IsSnapToTickEnabled" Value="True"/>
          <Setter Property="TickFrequency" Value="10"/>
          <Setter Property="AutoToolTipPlacement" Value="TopLeft"/>
          <Setter Property="TickPlacement" Value="TopLeft"/>
        </Style>
        <Style TargetType="GroupBox">
          <Setter Property="Margin" Value="4"/>
        </Style>
      </StackPanel.Resources>
      <GroupBox DataContext="{Binding ElementName=MyExCanvas}" Header="{Binding Name}">
        <StackPanel>
          <TextBlock Text="{Binding ActualWidth, StringFormat=ActualWidth \= {0:0.0}}"/>
          <TextBlock Text="{Binding Width, StringFormat=Width \= {0:0.0}}"/>
          <Button x:Name="Button1" Content="Width=200、Height=100に指定" Click="Button1_Click"/>
          <Button x:Name="Button11" Content="Width=NaN、Height=NaNに指定" Click="Button11_Click"/>
          <GroupBox DataContext="{Binding ElementName=My子要素12}" Header="{Binding Name}">
            <StackPanel>
              <Slider Value="{Binding FontSize}" Minimum="10" Maximum="100"/>
              <!--[WPF/XAML]添付プロパティにバインディングする - MithrilWorks
https://mithrilworks.jp/others/program/xaml/dpbinding.html-->
              <Slider Value="{Binding (Canvas.Left)}" Minimum="0" Maximum="200"/>
            </StackPanel>
          </GroupBox>
        </StackPanel>
      </GroupBox>
      <GroupBox DataContext="{Binding ElementName=MyCanvas}" Header="{Binding Name}">
        <StackPanel>
          <TextBlock Text="{Binding ActualWidth, StringFormat=ActualWidth \= {0:0.0}}"/>
          <TextBlock Text="{Binding Width, StringFormat=Width \= {0:0.0}}"/>
          <Button x:Name="Button2" Content="Width=200、Height=100に指定" Click="Button2_Click"/>
          <Button x:Name="Button21" Content="Width=NaN、Height=NaNに指定" Click="Button21_Click"/>
          <GroupBox DataContext="{Binding ElementName=My子要素22}" Header="{Binding Name}">
            <StackPanel>
              <Slider Value="{Binding FontSize}" Minimum="10" Maximum="100"/>
              <Slider Value="{Binding (Canvas.Left)}" Minimum="0" Maximum="200"/>
            </StackPanel>
          </GroupBox>
        </StackPanel>
      </GroupBox>
    </StackPanel>
  </Grid>
</Window>

長いけど、必要なところはExCanvasのところの4行だけで、あとは動作確認のためのもの



csMainWindow.xaml.cs

using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace _20221222_ExCanvas
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Button1_Click(object sender, RoutedEventArgs e)
        {
            MyExCanvas.Width = 200; MyExCanvas.Height = 100;
        }

        private void Button2_Click(object sender, RoutedEventArgs e)
        {
            MyCanvas.Width = 200; MyCanvas.Height = 100;
        }

        private void Button11_Click(object sender, RoutedEventArgs e)
        {
            MyExCanvas.Width = double.NaN; MyExCanvas.Height = double.NaN;
        }

        private void Button21_Click(object sender, RoutedEventArgs e)
        {
            MyCanvas.Width = double.NaN; MyCanvas.Height = double.NaN;
        }

    }
    /// <summary>
    /// オートサイズのCanvas、子要素のサイズや位置の変更時にサイズ更新
    /// </summary>
    public class ExCanvas : Canvas
    {
        protected override Size ArrangeOverride(Size arrangeSize)
        {
            if (double.IsNaN(Width) && double.IsNaN(Height))
            {
                base.ArrangeOverride(arrangeSize);
                Size size = new();
                foreach (var item in Children.OfType<FrameworkElement>())
                {
                    double x = GetLeft(item) + item.ActualWidth;
                    double y = GetTop(item) + item.ActualHeight;
                    if (size.Width < x) size.Width = x;
                    if (size.Height < y) size.Height = y;
                }
                return size;
            }
            else
            {
                return base.ArrangeOverride(arrangeSize);
            }
        }
    }
}

Canvasクラスを継承したクラス

ExCanvas
ArrangeOverrideをOverrideして、そこで子要素がすべて収まるサイズを計算して返している
これで自身のサイズ(ActualWidthとActualHeight)が更新される
サイズが指定されていた場合は、そちらを優先したいので50行目あたりの

if (double.IsNaN(Width) && double.IsNaN(Height))

サイズ未指定ならNaNの値が入っているはずなので、それで判定

子要素のサイズ取得

サイズ計算で子要素の幅サイズをWidthではなく、ActualWidthを使っているのは、Widthの値が常にNaNになっている要素があるから
RectangleなんかはWidthやHeightを指定しないと表示されないけど、TextBlockは文字列さえ指定すれば表示される。こういうサイズ指定しなくても表示される要素のWidthやHeightは常にNaNで、Widthを知りたいときはActualWidth、なのでActualWidthで計算している

サイズプロパティ関係だと、DesiredSizeってのがある。今回は使っていないけどMeasureOverrideで計算するときはActualWidthではなくて、このDesiredSizeを使う必要があるかも、
例えばフォントサイズ変更したときに欲しいのは、変更後のサイズなのにActualWidthだと変更前の値が入っているけど、DesiredSizeでは変更後の値が入っているから

フォントサイズを大きくしたときの変化

MeasureOverrideでのDesiredSizeとActual
MeasureOverrideでのActualWidthはフォントサイズ変更前の値が入っているけど、DesiredSizeでは変更後の値になっている


ArrangeOverrideでのDesiredSizeとActual
ArrangeOverrideではどちらでも変更後の値が取得できる


参照したところ

c# - WPF: Sizing Canvas to contain its children - Stack Overflow stackoverflow.com 今回はここからのコピペ改変
大きく違うところはMeasureOverrideではなくてArrangeOverrideを使っているところ
これは、子要素が移動したときにMeasureOverrideは実行されなかったけど、試しにArrangeOverrideを使ったら、こちらは実行されたから


子要素をレンガのようにタイル状に敷き詰める | Do Design Space
子要素をレンガのようにタイル状に敷き詰めるsakapon.wordpress.com

レイアウト - WPF .NET Framework | Microsoft Learn learn.microsoft.com

[WPF/XAML]添付プロパティにバインディングする - MithrilWorks
mithrilworks.jp



感想日記

ブルースクリーンエラーが3回あった、BAD POOLなんとかが2回に、IRQなんとかが1回だったけど発生したのはいずれもVisual Studioでのデバッグ中だった。以前もそんなことがあって、解決したのはプロジェクト名に長い日本語を使っているのをやめるだったはず!と試したら今のところ収まっている
元のプロジェクト名は
「20221221_子要素のサイズと位置でサイズ変更するCanvas
だった、これが原因なのかなあ
コード自体は今回やコピペ元と同じような感じだから問題ないと思うんだよねえ
昔のチェックディスクは真っ黒画面だったけど、いまはブルースクリーンなのね、初めて見たわ

アプリに戻って、気になるのが動作速度とか計算量、子要素が増えればそれだけ計算量が増えるし、ExCanvasの子要素にExCanvasを入れて行った場合に、一番下の子要素に変化があったときは一番上のExCanvasまでサイズの再計算になる?あとは位置変更でも再計算だから、マウスドラッグ移動させるような子要素の場合は移動中は常にサイズ計算することになる!