65.9K
CodeProject 正在变化。 阅读更多。
Home

GridEx for WPF: 子元素的自动放置

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (5投票s)

2014年4月8日

CPOL

4分钟阅读

viewsIcon

16934

downloadIcon

540

GridEx 通过其自动布局使 WPF Grid 的定义和编辑更加容易。

引言

我一直在寻找一个增强型的 Grid,它能提供以下好处

  • 默认情况下,它会将子元素自动排列到两列中,一个接一个。
  • 它不需要为每个子元素设置 Grid.RowGrid.Column 附加属性。
  • 它不需要 Grid.RowDefinitionsGrid.ColumnDefinitions。换句话说,它会在需要时自动添加行,列也是如此。它们的 Width/Height 将设置为 "Auto",但最后一列除外:默认设置为 "*"
  • 如果指定,它将允许一个元素相对于前一个元素放置
    • 在同一行
    • 直接在新的一行
    • 在同一个单元格
    • 在指定位置(使用 Grid.RowGrid.Column 附加属性的旧样式)

我在 Code Project 上几乎找到了合适的控件。IsaksTwoColumnGrid 几乎就是我想要的 Grid,但我想要更多功能。

在浏览网页时,我还发现了Sam Naseri关于简化 Grid 定义的精彩代码。我在我的文章中使用了他的代码。

使用代码

上面的 Grid 是使用以下 XAML 生成的(为简洁起见,我没有写资源部分)

第一个 Grid

<tools:GridEx>

    <Label Content="Name:" />
    <TextBox />

    <Label Content="First name:" />
    <TextBox />

    <Label Content="Age:" />
    <TextBox />
            
</tools:GridEx>

第二个 Grid

<tools:GridEx>

    <!-- A series of pair of items --/>
    <Label Content="Name:" />
    <TextBox />

    <Label Content="First name:" />
    <TextBox />

    <Label Content="Age:" />
    <TextBox />
            
    <!-- here we want to jump to the next row --/>
    <Label Content="Interested in:"
           tools:GridEx.GridExBehavior="NextRow" />
    <CheckBox Content="Geography" />
    <!-- here we want all the following items to remain on the same row -->
    <CheckBox Content="History"
              tools:GridEx.GridExBehavior="SameRow" />
    <CheckBox Content="Literature"
              tools:GridEx.GridExBehavior="SameRow" />
    <CheckBox Content="Math"
              tools:GridEx.GridExBehavior="SameRow" />
    <CheckBox Content="Physics"
              tools:GridEx.GridExBehavior="SameRow" />

    <!-- We want our smiley to be at a specific place, so we set its behavior to Manual -->
    <ContentControl Content="{StaticResource smiley}"
                    tools:GridEx.GridExBehavior="Manual"
                    Grid.Row="0"
                    Grid.Column="10"
                    Grid.RowSpan="2" />

</tools:GridEx>

第三个 Grid

<tools:GridEx>
    <!-- On this grid, we put 4 items per row
        and we don't want every column to be on Auto,
        so we define them manually using Sam Naseri's elegant property -->
    <tools:GridEx ItemsPerRow="4"
                  tools:GridEx.Columns="auto;*;auto;*">

    <Label Content="Name:" />
    <TextBox />
    <Label Content="First name:" />
    <TextBox />

    <Label Content="Address:" />
    <TextBox />
    <Label Content="City:" />
    <TextBox />

</tools:GridEx>

您看到了我们的 XAML 现在多么简洁明了?

  • 我没有在第一个和第二个 Grid 中编写任何 RowDefinitionsColumnDefinitions
  • 在第三个 Grid 中,我只使用了Sam Naseri优雅的附加属性编写了列定义。
  • 只有一个元素(笑脸)设置了 Grid.RowGrid.Column:因为我想手动放置它。

现在,让我们一步一步地看看如何实现这一点。

每个子元素的自动行/列索引

想法是在添加 Grid 的子元素时立即排列它们。第一个子元素将被赋予 Row=0, Column=0,第二个子元素 Row=0, Column=1,第三个子元素 Row=1, Column=0,依此类推。

为了实现这一点,我首先想向 Grid 类添加一个附加属性,并监听其 Children 集合,以便在集合被修改时收到通知。我以为可以使用 CollectionChanged 事件或类似的机制,但不幸的是,没有这样的事件。我发现的唯一方法是重载 Grid.OnVisualChildrenChanged 方法,这意味着我不得不进行子类化……

