WPF 列表虚拟化

@果酱  March 29, 2025

面试被问到了,应该是和Android的RecycleView一样的东西,对长列表,只渲染需要显的部分。作用于ListItem和TreeItem。

怎么实现没看明白,留份样板备用。

源码在这里:WPFControls

public class VirtualizingWrapPanel : VirtualizingPanel, IScrollInfo
{
    private TranslateTransform trans = new TranslateTransform();

    public VirtualizingWrapPanel()
    {
        this.RenderTransform = trans;
    }

    #region DependencyProperties
    public static readonly DependencyProperty ChildWidthProperty = DependencyProperty.RegisterAttached("ChildWidth", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(200.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

    public static readonly DependencyProperty ChildHeightProperty = DependencyProperty.RegisterAttached("ChildHeight", typeof(double), typeof(VirtualizingWrapPanel), new FrameworkPropertyMetadata(200.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

    //鼠标每一次滚动 UI上的偏移
    public static readonly DependencyProperty ScrollOffsetProperty = DependencyProperty.RegisterAttached("ScrollOffset", typeof(int), typeof(VirtualizingWrapPanel), new PropertyMetadata(10));

    public int ScrollOffset
    {
        get { return Convert.ToInt32(GetValue(ScrollOffsetProperty)); }
        set { SetValue(ScrollOffsetProperty, value); }
    }
    public double ChildWidth
    {
        get => Convert.ToDouble(GetValue(ChildWidthProperty));
        set => SetValue(ChildWidthProperty, value);
    }
    public double ChildHeight
    {
        get => Convert.ToDouble(GetValue(ChildHeightProperty));
        set => SetValue(ChildHeightProperty, value);
    }
    #endregion

    int GetItemCount(DependencyObject element)
    {
        var itemsControl = ItemsControl.GetItemsOwner(element);
        return itemsControl.HasItems ? itemsControl.Items.Count : 0;
    }
    int CalculateChildrenPerRow(Size availableSize)
    {
        int childPerRow = 0;
        if (availableSize.Width == double.PositiveInfinity)
            childPerRow = this.Children.Count;
        else
            childPerRow = Math.Max(1, Convert.ToInt32(Math.Floor(availableSize.Width / this.ChildWidth)));
        return childPerRow;
    }
    /// <summary>
    /// width不超过availableSize的情况下,自身实际需要的Size(高度可能会超出availableSize)
    /// </summary>
    /// <param name="availableSize"></param>
    /// <param name="itemsCount"></param>
    /// <returns></returns>
    Size CalculateExtent(Size availableSize, int itemsCount)
    {
        int childPerRow = CalculateChildrenPerRow(availableSize);//现有宽度下 一行可以最多容纳多少个
        return new Size(childPerRow * this.ChildWidth, this.ChildHeight * Math.Ceiling(Convert.ToDouble(itemsCount) / childPerRow));
    }
    /// <summary>
    /// 更新滚动条
    /// </summary>
    /// <param name="availableSize"></param>
    void UpdateScrollInfo(Size availableSize)
    {
        var extent = CalculateExtent(availableSize, GetItemCount(this));//extent 自己实际需要
        if (extent != this.extent)
        {
            this.extent = extent;
            this.ScrollOwner.InvalidateScrollInfo();
        }
        if (availableSize != this.viewPort)
        {
            this.viewPort = availableSize;
            this.ScrollOwner.InvalidateScrollInfo();
        }
    }
    /// <summary>
    /// 获取所有item,在可视区域内第一个item和最后一个item的索引
    /// </summary>
    /// <param name="firstIndex"></param>
    /// <param name="lastIndex"></param>
    void GetVisiableRange(ref int firstIndex, ref int lastIndex)
    {
        int childPerRow = CalculateChildrenPerRow(this.extent);
        firstIndex = Convert.ToInt32(Math.Floor(this.offset.Y / this.ChildHeight)) * childPerRow;
        lastIndex = Convert.ToInt32(Math.Ceiling((this.offset.Y + this.viewPort.Height) / this.ChildHeight)) * childPerRow - 1;
        int itemsCount = GetItemCount(this);
        if (lastIndex >= itemsCount)
            lastIndex = itemsCount - 1;

    }
    /// <summary>
    /// 将不在可视区域内的item 移除
    /// </summary>
    /// <param name="startIndex">可视区域开始索引</param>
    /// <param name="endIndex">可视区域结束索引</param>
    void CleanUpItems(int startIndex, int endIndex)
    {
        var children = this.InternalChildren;
        var generator = this.ItemContainerGenerator;
        for (int i = children.Count - 1; i >= 0; i--)
        {
            var childGeneratorPosi = new GeneratorPosition(i, 0);
            int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPosi);

            if (itemIndex < startIndex || itemIndex > endIndex)
            {

                generator.Remove(childGeneratorPosi, 1);
                RemoveInternalChildRange(i, 1);
            }
        }
    }
    /// <summary>
    /// scroll/availableSize/添加删除元素 改变都会触发  edit元素不会改变
    /// </summary>
    /// <param name="availableSize"></param>
    /// <returns></returns>
    protected override Size MeasureOverride(Size availableSize)
    {
        this.UpdateScrollInfo(availableSize);//availableSize更新后,更新滚动条
        int firstVisiableIndex = 0, lastVisiableIndex = 0;
        GetVisiableRange(ref firstVisiableIndex, ref lastVisiableIndex);//availableSize更新后,获取当前viewport内可放置的item的开始和结束索引  firstIdnex-lastIndex之间的item可能部分在viewport中也可能都不在viewport中。

        UIElementCollection children = this.InternalChildren;//因为配置了虚拟化,所以children的个数一直是viewport区域内的个数,如果没有虚拟化则是ItemSource的整个的个数
        IItemContainerGenerator generator = this.ItemContainerGenerator;
        //获得第一个可被显示的item的位置
        GeneratorPosition startPosi = generator.GeneratorPositionFromIndex(firstVisiableIndex);
        int childIndex = (startPosi.Offset == 0) ? startPosi.Index : startPosi.Index + 1;//startPosi在chilren中的索引
        using (generator.StartAt(startPosi, GeneratorDirection.Forward, true))
        {
            int itemIndex = firstVisiableIndex;
            while (itemIndex <= lastVisiableIndex)//生成lastVisiableIndex-firstVisiableIndex个item
            {
                bool newlyRealized = false;
                var child = generator.GenerateNext(out newlyRealized) as UIElement;
                if (newlyRealized)
                {
                    if (childIndex >= children.Count)
                        base.AddInternalChild(child);
                    else
                    {
                        base.InsertInternalChild(childIndex, child);
                    }
                    generator.PrepareItemContainer(child);
                }
                else
                {
                    //处理 正在显示的child被移除了这种情况
                    if (!child.Equals(children[childIndex]))
                    {
                        base.RemoveInternalChildRange(childIndex, 1);
                    }
                }
                child.Measure(new Size(this.ChildWidth, this.ChildHeight));
                //child.DesiredSize//child想要的size
                itemIndex++;
                childIndex++;
            }
        }
        CleanUpItems(firstVisiableIndex, lastVisiableIndex);
        return new Size(double.IsInfinity(availableSize.Width) ? 0 : availableSize.Width, double.IsInfinity(availableSize.Height) ? 0 : availableSize.Height);//自身想要的size
    }
    protected override Size ArrangeOverride(Size finalSize)
    {
        Debug.WriteLine("----ArrangeOverride");
        var generator = this.ItemContainerGenerator;
        UpdateScrollInfo(finalSize);
        int childPerRow = CalculateChildrenPerRow(finalSize);
        double availableItemWidth = finalSize.Width / childPerRow;
        for (int i = 0; i <= this.Children.Count - 1; i++)
        {
            var child = this.Children[i];
            int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));
            int row = itemIndex / childPerRow;//current row
            int column = itemIndex % childPerRow;
            double xCorrdForItem = 0;

            xCorrdForItem = column * availableItemWidth + (availableItemWidth - this.ChildWidth) / 2;

            Rect rec = new Rect(xCorrdForItem, row * this.ChildHeight, this.ChildWidth, this.ChildHeight);
            child.Arrange(rec);
        }
        return finalSize;
    }
    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
    {
        base.OnRenderSizeChanged(sizeInfo);
        this.SetVerticalOffset(this.VerticalOffset);
    }
    protected override void OnClearChildren()
    {
        base.OnClearChildren();
        this.SetVerticalOffset(0);
    }
    protected override void BringIndexIntoView(int index)
    {
        if (index < 0 || index >= Children.Count)
            throw new ArgumentOutOfRangeException();
        int row = index / CalculateChildrenPerRow(RenderSize);
        SetVerticalOffset(row * this.ChildHeight);
    }
    #region IScrollInfo Interface
    public bool CanVerticallyScroll { get; set; }
    public bool CanHorizontallyScroll { get; set; }

    private Size extent = new Size(0, 0);
    public double ExtentWidth => this.extent.Width;

    public double ExtentHeight => this.extent.Height;

    private Size viewPort = new Size(0, 0);
    public double ViewportWidth => this.viewPort.Width;

    public double ViewportHeight => this.viewPort.Height;

    private Point offset;
    public double HorizontalOffset => this.offset.X;

    public double VerticalOffset => this.offset.Y;

    public ScrollViewer ScrollOwner { get; set; }

    public void LineDown()
    {
        this.SetVerticalOffset(this.VerticalOffset + this.ScrollOffset);
    }

    public void LineLeft()
    {
        throw new NotImplementedException();
    }

    public void LineRight()
    {
        throw new NotImplementedException();
    }

    public void LineUp()
    {
        this.SetVerticalOffset(this.VerticalOffset - this.ScrollOffset);
    }

    public Rect MakeVisible(Visual visual, Rect rectangle)
    {
        return new Rect();
    }

    public void MouseWheelDown()
    {
        this.SetVerticalOffset(this.VerticalOffset + this.ScrollOffset);
    }

    public void MouseWheelLeft()
    {
        throw new NotImplementedException();
    }

    public void MouseWheelRight()
    {
        throw new NotImplementedException();
    }

    public void MouseWheelUp()
    {
        this.SetVerticalOffset(this.VerticalOffset - this.ScrollOffset);
    }

    public void PageDown()
    {
        this.SetVerticalOffset(this.VerticalOffset + this.viewPort.Height);
    }

    public void PageLeft()
    {
        throw new NotImplementedException();
    }

    public void PageRight()
    {
        throw new NotImplementedException();
    }

    public void PageUp()
    {
        this.SetVerticalOffset(this.VerticalOffset - this.viewPort.Height);
    }

    public void SetHorizontalOffset(double offset)
    {
        throw new NotImplementedException();
    }

    public void SetVerticalOffset(double offset)
    {
        if (offset < 0 || this.viewPort.Height >= this.extent.Height)
            offset = 0;
        else
            if (offset + this.viewPort.Height >= this.extent.Height)
            offset = this.extent.Height - this.viewPort.Height;

        this.offset.Y = offset;
        this.ScrollOwner?.InvalidateScrollInfo();
        this.trans.Y = -offset;
        this.InvalidateMeasure();
        //接下来会触发MeasureOverride()
    }
    #endregion
}


添加新评论

