65.9K
CodeProject 正在变化。 阅读更多。
Home

Dan Crevier 的 VirtualizingTilePanel 修改版

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (7投票s)

2011年2月15日

CPOL

2分钟阅读

viewsIcon

37098

downloadIcon

1227

修改 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

© . All rights reserved.