Dan Crevier 的 VirtualizingTilePanel 修改版






4.80/5 (7投票s)
修改 Dan Crevier 的 VirtualizingTilePanel 以填充整个区域
引言
这篇文档的价值可能不高,但我还是发布它,希望能为尝试实现相同机制的其他人节省时间。Dan Crevier 写了一篇精彩的文章,介绍了如何在 WPF 中实现虚拟化平铺/换行面板 (链接)。这篇文章中 99% 的代码都归他所有。我对他的实现的一个问题是,他的布局逻辑可能会在项目控件的侧面留下难看的空白。使用 Dan 的逻辑,你指定面板中每个子项的宽度(技术上是高度),然后算法会计算在可用空间中可以容纳多少个子项。
我调整了逻辑,以消除空白;行上的项目数量基于提供的 MinChildWidth 属性。然后,每个项目的最终宽度根据可用宽度除以可以容纳的项目数量来计算。这种逻辑消除了项目溢出到新行时出现的难看间隙,并保持了出色的换行行为。
请注意,在早期版本中,你必须从一开始就提供所需的列数,然后相应地确定项目宽度。但是,我后来意识到这种逻辑实际上会破坏换行行为,因此是不希望的。如果你希望项目根据实际空间重新排列,这一点很重要。尽管如此,为了保持向后兼容性,我保留了新的逻辑。
Using the Code
我将描述 Dan 文章中的更改。添加了三个新的依赖属性。
Tile
属性指定要执行的布局逻辑。设置为 true
以使用 Dan 修改后的逻辑(推荐),或false使用修改后的逻辑。
如果使用新逻辑,则 Columns
属性设置行上所需的子项数量。此属性仅在 Tile 属性为 false 时适用。
ChildSize
属性已重命名为 ChildHeight
。
MinChildWidth
属性指示每个子项所需的最小宽度。此属性仅在 Tile 属性为 true 时适用。
/// <summary>
/// Controls the size of the child elements.
/// </summary>
public static readonly DependencyProperty ChildHeightProperty
= DependencyProperty.RegisterAttached("ChildHeight",
typeof(double), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(200.0d,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
/// <summary>
/// Controls the size of the child elements.
/// </summary>
public static readonly DependencyProperty MinChildWidthProperty
= DependencyProperty.RegisterAttached("MinChildWidth", typeof(double), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
/// <summary>
/// Controls the number of the child elements in a row.
/// </summary>
public static readonly DependencyProperty ColumnsProperty
= DependencyProperty.RegisterAttached("Columns",
typeof(int), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(10,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
/// <summary>
/// If setting is true, the component will calulcate the number
/// of children per row, the width of each item is set equal
/// to the height. In this mode the columns property is ignored.
/// If the setting is false, the component will calulate the
/// width of each item by dividing the available size by the
/// number of desired rows.
/// </summary>
public static readonly DependencyProperty TileProperty
= DependencyProperty.RegisterAttached("Tile",
typeof(bool), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(true,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
/// <summary>
/// Gets or sets the height of each child.
/// </summary>
public double ChildHeight
{
get { return (double)GetValue(ChildHeightProperty); }
set { SetValue(ChildHeightProperty, value); }
}
/// <summary>
/// Gets or sets the minimum width of each child.
/// </summary>
public double MinChildWidth
{
get { return (double)GetValue(MinChildWidthProperty); }
set { SetValue(MinChildWidthProperty, value); }
}
/// <summary>
/// Gets or sets the number of desired columns.
/// </summary>
public int Columns
{
get { return (int)GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
/// <summary>
/// Gets or sets whether the component is operating
/// in tile mode.If set to true, the component
/// will calulcate the number of children per row,
/// the width of each item is set equal to the height.
/// In this mode the Columns property is ignored. If the
/// setting is false, the component will calulate the
/// width of each item by dividing the available size
/// by the number of desired columns.
/// </summary>
public bool Tile
{
get { return (bool)GetValue(TileProperty); }
set { SetValue(TileProperty, value); }
}
当然,Dan 已经很好地进行了区域化的布局特定代码也已经修改了。
/// <summary>
/// Calculate the extent of the view based on the available size
/// </summary>
/// <param name="availableSize">available size</param>
/// <param name="itemCount">number of data items</param>
/// <returns>Returns the extent size of the viewer.</returns>
private Size CalculateExtent(Size availableSize, int itemCount)
{
//If tile mode.
if (Tile)
{
//Gets the number of children or items for each row.
int childrenPerRow = CalculateChildrenPerRow(availableSize);
// See how big we are
return new Size(childrenPerRow * (this.MinChildWidth > 0 ? this.MinChildWidth : this.ChildHeight),
this.ChildHeight * Math.Ceiling((double)itemCount / childrenPerRow));
}
else
{
//Gets the width of each child.
double childWidth = CalculateChildWidth(availableSize);
// See how big we are
return new Size(this.Columns * childWidth,
this.ChildHeight * Math.Ceiling((double)itemCount / this.Columns));
}
}
/// <summary>
/// Get the range of children that are visible
/// </summary>
/// <param name="firstVisibleItemIndex">The item index of the first visible item</param>
/// <param name="lastVisibleItemIndex">The item index of the last visible item</param>
void GetVisibleRange(out int firstVisibleItemIndex, out int lastVisibleItemIndex)
{
//If tile mode.
if (Tile)
{
//Get the number of children
int childrenPerRow = CalculateChildrenPerRow(_extent);
firstVisibleItemIndex =
(int)Math.Floor(_offset.Y / this.ChildHeight) * childrenPerRow;
lastVisibleItemIndex = (int)Math.Ceiling(
(_offset.Y + _viewport.Height) / this.ChildHeight) * childrenPerRow - 1;
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
if (lastVisibleItemIndex >= itemCount)
lastVisibleItemIndex = itemCount - 1;
}
else
{
firstVisibleItemIndex =
(int)Math.Floor(_offset.Y / this.ChildHeight) * this.Columns;
lastVisibleItemIndex =
(int)Math.Ceiling((_offset.Y + _viewport.Height) /
this.ChildHeight) * this.Columns - 1;
ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
if (lastVisibleItemIndex >= itemCount)
lastVisibleItemIndex = itemCount - 1;
}
}
/// <summary>
/// Get the size of the each child.
/// </summary>
/// <returns>The size of each child.</returns>
Size GetChildSize(Size availableSize)
{
if (Tile)
{
//Gets the number of children or items for each row.
int childrenPerRow = CalculateChildrenPerRow(availableSize);
return new Size(availableSize.Width / childrenPerRow, this.ChildHeight);
}
else
{
return new Size(CalculateChildWidth(availableSize), this.ChildHeight);
}
}
/// <summary>
/// Position a child
/// </summary>
/// <param name="itemIndex">The data item index of the child</param>
/// <param name="child">The element to position</param>
/// <param name="finalSize">The size of the panel</param>
void ArrangeChild(int itemIndex, UIElement child, Size finalSize)
{
if (Tile)
{
int childrenPerRow = CalculateChildrenPerRow(finalSize);
double childWidth = finalSize.Width / childrenPerRow;
int row = itemIndex / childrenPerRow;
int column = itemIndex % childrenPerRow;
child.Arrange(new Rect(column * childWidth, row * this.ChildHeight,
childWidth, this.ChildHeight)); }
else
{
//Get the width of each child.
double childWidth = CalculateChildWidth(finalSize);
int row = itemIndex / this.Columns;
int column = itemIndex % this.Columns;
child.Arrange(new Rect(column * childWidth, row * this.ChildHeight,
childWidth, this.ChildHeight));
}
}
/// <summary>
/// Calculate the width of each tile by
/// dividing the width of available size
/// by the number of required columns.
/// </summary>
/// <param name="availableSize">The total layout size available.</param>
/// <returns>The width of each tile.</returns>
double CalculateChildWidth(Size availableSize)
{
return availableSize.Width / this.Columns;
}
/// <summary>
/// Helper function for tiling layout
/// </summary>
/// <param name="availableSize">Size available</param>
/// <returns>The number of tiles on each row.</returns>
int CalculateChildrenPerRow(Size availableSize)
{
// Figure out how many children fit on each row
int childrenPerRow;
if (availableSize.Width == Double.PositiveInfinity)
childrenPerRow = this.Children.Count;
else
childrenPerRow = Math.Max(1, (int)Math.Floor(availableSize.Width / (this.MinChildWidth > 0 ? this.MinChildWidth : this.ChildHeight)));
return childrenPerRow;
}
关注点
就这样了。我确信有人会指出每个方法中代码的重复;肯定有改进的空间,可以重构为通用方法。
历史
- 2011-2-16:初始发布
- 2016-3-24:修复了计算逻辑中的一个小错误
- 2016-7-05:更改了现有的平铺逻辑,使其基于最小宽度工作。
false