  1. 新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

    Reply
  2. 做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com

    Reply
  3. 新项目准备上线,寻找志同道合的合作伙伴coinsrore.com

    Reply
  4. 2025年10月新盘 做第一批吃螃蟹的人

    Reply
  5. 2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
    新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
    新车首发,新的一年,只带想赚米的人coinsrore.com
    新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
    做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
    新车上路,只带前10个人coinsrore.com
    新盘首开 新盘首开 征召客户!!!coinsrore.com
    新项目准备上线,寻找志同道合的合作伙伴coinsrore.com
    新车即将上线 真正的项目,期待你的参与coinsrore.com
    新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
    新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

    Reply
  6. 2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
    新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
    新车首发,新的一年,只带想赚米的人coinsrore.com
    新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
    做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
    新车上路,只带前10个人coinsrore.com
    新盘首开 新盘首开 征召客户!!!coinsrore.com
    新项目准备上线,寻找志同道合的合作伙伴coinsrore.com
    新车即将上线 真正的项目,期待你的参与coinsrore.com
    新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
    新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

    Reply
  7. 2025年10月新盘 做第一批吃螃蟹的人coinsrore.com
    新车新盘 嘎嘎稳 嘎嘎靠谱coinsrore.com
    新车首发,新的一年,只带想赚米的人coinsrore.com
    新盘 上车集合 留下 我要发发 立马进裙coinsrore.com
    做了几十年的项目 我总结了最好的一个盘(纯干货)coinsrore.com
    新车上路,只带前10个人coinsrore.com
    新盘首开 新盘首开 征召客户!!!coinsrore.com
    新项目准备上线,寻找志同道合 的合作伙伴coinsrore.com
    新车即将上线 真正的项目,期待你的参与coinsrore.com
    新盘新项目,不再等待,现在就是最佳上车机会!coinsrore.com
    新盘新盘 这个月刚上新盘 新车第一个吃螃蟹!coinsrore.com

