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






4.79/5 (5投票s)
GridEx 通过其自动布局使 WPF Grid 的定义和编辑更加容易。
引言
我一直在寻找一个增强型的 Grid
,它能提供以下好处
- 默认情况下,它会将子元素自动排列到两列中,一个接一个。
- 它不需要为每个子元素设置
Grid.Row
和Grid.Column
附加属性。 - 它不需要
Grid.RowDefinitions
和Grid.ColumnDefinitions
。换句话说,它会在需要时自动添加行,列也是如此。它们的Width
/Height
将设置为"Auto"
,但最后一列除外:默认设置为"*"
。 - 如果指定,它将允许一个元素相对于前一个元素放置
- 在同一行
- 直接在新的一行
- 在同一个单元格
- 在指定位置(使用
Grid.Row
和Grid.Column
附加属性的旧样式)
我在 Code Project 上几乎找到了合适的控件。Isaks 的 TwoColumnGrid 几乎就是我想要的 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 中编写任何
RowDefinitions
或ColumnDefinitions
。 - 在第三个 Grid 中,我只使用了Sam Naseri优雅的附加属性编写了列定义。
- 只有一个元素(笑脸)设置了
Grid.Row
和Grid.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
意味着在该属性更改后应执行新的布局传递。
自动行和列定义
想法是检索所有子元素中的最大行索引,并检查是否需要添加额外的行。列的逻辑相同。
因此,在所有子元素都获得了行和列索引后,我们必须找到添加 RowDefinitions
和 ColumnDefinitions
的正确位置。重写 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
/ColumnDefinition
,GridEx
只会添加缺少的。
额外的行为
我们已经完成了几乎所有的要求,唯一缺失的功能是最后一些。
- 如果指定,它将允许一个元素相对于前一个元素放置
- 在同一行
- 直接在新的一行
- 在同一个单元格
- 在指定位置(使用
Grid.Row
和Grid.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:初版。