因此,我创建了 GridEx 类并重载了其 OnVisualChildrenChanged 方法

public class GridEx : Grid
{
    /// <summary>
    /// Overriden to automatically set the Row and Column properties of each child.
    /// This method is called when children are added or removed from the Grid.Children collection.
    /// </summary>
    protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
    {
        UpdateChildren();
        base.OnVisualChildrenChanged(visualAdded, visualRemoved);
    }

    void UpdateChildren()
    {
        //current row and column
        //columnIndex is incremented for each item to place them one after the other
        int rowIndex = 0;
        int columnIndex = 0;
        foreach (UIElement child in Children)
        {
            //happens while editing the children collection in xaml
            //if we don't handle this, the Designer throws an exception
            if (child == null)
                continue;

            //if columnIndex is below ItemsPerRow, put this item on the current row
            //else put this item on the next row, first column
            if (columnIndex >= ItemsPerRow)
            {
                rowIndex++;
                columnIndex = 0;
            }
            Grid.SetRow(child, rowIndex);
            Grid.SetColumn(child, columnIndex++);
        }
    }
}

然后,我添加了 ItemsPerRow 属性

/// <summary>
/// Number of items per row.
/// Default is 2.
/// </summary>
public int ItemsPerRow
{
    get { return (int)GetValue(ItemsPerRowProperty); }
    set { SetValue(ItemsPerRowProperty, value); }
}
public static readonly DependencyProperty ItemsPerRowProperty = DependencyProperty.Register(
    "ItemsPerRow",
    typeof(int),
    typeof(GridEx),
    new FrameworkPropertyMetadata(2, FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure, ItemsPerRow_PropertyChanged));
static void ItemsPerRow_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    GridEx grid = d as GridEx;
    if (grid != null)
        grid.UpdateChildren();
}

ItemsPerRow 属性上的 FrameworkPropertyMetadataOptions 意味着在该属性更改后应执行新的布局传递。

自动行和列定义

想法是检索所有子元素中的最大行索引,并检查是否需要添加额外的行。列的逻辑相同。

因此,在所有子元素都获得了行和列索引后,我们必须找到添加 RowDefinitionsColumnDefinitions 的正确位置。重写 MeasureOverride 方法可能是最佳选择,因为它在控件准备好进行布局之前和布局之前被调用。

protected override Size MeasureOverride(Size constraint)
{
    //OfType<> is used to get an enumerable collection of non-null objects.
    //Children may contain null objects at design time --> they should be ignored to avoid exceptions.
    IEnumerable<UIElement> children = Children.OfType<UIElement>();
    if (children.Count() > 0)
    {
        int maxRowIndex = children.Max(child => GetRow(child));
        int maxColumnIndex = children.Max(child => GetColumn(child));

        //automatically add rows if needed
        for (int i = RowDefinitions.Count; i <= maxRowIndex; i++)
            RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
        //automatically add columns if needed
        for (int i = ColumnDefinitions.Count; i <= maxColumnIndex; i++)
            ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
    }

    return base.MeasureOverride(constraint);
}

我们将额外行和列的大小设置为 Auto,但我们希望最后一列默认拉伸。因此,让我们为此添加另一个属性,LastColumnWidth

/// <summary>
/// Width of the last automatically added column.
/// Default is "*".
/// Note that this property will be ignored if the last column was defined manually.
/// </summary>
public GridLength LastColumnWidth
{
    get { return (GridLength)GetValue(LastColumnWidthProperty); }
    set { SetValue(LastColumnWidthProperty, value); }
}
public static readonly DependencyProperty LastColumnWidthProperty = DependencyProperty.Register(
    "LastColumnWidth",
    typeof(GridLength),
    typeof(GridEx),
    new FrameworkPropertyMetadata(new GridLength(1, GridUnitType.Star), FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure));

现在我们修改添加列的 MeasureOverride 方法

//automatically add columns if needed
for (int i = ColumnDefinitions.Count; i <= maxColumnIndex; i++)
{
    GridLength unitType = new GridLength(1, GridUnitType.Auto);
    if (i == maxColumnIndex)
        unitType = LastColumnWidth;
    ColumnDefinitions.Add(new ColumnDefinition() { Width = unitType });
}

我们的方法现在 OK 了。请注意,仅当最大索引超过当前计数时才会添加额外的定义。这意味着我们仍然可以提供自己的 RowDefinition/ColumnDefinitionGridEx 只会添加缺少的。