    Reply
  8. hello

    Reply
  9. 华纳企业开户所需材料?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】

    Reply
  10. 华纳东方明珠客服热线?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】

    Reply
  11. 华纳东方明珠客服电话是多少?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】
    华纳东方明珠开户专线联系方式?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】
    如何联系华纳东方明珠客服?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】
    华纳东方明珠官方客服联系方式?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】
    华纳东方明珠客服热线?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】
    华纳东方明珠开户客服电话?(▲182(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】
    华纳东方明珠24小时客服电话?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】
    华纳东方明珠客服邮箱?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】
    华纳东方明珠官方客服在线咨询?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】
    华纳东方明珠客服微信?(▲18288362750?《?微信STS5099? 】【╃q 2704132802╃】

    Reply
  12. 华纳东方明珠客服电话是多少?(▲18288362750?《?微信STS5099? 】
    如何联系华纳东方明珠客服?(▲18288362750?《?微信STS5099? 】
    华纳东方明珠官方客服联系方式?(▲18288362750?《?微信STS5099?
    华纳东方明珠客服热线?(▲18288362750?《?微信STS5099?
    华纳东方明珠24小时客服电话?(▲18288362750?《?微信STS5099? 】
    华纳东方明珠官方客服在线咨询?(▲18288362750?《?微信STS5099?

    Reply
  13. 华纳东方明珠开户专线联系方式?(?183-8890--9465—《?薇-STS5099】【?扣6011643??】

    Reply
  14. 东方明珠客服开户联系方式【182-8836-2750—】?μ- cxs20250806
    东方明珠客服电话联系方式【182-8836-2750—】?- cxs20250806】
    东方明珠开户流程【182-8836-2750—】?薇- cxs20250806】
    东方明珠客服怎么联系【182-8836-2750—】?薇- cxs20250806】

    Reply
  15. 华纳圣淘沙开户步骤详解(183-8890-9465—?薇-STS5099【6011643】

    华纳圣淘沙公司开户流程全解析(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙公司账户注册指南(183-8890-9465—?薇-STS5099【6011643】
    新手如何开通华纳圣淘沙公司账户(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙企业开户标准流程(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙公司开户:从零到一(183-8890-9465—?薇-STS5099【6011643】
    官方指南:华纳圣淘沙公司开户流程(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙公司开户流程说明书(183-8890-9465—?薇-STS5099【6011643】

    Reply
  16. 华纳圣淘沙开户步骤详解(183-8890-9465—?薇-STS5099【6011643】

    华纳圣淘沙公司开户流程全解析(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙公司账户注册指南(183-8890-9465—?薇-STS5099【6011643】
    新手如何开通华纳圣淘沙公司账户(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙企业开户标准流程(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙公司开户:从零到一(183-8890-9465—?薇-STS5099【6011643】
    官方指南:华纳圣淘沙公司开户流程(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙公司开户流程说明书(183-8890-9465—?薇-STS5099【6011643】

    Reply
  17. 华纳圣淘沙开户步骤详解(183-8890-9465—?薇-STS5099【6011643】

    华纳圣淘沙公司开户流程全解析(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙公司账户注册指南(183-8890-9465—?薇-STS5099【6011643】
    新手如何开通华纳圣淘沙公司账户(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙企业开户标准流程(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙公司开户:从零到一(183-8890-9465—?薇-STS5099【6011643】
    官方指南:华纳圣淘沙公司开户流程(183-8890-9465—?薇-STS5099【6011643】
    华纳圣淘沙公司开户流程说明书(183-8890-9465—?薇-STS5099【6011643】

    Reply