WPF自定義FixedColumnGrid佈局控制元件

ggtc發表於2024-04-24

按照上一節所講,我已經對佈局系統又所瞭解。接下來我就實現一個佈局控制元件FixedColumnGrid

1.基礎版

佈局控制元件機制如下,FixedColumnGrid將子控制元件按照水平排列,每行滿兩列後換行。每個控制元件大小相同,高度固定為50。

第一步,先過載測量和排列方法

protected override Size MeasureOverride(Size constraint)
{
    //base.MeasureOverride(constraint);
    return constraint;
}

protected override Size ArrangeOverride(Size arrangeBounds)
{
    //base.ArrangeOverride(arrangeBounds);
    return arrangeBounds;
}

根據機制,我們需要自己決定子控制元件尺寸,也就是需要自己測量和排列子控制元件。所以我們就不需要祖先的遞迴了,將由我們自己手動遞迴,所有註釋掉base呼叫。

第二步,測量子控制元件

雖然我們可以直接把constraint傳過去,但我們根據佈局機制,儘可能的少傳遞可用空間給子控制元件。所以我們接下來在MeasureOverride新增如下測量程式碼。

//base.MeasureOverride(constraint);
for (int i = 0; i < this.VisualChildrenCount; i++)
{
    UIElement child = (UIElement)this.GetVisualChild(i);
    if (child!=null)
    {
        child.Measure(new Size(constraint.Width / 2, 50));
    }
}
return constraint;

第三步,測量子控制元件後,根據其期望尺寸,排列子控制元件,因此,接下來在ArrangeOverride中新增排列程式碼。

//base.ArrangeOverride(arrangeBounds);
for (int i = 0; i < this.VisualChildrenCount; i++)
{
    UIElement child = (UIElement)this.GetVisualChild(i);
    if (child!=null)
    {
        if (i % 2 == 0)
        {
            child.Arrange(new Rect(new Point(0, Math.Floor(i / 2d) * 50), child.DesiredSize));
        }
        else
        {
            child.Arrange(new Rect(new Point(arrangeBounds.Width / 2, i / 2 * 50), child.DesiredSize));
        }
    }
}
return arrangeBounds;

現在,我們已經可以試著看看效果了。先生成一下專案,再到mainWindow.xaml中新增一個FixedColumnGrid控制元件

    <Border Background="Blue">
        <local:FixedColumnGrid>
            <Button Content="btn" Width="500" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center"/>
            <Button Content="btn" Width="500" Height="100"/>
            <Button Content="btn" Width="500" Height="100"/>
            <Button Content="btn"/>
        </local:FixedColumnGrid>
    </Border>

可以看到一些問題。第一個button是手動居中的,第二個就沒有居中了。這時因為第二個button期望的控制元件大於FixedColumnGrid理應給他的控制元件,所以儘管他的期望尺寸被限制在了FixedColumnGrid所給的尺寸(400,50),但其真是大小卻要大些,所以看起來第二個button的內容就沒有居中了。

第4個button沒有設定尺寸,又太小了。這時因為我們排列button時,使用的時其期望大小,而button沒有手動指定width和height時,其期望大小是根據內容定的。

因此我改進了下排列,不根據子控制元件期望尺寸排列,而是根據我們根據佈局機制規定的尺寸排列,現在得到了想要的效果。

if (i % 2 == 0)
{
    child.Arrange(new Rect(new Point(0, i/2 * 50), new Size(arrangeBounds.Width / 2, 50)));
}
else
{
    child.Arrange(new Rect(new Point(arrangeBounds.Width / 2, Math.Floor(i / 2d) * 50), new Size(arrangeBounds.Width / 2, 50)));
}

對MeasureOverride和ArrangeOverride稍作修改

protected override Size MeasureOverride(Size constraint)
{
    //base.MeasureOverride(constraint);
    for (int i = 0; i < this.VisualChildrenCount; i++)
    {
        UIElement child = (UIElement)this.GetVisualChild(i);
        if (child!=null)
        {
            child.Measure(new Size(constraint.Width / 2, 50));
        }
    }
    return constraint;
}

protected override Size ArrangeOverride(Size arrangeBounds)
{
    //base.ArrangeOverride(arrangeBounds);
    for (int i = 0; i < this.VisualChildrenCount; i++)
    {
        UIElement child = (UIElement)this.GetVisualChild(i);
        if (child!=null)
        {
            if (this.Columns == default(int))
            {
                if (i % 2 == 0)
                {
                    child.Arrange(new Rect(new Point(0, i / 2 * 50), new Size(arrangeBounds.Width / 2, 50)));
                }
                else
                {
                    child.Arrange(new Rect(new Point(arrangeBounds.Width / 2, Math.Floor(i / 2d) * 50), new Size(arrangeBounds.Width / 2, 50)));
                }
            }
            else
            {
                double columnWidth = arrangeBounds.Width / this.Columns;//列寬
                int offsetColumn = i % this.Columns;//當前單元格處於哪一列
                child.Arrange(new Rect(new Point(offsetColumn * columnWidth, Math.Floor((double)i / this.Columns) * 50), new Size(columnWidth, 50)));
            }
        }
    }
    return arrangeBounds;
}

2.可擴充套件列數版

既然我們可以排兩列,哪能不能排1列,2列,3列呢。我決定繼續進行增強。

首先,我們要能定義列數,於是我增加了一個依賴屬性Columns。

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

public static readonly DependencyProperty ColumnsProperty =
    DependencyProperty.Register("Columns", typeof(int), typeof(FixedColumnGrid), new FrameworkPropertyMetadata(default(int),FrameworkPropertyMetadataOptions.AffectsArrange));

