[WPF]便利なPanelを作りました

こんばんは。

結構便利で重宝しているPanelを作ったので、共有したいと思います。

https://github.com/dhq-boiler/SimpleGridSample


![SimpleGrid](https://raw.githubusercontent.com/dhq-boiler/SimpleGridSample/develop/WebComponents/2021-11-10.png)

左の赤い枠内がOrientation=Vertical、右の青い枠内がOrientation=Horizontalです。

上記の図のように描画させるには以下のXAMLコードを書くだけです。
SimpleGridはRowsとColumnsとOrientationを設定すれば、あとは縦x横=(Rows, Columns)のグリッドに、Orientationの向きで順番に要素を埋めていってくれます。特にGridクラスのように一々Grid.Columnプロパティや、Grid.Columnプロパティを設定する必要がないので楽です。

```xml
<Window x:Class="SimpleGridSample.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:SimpleGridSample"
        mc:Ignorable="d"
        Title="MainWindow"
        SizeToContent="WidthAndHeight">
    <local:SimpleGrid Rows="1" Columns="2" Orientation="Horizontal">
        <Border BorderBrush="Red" BorderThickness="3">
            <local:SimpleGrid Rows="4" Columns="6" Orientation="Vertical">
                <Label>1</Label>
                <Label>2</Label>
                <Label>3</Label>
                <Label>4</Label>
                <Label>5</Label>
                <Label>6</Label>
                <Label>7</Label>
                <Label>8</Label>
                <Label>9</Label>
                <Label>10</Label>
                <Label>11</Label>
                <Label>12</Label>
                <Label>13</Label>
                <Label>14</Label>
                <Label>15</Label>
                <Label>16</Label>
                <Label>17</Label>
                <Label>18</Label>
                <Label>19</Label>
                <Label>20</Label>
                <Label>21</Label>
                <Label>22</Label>
                <Label>23</Label>
                <Label>24</Label>
            </local:SimpleGrid>
        </Border>
        <Border BorderBrush="Blue" BorderThickness="3">
            <local:SimpleGrid Rows="6" Columns="4" Orientation="Horizontal">
                <Label>21</Label>
                <Label>22</Label>
                <Label>23</Label>
                <Label>24</Label>
                <Label>25</Label>
                <Label>26</Label>
                <Label>27</Label>
                <Label>28</Label>
                <Label>29</Label>
                <Label>30</Label>
                <Label>31</Label>
                <Label>32</Label>
                <Label>33</Label>
                <Label>34</Label>
                <Label>35</Label>
                <Label>36</Label>
                <Label>37</Label>
                <Label>38</Label>
                <Label>39</Label>
                <Label>40</Label>
                <Label>40</Label>
                <Label>41</Label>
                <Label>42</Label>
                <Label>43</Label>
            </local:SimpleGrid>
        </Border>
    </local:SimpleGrid>
</Window>
```

## 例

boiler's Graphics で統計機能を作ったのですが、ここでSimpleGridが大活躍しました。縦:横=20:3のグリッドになります。（コード上の定義では20x5と広めに設定していますが、子要素の数が足りなくて20x3で表示されています）
このSimpleGrid、子要素としてDockPanelを使うと良い具合になります。

![2021-11-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4734/e9d12a10-7dc5-96c2-09ac-8b4806280af1.png)

```xml
<UserControl x:Class="boilersGraphics.Views.Statistics"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:controls="clr-namespace:boilersGraphics.Controls"
             xmlns:converter="clr-namespace:boilersGraphics.Converters"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:viewModel="clr-namespace:boilersGraphics.ViewModels"
             d:DataContext="{d:DesignInstance viewModel:StatisticsDialogViewModel}"
             prism:ViewModelLocator.AutoWireViewModel="True"
             Background="White"
             mc:Ignorable=" d">
    <UserControl.Resources>
        <converter:TimeSpanRoundConverter x:Key="TimeSpanRound" />
    </UserControl.Resources>
    <prism:Dialog.WindowStyle>
        <Style TargetType="Window">
            <Setter Property="SizeToContent" Value="WidthAndHeight" />
        </Style>
    </prism:Dialog.WindowStyle>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Loaded">
            <i:InvokeCommandAction Command="{Binding LoadedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <controls:SimpleGrid Columns="5"
                         Orientation="Vertical"
                         Rows="20">
        <controls:SimpleGrid.Resources>
            <Style TargetType="Label">
                <Setter Property="HorizontalAlignment" Value="Right" />
            </Style>
        </controls:SimpleGrid.Resources>
        <DockPanel>
            <Label>起動回数</Label>
            <Label Content="{Binding NumberOfBoots.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label>累計稼働時間</Label>
            <Label Content="{Binding Uptime.Value, StringFormat={}{0:hh\\:mm\\:ss}, ConverterCulture=ja-jP, Converter={StaticResource TimeSpanRound}}" d:Content="00:00:00" />
        </DockPanel>
        <DockPanel>
            <Label>ファイルを指定して開いた回数</Label>
            <Label Content="{Binding NumberOfTimesTheFileWasOpenedBySpecifyingIt.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label>自動保存ファイルを指定して開いた回数</Label>
            <Label Content="{Binding NumberOfTimesTheAutoSaveFileIsSpecifiedAndOpened.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label>ポインターツールでクリックした回数</Label>
            <Label Content="{Binding NumberOfClicksWithThePointerTool.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label>なげなわツールで選択したアイテムの累計</Label>
            <Label Content="{Binding CumulativeTotalOfItemsSelectedWithTheLassoTool.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label>直線ツールの描画回数</Label>
            <Label Content="{Binding NumberOfDrawsOfTheStraightLineTool.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label>四角形ツールの描画回数</Label>
            <Label Content="{Binding NumberOfDrawsOfTheRectangleTool.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label>円ツールの描画回数</Label>
            <Label Content="{Binding NumberOfDrawsOfTheEllipseTool.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label>画像ファイルツールの描画回数</Label>
            <Label Content="{Binding NumberOfDrawsOfTheImageFileTool.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label>文字ツールの描画回数</Label>
            <Label Content="{Binding NumberOfDrawsOfTheLetterTool.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label>縦書きツールの描画回数</Label>
            <Label Content="{Binding NumberOfDrawsOfTheVerticalLetterTool.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 多角形ツールの描画回数</Label>
            <Label Content="{Binding NumberOfDrawsOfPolygonTool.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> ベジエ曲線ツールの描画回数</Label>
            <Label Content="{Binding NumberOfDrawsOfBezierCurveTool.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> スナップポイントツールの設置回数</Label>
            <Label Content="{Binding NumberOfSnapPointToolInstallations.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> ブラシツールの描画回数</Label>
            <Label Content="{Binding BrushToolDrawCount.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 消しゴムツールの使用回数</Label>
            <Label Content="{Binding NumberOfTimesTheEraserToolHasBeenUsed.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 上書き保存した回数</Label>
            <Label Content="{Binding NumberOfTimesSaved.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 名前を付けて保存した回数</Label>
            <Label Content="{Binding NumberOfTimesYouHaveNamedAndSaved.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> エクスポートした回数</Label>
            <Label Content="{Binding NumberOfExports.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> Jpegエクスポートした回数</Label>
            <Label Content="{Binding NumberOfJpegExports.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> PNGエクスポートした回数</Label>
            <Label Content="{Binding NumberOfPngExports.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> GIFエクスポートした回数</Label>
            <Label Content="{Binding NumberOfGifExports.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> BMPエクスポートした回数</Label>
            <Label Content="{Binding NumberOfBmpExports.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> TIFFエクスポートした回数</Label>
            <Label Content="{Binding NumberOfTiffExports.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> WMPエクスポートした回数</Label>
            <Label Content="{Binding NumberOfWmpExports.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> グループ化した回数</Label>
            <Label Content="{Binding NumberOfTimesGrouped.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> グループ化解除した回数</Label>
            <Label Content="{Binding NumberOfUngrouped.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 最前面へ移動した回数</Label>
            <Label Content="{Binding NumberOfMovesToTheFrontend.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 前面へ移動した回数</Label>
            <Label Content="{Binding NumberOfMovesToTheFront.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 背面へ移動した回数</Label>
            <Label Content="{Binding NumberOfMovesToTheBack.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 最背面へ移動した回数</Label>
            <Label Content="{Binding NumberOfMovesToTheBackend.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 上揃えした回数</Label>
            <Label Content="{Binding NumberOfTopAlignment.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 上下中央揃えした回数</Label>
            <Label Content="{Binding NumberOfTimesTheTopAndBottomAreCentered.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 下揃えした回数</Label>
            <Label Content="{Binding NumberOfBottomAlignment.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 左揃えした回数</Label>
            <Label Content="{Binding NumberOfLeftAlignment.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 左右中央揃えした回数</Label>
            <Label Content="{Binding NumberOfTimesLeftAndRightCentered.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 右揃えした回数</Label>
            <Label Content="{Binding NumberOfRightAlignment.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 左右に整列した回数</Label>
            <Label Content="{Binding NumberOfTimesAlignedLeftAndRight.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 上下に整列した回数</Label>
            <Label Content="{Binding NumberOfTimesAlignedUpAndDown.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 幅を合わせた回数</Label>
            <Label Content="{Binding NumberOfTimesToMatchTheWidth.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 高さを合わせた回数</Label>
            <Label Content="{Binding NumberOfTimesToMatchTheHeight.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> Unionした回数</Label>
            <Label Content="{Binding NumberOfUnions.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> Intersectした回数</Label>
            <Label Content="{Binding NumberOfIntersects.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> Xorした回数</Label>
            <Label Content="{Binding NumberOfXors.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> Excludeした回数</Label>
            <Label Content="{Binding NumberOfExcludes.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 切り取りした回数</Label>
            <Label Content="{Binding NumberOfCuts.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> コピーした回数</Label>
            <Label Content="{Binding NumberOfCopies.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 貼り付けした回数</Label>
            <Label Content="{Binding NumberOfPasted.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 元に戻した回数</Label>
            <Label Content="{Binding NumberOfUndos.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> やり直しした回数</Label>
            <Label Content="{Binding NumberOfRedoes.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 自動保存した回数</Label>
            <Label Content="{Binding NumberOfTimesAutomaticallySaved.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 新規作成したレイヤー数</Label>
            <Label Content="{Binding NumberOfNewlyCreatedLayers.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> 削除したレイヤー数</Label>
            <Label Content="{Binding NumberOfDeletedLayers.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> アイテムを描画した回数</Label>
            <Label Content="{Binding NumberOfTimesTheItemWasDrawn.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> アイテムを削除した回数</Label>
            <Label Content="{Binding NumberOfTimesTheItemWasDeleted.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> ログレベルを変更した回数</Label>
            <Label Content="{Binding NumberOfLogLevelChanges.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> バージョン情報ダイアログを表示した回数</Label>
            <Label Content="{Binding NumberOfTimesTheVersionInformationDialogWasDisplayed.Value}" d:Content="0" />
        </DockPanel>
        <DockPanel>
            <Label> アプリケーションログを表示した回数</Label>
            <Label Content="{Binding NumberOfTimesTheApplicationLogWasDisplayed.Value}" d:Content="0" />
        </DockPanel>
    </controls:SimpleGrid>
</UserControl>
```

## 注意

Grid.ColumnSpanプロパティやGrid.RowSpanプロパティはSimpleGridの子要素に使っても効果がないので、そこだけ注意してください。

## ライセンス

MITライセンスです。

## 追記(2021/11/16 15時頃)

SimpleGridのソースコードを載せ忘れましたので載せときます。

```csharp
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace SimpleGridSample
{
    [DesignTimeVisible(true)]
    public class SimpleGrid : Panel
    {
        #region 依存プロパティ

        public static readonly DependencyProperty RowsProperty = DependencyProperty.Register("Rows",
            typeof(int),
            typeof(SimpleGrid),
            new FrameworkPropertyMetadata(1, new PropertyChangedCallback(SimpleGrid.OnRowsChanged)));

        public static readonly DependencyProperty ColumnsProperty = DependencyProperty.Register("Columns",
            typeof(int),
            typeof(SimpleGrid),
            new FrameworkPropertyMetadata(1, new PropertyChangedCallback(SimpleGrid.OnColumnsChanged)));

        public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register("Orientation",
            typeof(Orientation),
            typeof(SimpleGrid),
            new FrameworkPropertyMetadata(Orientation.Horizontal));

        #endregion //依存プロパティ

        #region 依存プロパティコールバック

        private static void OnRowsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            SimpleGrid ctrl = d as SimpleGrid;
            if (ctrl != null)
            { }
        }

        private static void OnColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            SimpleGrid ctrl = d as SimpleGrid;
            if (ctrl != null)
            { }
        }

        #endregion //依存プロパティコールバック

        #region CLR プロパティ

        public int Rows
        {
            get { return (int)GetValue(RowsProperty); }
            set { SetValue(RowsProperty, value); }
        }

        public int Columns
        {
            get { return (int)GetValue(ColumnsProperty); }
            set { SetValue(ColumnsProperty, value); }
        }

        public Orientation Orientation
        {
            get { return (Orientation)GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

        #endregion //CLR プロパティ

        private List<Cell> _cells;

        /// <summary>
        /// 子要素を配置し、パネルのサイズを決定する。
        /// </summary>
        /// <param name="finalSize">パネル自体と子要素を配置するために使用する親の末尾の領域。</param>
        /// <returns>使用する実際のサイズ。</returns>
        protected override Size ArrangeOverride(Size finalSize)
        {
            Rect viewport = new Rect(new Point(0, 0), finalSize);

            if (Orientation == Orientation.Horizontal)
            {
                LinkedList<List<UIElement>> rows = GetChildrenStructure();
                var iterator = rows.First;
                for (int y = 0; y < rows.Count; ++y)
                {
                    var topHeight = _cells.Where(a => a.Y < y).GroupBy(b => b.Y).Sum(c => c.Max(d => d.Height));
                    var heightOn = _cells.Where(a => a.Y == y).Max(b => b.Height);
                    var row = iterator.Value;
                    if (row == null) continue;
                    for (int x = 0; x < row.Count; ++x)
                    {
                        var cell = row[x];
                        var leftWidth = _cells.Where(a => a.X < x).GroupBy(b => b.X).Sum(c => c.Max(d => d.Width));
                        var widthOn = _cells.Where(a => a.X == x).Max(b => b.Width);
                        Rect finalRect = new Rect(leftWidth, topHeight, widthOn, heightOn);
                        cell.Arrange(finalRect);
                    }
                    iterator = iterator.Next;
                }
            }
            else
            {
                LinkedList<List<UIElement>> cols = GetChildrenStructure();
                var iterator = cols.First;
                for (int x = 0; x < cols.Count; ++x)
                {
                    var leftWidth = _cells.Where(a => a.X < x).GroupBy(b => b.X).Sum(c => c.Max(d => d.Width));
                    var widthOn = _cells.Where(a => a.X == x).Max(b => b.Width);
                    
                    var row = iterator.Value;
                    if (row == null) continue;
                    for (int y = 0; y < row.Count; ++y)
                    {
                        var cell = row[y];
                        var topHeight = _cells.Where(a => a.Y < y).GroupBy(b => b.Y).Sum(c => c.Max(d => d.Height));
                        var heightOn = _cells.Where(a => a.Y == y).Max(b => b.Height);
                        Rect finalRect = new Rect(leftWidth, topHeight, widthOn, heightOn);
                        cell.Arrange(finalRect);
                    }
                    iterator = iterator.Next;
                }
            }

            return finalSize;
        }

        /// <summary>
        /// 子要素に必要なレイアウトのサイズを測定し、パネルのサイズを決定する。
        /// </summary>
        /// <param name="availableSize">子要素に与えることができる使用可能なサイズ。</param>
        /// <returns>レイアウト時にこのパネルが必要とするサイズ。</returns>
        protected override Size MeasureOverride(Size availableSize)
        {
            LinkedList<List<UIElement>> rows = GetChildrenStructure();
            _cells = getCellsSize(Orientation, availableSize, rows);
            double totalMaxWidth = _cells.GroupBy(a => a.X).Sum(b => b.Max(c => c.Width));
            double totalMaxHeight = _cells.GroupBy(a => a.Y).Sum(b => b.Max(c => c.Height));

            return new Size(totalMaxWidth, totalMaxHeight);
        }

        private static List<Cell> getCellsSize(Orientation Orientation, Size availableSize, LinkedList<List<UIElement>> rows)
        {
            List<Cell> cells = new List<Cell>();

            if (Orientation == Orientation.Horizontal)
            {
                var iterator = rows.First;
                for (int y = 0; y < rows.Count; ++y)
                {
                    var row = iterator.Value;
                    for (int x = 0; x < row.Count; ++x)
                    {
                        var cell = row[x];
                        cell.Measure(availableSize);
                        var disiredSize = cell.DesiredSize;
                        cells.Add(new Cell(x, y, disiredSize.Width, disiredSize.Height));
                    }
                    iterator = iterator.Next;
                }
            }
            else
            {
                var iterator = rows.First;
                for (int x = 0; x < rows.Count; ++x)
                {
                    var row = iterator.Value;
                    for (int y = 0; y < row.Count; ++y)
                    {
                        var cell = row[y];
                        cell.Measure(availableSize);
                        var disiredSize = cell.DesiredSize;
                        cells.Add(new Cell(x, y, disiredSize.Width, disiredSize.Height));
                    }
                    iterator = iterator.Next;
                }
            }

            return cells;
        }

        private LinkedList<List<UIElement>> GetChildrenStructure()
        {
            LinkedList<List<UIElement>> rows = new LinkedList<List<UIElement>>();

            if (Orientation == Orientation.Horizontal)
            {
                foreach (UIElement child in this.InternalChildren)
                {
                    var currentList = rows.Last?.Value;
                    if (currentList == null || (currentList.Count == Columns && rows.Count < Rows))
                    {
                        var list = new List<UIElement>();
                        list.Add(child);
                        rows.AddLast(list);
                    }
                    else if (currentList.Count < Columns)
                    {
                        currentList.Add(child);
                    }
                }
            }
            else
            {
                foreach (UIElement child in this.InternalChildren)
                {
                    var currentList = rows.Last?.Value;
                    if (currentList == null || (currentList.Count == Rows && rows.Count < Columns))
                    {
                        var list = new List<UIElement>();
                        list.Add(child);
                        rows.AddLast(list);
                    }
                    else if (currentList.Count < Rows)
                    {
                        currentList.Add(child);
                    }
                }
            }

            return rows;
        }

        private class Cell
        {
            internal int X { get; set; }
            internal int Y { get; set; }
            internal double Width { get; set; }
            internal double Height { get; set; }

            internal Cell(int x, int y, double width, double height)
            {
                X = x;
                Y = y;
                Width = width;
                Height = height;
            }

            public override string ToString()
            {
                return $"{{X={X}, Y={Y}, W={Width}, H={Height}}}";
            }
        }
    }
}

```