Introduction
This is barely worth an article but I'm posting it anyway because hopefully it will save time for others trying to implement the same mechanism. Dan Crevier has a wonderful post about how to implement a virtualizing tile/wrap panel in WPF (link). 99% of the code in this article belongs to him. My one issue with his implementation was that his layout logic can leave ugly empty space at the side of the items control. With Dan's logic, you specify the width of each child item in the panel and the algorithm calculates how many children can occupy a row in the available space. With my logic, you specify the number of children desired in each row and the algorithm calculates the width of each child.
Using the Code
I'll just describe the changes from Dan's article. Two new dependency properties have been added. The Columns property sets the number of desired children on a row. The Tile property specifies which layout logic to execute. Set to true to use Dan's original logic, the Columns property if specified will be ignored in this case; otherwise set to false to use the changed logic.
The ChildSize property has been renamed to ChildHeight simply because this name was less confusing than ChildSize when writing the new logic; as under the new code, it is actually only the height component that this property represents. Note that when using the original logic, this property still represents both the width and the height.
public static readonly DependencyProperty ChildHeightProperty
= DependencyProperty.RegisterAttached("ChildHeight",
typeof(double), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(200.0d,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
public static readonly DependencyProperty ColumnsProperty
= DependencyProperty.RegisterAttached("Columns",
typeof(int), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(10,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
public static readonly DependencyProperty TileProperty
= DependencyProperty.RegisterAttached("Tile",
typeof(bool), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(true,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
public double ChildHeight
{
get { return (double)GetValue(ChildHeightProperty); }
set { SetValue(ChildHeightProperty, value); }
}
public int Columns
{
get { return (int)GetValue(ColumnsProperty); }
set { SetValue(ColumnsProperty, value); }
}
public bool Tile
{
get { return (bool)GetValue(TileProperty); }
set { SetValue(TileProperty, value); }
}
And the layout specific code, which Dan had very nicely regionalised, has of course been modified.
private Size CalculateExtent(Size availableSize, int itemCount)
{
if (Tile)
{
int childrenPerRow = CalculateChildrenPerRow(availableSize);
return new Size(childrenPerRow * this.ChildHeight,
this.ChildHeight * Math.Ceiling((double)itemCount / childrenPerRow));
}
else
{
double childWidth = CalculateChildWidth(availableSize);
return new Size(this.Columns * childWidth,
this.ChildHeight * Math.Ceiling((double)itemCount / this.Columns));
}
}
void GetVisibleRange(out int firstVisibleItemIndex, out int lastVisibleItemIndex)
{
if (Tile)
{
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
{
double childWidth = CalculateChildWidth(_extent);
firstVisibleItemIndex =
(int)Math.Floor(_offset.Y / childWidth) * 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;
}
}
Size GetChildSize(Size availableSize)
{
return new Size((Tile) ? this.ChildHeight :
CalculateChildWidth(availableSize), this.ChildHeight);
}
void ArrangeChild(int itemIndex, UIElement child, Size finalSize)
{
if (Tile)
{
int childrenPerRow = CalculateChildrenPerRow(finalSize);
int row = itemIndex / childrenPerRow;
int column = itemIndex % childrenPerRow;
child.Arrange(new Rect(column * this.ChildHeight,
row * this.ChildHeight,
this.ChildHeight, this.ChildHeight));
}
else
{
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));
}
}
double CalculateChildWidth(Size availableSize)
{
return availableSize.Width / this.Columns;
}
int CalculateChildrenPerRow(Size availableSize)
{
int childrenPerRow;
if (availableSize.Width == Double.PositiveInfinity)
childrenPerRow = this.Children.Count;
else
childrenPerRow = Math.Max(1, (int)Math.Floor(
availableSize.Width / this.ChildHeight));
return childrenPerRow;
}
Points of Interest
That's all there is to it. I'm sure some-one or some-all will point out the duplication of code within each method; definitely there is room to refactor into generic methods.
History
- 2011-2-16: Initial release.