為了在更改列數時能重新排列,所以還增加了AffectsArrange

然後就可以在xaml中定義列數

<Border Background="Blue">
    <local:FixedColumnGrid Columns="1">
        <Button Content="btn" Width="500" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <Button Content="btn" Width="500" Height="100"/>
        <Button Content="btn" Width="500" Height="100" HorizontalAlignment="Left"/>
        <Button Content="btn"/>
    </local:FixedColumnGrid>
</Border>

這裡沒有居中的原因button自身HorizontalAlignment屬性會影響到排列

3.可自定義高度版

列數都能調整了,哪每行高度也能否調整呢,這倒是簡單,只是增加一個RowHeight依賴屬性罷了。

public int ColumnHeight
{
    get { return (int)GetValue(ColumnHeightProperty); }
    set { SetValue(ColumnHeightProperty, value); }
}

// Using a DependencyProperty as the backing store for ColumnHeight.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnHeightProperty =
    DependencyProperty.Register("ColumnHeight", typeof(int), typeof(FixedColumnGrid), new FrameworkPropertyMetadata(50,FrameworkPropertyMetadataOptions.AffectsMeasure));

同時載將測量和排列過載中的50換成高度

child.Measure(new Size(constraint.Width / 2, this.ColumnHeight));
...
child.Arrange(new Rect(new Point(0, i / 2 * this.ColumnHeight), new Size(arrangeBounds.Width / 2, this.ColumnHeight)));
...
child.Arrange(new Rect(new Point(arrangeBounds.Width / 2, Math.Floor(i / 2d) * this.ColumnHeight), new Size(arrangeBounds.Width / 2, this.ColumnHeight)));
...
child.Arrange(new Rect(new Point(offsetColumn * columnWidth, Math.Floor((double)i / this.Columns) * this.ColumnHeight), new Size(columnWidth, this.ColumnHeight)));

現在就可以在xaml中使用自定義高度了

<Border Background="Blue">
    <local:FixedColumnGrid Columns="2" ColumnHeight="120">
        <Button Content="btn" Width="500" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        <Button Content="btn" Width="500" Height="100"/>
        <Button Content="btn"/>
        <Button Content="btn"/>
        <Button Content="btn"/>
        <Button Content="btn"/>
    </local:FixedColumnGrid>
</Border>

FixedColumnGrid完整程式碼

WPF自定義FixedColumnGrid佈局控制元件
 1 public partial class FixedColumnGrid : Panel
 2 {
 3     public FixedColumnGrid()
 4     {
 5         InitializeComponent();
 6     }
 7 
 8     protected override Size MeasureOverride(Size constraint)
 9     {
10         //base.MeasureOverride(constraint);
11         for (int i = 0; i < this.VisualChildrenCount; i++)
12         {
13             UIElement child = (UIElement)this.GetVisualChild(i);
14             if (child!=null)
15             {
16                 child.Measure(new Size(constraint.Width / 2, this.ColumnHeight));
17             }
18         }
19         return constraint;
20     }
21 
22     protected override Size ArrangeOverride(Size arrangeBounds)
23     {
24         //base.ArrangeOverride(arrangeBounds);
25         for (int i = 0; i < this.VisualChildrenCount; i++)
26         {
27             UIElement child = (UIElement)this.GetVisualChild(i);
28             if (child!=null)
29             {
30                 if (this.Columns == default(int))
31                 {
32                     if (i % 2 == 0)
33                     {
34                         child.Arrange(new Rect(new Point(0, i / 2 * this.ColumnHeight), new Size(arrangeBounds.Width / 2, this.ColumnHeight)));
35                     }
36                     else
37                     {
38                         child.Arrange(new Rect(new Point(arrangeBounds.Width / 2, Math.Floor(i / 2d) * this.ColumnHeight), new Size(arrangeBounds.Width / 2, this.ColumnHeight)));
39                     }
40                 }
41                 else
42                 {
43                     double columnWidth = arrangeBounds.Width / this.Columns;//列寬
44                     int offsetColumn = i % this.Columns;//當前單元格處於哪一列
45                     child.Arrange(new Rect(new Point(offsetColumn * columnWidth, Math.Floor((double)i / this.Columns) * this.ColumnHeight), new Size(columnWidth, this.ColumnHeight)));
46                 }
47             }
48         }
49         return arrangeBounds;
50     }
51 
52 
53 
54     public int Columns
55     {
56         get { return (int)GetValue(ColumnsProperty); }
57         set { SetValue(ColumnsProperty, value); }
58     }
59 
60     public static readonly DependencyProperty ColumnsProperty =
61         DependencyProperty.Register("Columns", typeof(int), typeof(FixedColumnGrid), new FrameworkPropertyMetadata(default(int),FrameworkPropertyMetadataOptions.AffectsArrange));
62 
63 
64 
65     public int ColumnHeight
66     {
67         get { return (int)GetValue(ColumnHeightProperty); }
68         set { SetValue(ColumnHeightProperty, value); }
69     }
70 
71     // Using a DependencyProperty as the backing store for ColumnHeight.  This enables animation, styling, binding, etc...
72     public static readonly DependencyProperty ColumnHeightProperty =
73         DependencyProperty.Register("ColumnHeight", typeof(int), typeof(FixedColumnGrid), new FrameworkPropertyMetadata(50,FrameworkPropertyMetadataOptions.AffectsMeasure));
74 
75 
76 }
View Code

相關文章