Click here to Skip to main content
15,884,388 members
Articles / Desktop Programming / WPF

Dan Crevier's VirtualizingTilePanel Mod

Rate me:
Please Sign up or sign in to vote.
4.80/5 (7 votes)
4 Jul 2016CPOL2 min read 35.5K   1.2K   10   9
Modification of Dan Crevier's VirtualizingTilePanel to fill the total area

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 <font face="Courier New">Tile</font> property specifies which layout logic to execute. Set to <font face="Courier New">true</font> 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.

C#
/// <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); }
}

And the layout specific code, which Dan had very nicely regionalised, has of course been modified.

C#
/// <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;
}

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Ireland Ireland
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionOnItemsChanged Pin
dieguito8318-Sep-20 15:35
dieguito8318-Sep-20 15:35 
Hello,

The control raises an exception when I remove an item from the associated Items source collection; to get it work I had to make the following change:

protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
       {
           switch (args.Action)
           {
               //case NotifyCollectionChangedAction.Remove:
               case NotifyCollectionChangedAction.Replace:
               case NotifyCollectionChangedAction.Reset:
                   //Peform layout refreshment.
                   if (_owner != null)
                   {
                       _owner.ScrollToTop();
                   }
                   break;
               case NotifyCollectionChangedAction.Remove:
               case NotifyCollectionChangedAction.Move:
                   RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
                   break;
           }
       }


In general the controls works pretty well Smile | :)
QuestionScollIntoView does not work Pin
LumoWIT3-Mar-19 22:27
LumoWIT3-Mar-19 22:27 
Questionkeyboard navigation Pin
springy762-Jan-12 2:54
springy762-Jan-12 2:54 
AnswerRe: keyboard navigation Pin
NobsterTheLobster2-Jan-12 4:05
NobsterTheLobster2-Jan-12 4:05 
GeneralRe: keyboard navigation Pin
springy762-Jan-12 5:37
springy762-Jan-12 5:37 
GeneralRe: keyboard navigation Pin
springy7613-Jun-12 0:40
springy7613-Jun-12 0:40 
QuestionSystem.InvalidOperationException Pin
myCollections28-Oct-11 4:00
myCollections28-Oct-11 4:00 
AnswerRe: System.InvalidOperationException Pin
NobsterTheLobster28-Oct-11 23:50
NobsterTheLobster28-Oct-11 23:50 
GeneralRe: System.InvalidOperationException Pin
myCollections30-Oct-11 22:09
myCollections30-Oct-11 22:09 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.