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 (technically the height) of each child item in the panel and the algorithm calculates how many children can occupy a row in the available space.
I've tweaked the logic so it removes the empty space; the number of items that will fit on a row is based on a provided MinChildWidth property. The final width of each item is then calculated based on available width divided by the number of items that could potentially fit. This logic removes the ugly gap the occurs when items spill over to new rows and keeps the great wrapping behaviour.
Please note that In previous versions of this mod you had to provide the number of columns you want from the outset and the width of items was then determined accordingly. However I realised much later that this logic essentially breaks the wrapping behaviour and is therefore undesirable If you want the items to reflow according to real estate. Regardless to maintain backwards compatibilty I have left the new logic intact.
Using the Code
I'll just describe the changes from Dan's article. Three new dependency properties have been added.
The Tile
property specifies which layout logic to execute. Set to true
to use Dan's modified logic (Recommended) or false to use the changed logic.
If using the new logic the Columns
property sets the number of desired children on a row. This property is only applicable when the Tile property is false.
The ChildSize
property has been renamed to ChildHeight.
The MinChildWidth property indicates the minium desired width of each child. This property is only applicable when the Tile property is true.
public static readonly DependencyProperty ChildHeightProperty
= DependencyProperty.RegisterAttached("ChildHeight",
typeof(double), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(200.0d,
FrameworkPropertyMetadataOptions.AffectsMeasure |
FrameworkPropertyMetadataOptions.AffectsArrange));
public static readonly DependencyProperty MinChildWidthProperty
= DependencyProperty.RegisterAttached("MinChildWidth", typeof(double), typeof(VirtualizingTilePanel),
new FrameworkPropertyMetadata(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 double MinChildWidth
{
get { return (double)GetValue(MinChildWidthProperty); }
set { SetValue(MinChildWidthProperty, 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.MinChildWidth > 0 ? this.MinChildWidth : 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
{
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;
}
}
Size GetChildSize(Size availableSize)
{
if (Tile)
{
int childrenPerRow = CalculateChildrenPerRow(availableSize);
return new Size(availableSize.Width / childrenPerRow, this.ChildHeight);
}
else
{
return new Size(CalculateChildWidth(availableSize), this.ChildHeight);
}
}
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
{
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.MinChildWidth > 0 ? this.MinChildWidth : 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
- 2016-3-24: Minor fix to calculation logic
- 2016-7-05: Changed existing tile logic to work off min width.
false