额外的行为

我们已经完成了几乎所有的要求,唯一缺失的功能是最后一些。

  • 如果指定,它将允许一个元素相对于前一个元素放置
    • 在同一行
    • 直接在新的一行
    • 在同一个单元格
    • 在指定位置(使用 Grid.RowGrid.Column 附加属性的旧样式)

我们将对此使用子元素上的附加属性。这个属性的类型将是一个枚举。

/// <summary>
/// Defines the behavior of a child item.
/// </summary>
public enum GridExBehavior
{
    /// <summary>
    /// Items are added one after the other, with 2 items per row by default.
    /// </summary>
    Default,
    /// <summary>
    /// This item should be placed on the same row, next column.
    /// </summary>
    SameRow,
    /// <summary>
    /// This item should be placed on the same row, same column.
    /// </summary>
    SameCell,
    /// <summary>
    /// This item should be placed on the next row, first column.
    /// </summary>
    NextRow,
    /// <summary>
    /// This item should be ignored.
    /// Its Grid.Row and Grid.Column properties should be set manually.
    /// </summary>
    Manual
}

然后是相应的附加属性。这个属性可以用于任何类型的子元素,所以我们将它的 OwnerType 设置为 UIElement

public static GridExBehavior GetGridExBehavior(DependencyObject obj)
{
    return (GridExBehavior)obj.GetValue(GridExBehaviorProperty);
}
public static void SetGridExBehavior(DependencyObject obj, GridExBehavior value)
{    
    obj.SetValue(GridExBehaviorProperty, value);
}
public static readonly DependencyProperty GridExBehaviorProperty = DependencyProperty.RegisterAttached(
    "GridExBehavior",
    typeof(GridExBehavior),
    typeof(UIElement),
    new UIPropertyMetadata(GridExBehavior.Default, GridExBehavior_PropertyChangedCallback));
static void GridExBehavior_PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    GridEx grid = VisualTreeHelper.GetParent(d) as GridEx;
    if (grid != null)
        grid.UpdateChildren();
}

请注意,如果属性值发生更改,它会调用 UpdateChildren,因为所有索引都可能受到此更改的影响。

最后,我们必须更改 UpdateChildren 方法来处理不同的行为

void UpdateChildren()
{
    //current row and column
    //columnIndex is incremented for each item to place them one after the other
    int rowIndex = 0;
    int columnIndex = 0;
    foreach (UIElement child in Children)
    {
        //happens while editing the children collection in xaml
        //if we don't handle this, the Designer throws an exception
        if (child == null)
            continue;

        //we'll set the child's Row and Column properties depending on its attached behavior
        switch (GridEx.GetGridExBehavior(child))
        {
            case GridExBehavior.Default:
                //if columnIndex is below ItemsPerRow, put this item on the current row
                //else put this item on the next row, first column
                if (columnIndex >= ItemsPerRow)
                {
                    rowIndex++;
                    columnIndex = 0;
                }
                Grid.SetRow(child, rowIndex);
                Grid.SetColumn(child, columnIndex++);
                break;

            case GridExBehavior.SameRow:
                //put this item on the same row
                Grid.SetRow(child, rowIndex);
                Grid.SetColumn(child, columnIndex++);
                break;

            case GridExBehavior.SameCell:
                //put this item on the same row, same column (of the previous item)
                //just make sure we don't set a negative column index (happens sometimes when editing XAML code)
                Grid.SetRow(child, rowIndex);
                Grid.SetColumn(child, Math.Max(0, columnIndex - 1));
                break;

            case GridExBehavior.NextRow:
                //put this item on the next row, first column
                columnIndex = 0;
                Grid.SetRow(child, ++rowIndex);
                Grid.SetColumn(child, columnIndex++);
                break;

            case GridExBehavior.Manual:
                //don't change anything on this item
                break;
        }
    }
}

我们的 GridEx 现在完成了。您也可以将Sam Naseri的代码包含进去,以利用他出色的附加属性。

结论

它不是很多代码,也不是一个非常复杂的控件,但它比常规的 Grid 容易使用得多。它提高了可读性,并使编辑(移动行等)变得轻松。我希望您会觉得它有用。我发现了一个小问题:您有时需要手动重新加载设计器(例如通过重新生成项目)才能反映您的更改。

历史

  • 2014-04-07:初版。
© . All rights reserved.