WPF中實現自定義虛擬容器(實現VirtualizingPanel)

 WPF中實現自定義虛擬容器(實現VirtualizingPanel)html

 

在WPF應用程序開發過程當中,大數據量的數據展示一般都要考慮性能問題。有下面一種常見的狀況:原始數據源數據量很大,可是某一時刻數據容器中的可見元素個數是有限的,剩餘大多數元素都處於不可見狀態,若是一次性將全部的數據元素都渲染出來則會很是的消耗性能。於是能夠考慮只渲染當前可視區域內的元素,當可視區域內的元素須要發生改變時,再渲染即將展示的元素,最後將再也不須要展示的元素清除掉,這樣能夠大大提升性能。在WPF中System.Windows.Controls命名空間下的VirtualizingStackPanel能夠實現數據展示的虛擬化功能,ListBox的默認元素展示容器就是它。但有時VirtualizingStackPanel的佈局並不能知足咱們的實際須要,此時就須要實現自定義佈局的虛擬容器了。本文將簡單介紹容器自定義佈局,而後介紹實現虛擬容器的基本原理,最後給出一個虛擬化分頁容器的演示程序。 編程

 

1、WPF中自定義佈局 (已瞭解容器自定義佈局的朋友可略過此節)

 

一般實現一個自定義佈局的容器,須要繼承System.Windows.Controls.Panel, 並重寫下面兩個方法:windows

MeasureOverride —— 用來測量子元素指望的佈局尺寸ide

ArrangeOverride —— 用來安排子元素在容器中的佈局。佈局

 

下面用一個簡單的SplitPanel來加以說明這兩個方法的做用。下面的Window中放置了一個SplitPanel,每點擊一次「添加」按鈕,都會向SplitPanel中添加一個填充了隨機色的Rectangle, 而SplitPanel中的Rectangle不管有幾個,都會在垂直方向上佈滿容器,水平方向上平均分配寬度。性能

2012090619124827

實現代碼以下:大數據

 

SplitPanel 
/// <summary>
/// 簡單的自定義容器
/// 子元素在垂直方向佈滿容器,水平方向平局分配容器寬度
/// </summary>
public class SplitPanel : Panel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        foreach (UIElement child in InternalChildren)
        {
            child.Measure(availableSize);   // 測量子元素指望佈局尺寸(child.DesiredSize)
        }

        return base.MeasureOverride(availableSize);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        if (double.IsInfinity(finalSize.Height) || double.IsInfinity(finalSize.Width))
        {
            throw new InvalidOperationException("容器的寬和高必須是肯定值");
        }

        if (Children.Count > 0)
        {
            double childAverageWidth = finalSize.Width / Children.Count;
            for (int childIndex = 0; childIndex < InternalChildren.Count; childIndex++)
            {
                // 計算子元素將被安排的佈局區域
                var rect = new Rect(childIndex * childAverageWidth, 0, childAverageWidth, finalSize.Height);
                InternalChildren[childIndex].Arrange(rect);
            }
        }

        return base.ArrangeOverride(finalSize);
    }
}
SplitPanel

 

SplitPanel的MeasureOverride 方法參數availableSize是容器能夠給出的總佈局大小,在方法體中只依次調用了子元素的Measure方法,調用該方法後,子元素的DesiredSize屬性就會被賦值, 該屬性指明瞭子元素指望的佈局尺寸。(在SplitPanel中並不須要知道子元素的指望佈局尺寸,因此能夠沒必要重寫MeasureOverride 方法,可是在一些比較複雜的佈局中須要用到子元素的DesiredSize屬性時就必須重寫)this

SplitPaneld的ArrangeOverride 方法參數finalSize是容器最終給出的佈局大小,26行根據子元素個數先計算出子元素平均寬度,30行再按照子元素索引計算出各自的佈局區域信息。而後31行調用子元素的Arrange方法將子元素安排在容器中的合適位置。這樣就能夠實現指望的佈局效果。當UI重繪時(例如子元素個數發生改變、容器佈局尺寸發生改變、強制刷新UI等),會從新執行MeasureOverride 和ArrangeOverride 方法。 

spa

2、虛擬容器原理

要想實現一個虛擬容器,並讓虛擬容器正常工做,必須知足如下兩個條件:3d

一、容器繼承自System.Windows.Controls.VirtualizingPanel,並實現子元素的實例化、虛擬化及佈局處理。

二、虛擬容器要作爲一個System.Windows.Controls.ItemsControl(或繼承自ItemsControl的類)實例的ItemsPanel(其實是定義一個ItemsPanelTemplate) 

 

下面咱們先來了解一下ItemsControl的工做機制:

當咱們爲一個ItemsControl指定了ItemsSource屬性後,ItemsControl的Items屬性就會被初始化,這裏面裝的就是原始的數據(題外話:經過修改Items的Filter能夠實現不切換數據源的元素過濾,修改Items的SortDescriptions屬性能夠實現不切換數據源的元素排序)。以後ItemsControl會根據Items來生成子元素的容器(ItemsControl生成ContentPresenter, ListBox生成ListBoxItem, ComboBox生成ComboBox等等),同時將子元素容器的DataContext設置爲與之對應的數據源,最後每一個子元素容器再根據ItemTemplate的定義來渲染子元素實際顯示效果。

對於Panel來講,ItemsControl會一次性生成全部子元素的子元素容器並進行數據初始化,這樣就致使在數據量較大時性能會不好。而對於VirtualizingPanel,ItemsControl則不會自動生成子元素容器及子元素的渲染,這一過程須要編程實現。 

接下來咱們引入另外一個重要概念:GeneratorPosition,這個結構體用來描述ItemsControl的Items屬性中實例化和虛擬化數據項的位置關係,在VirtualizingPanel中能夠經過ItemContainerGenerator(注意:在VirtualizingPanel第一次訪問這個屬性以前要先訪問一下InternalChildren屬性,不然ItemContainerGenerator會是null,貌似是一個Bug)屬性來獲取數據項的位置信息,此外經過這個屬性還能夠進行數據項的實例化和虛擬化。

獲取數據項GeneratorPosition信息:

 

DumpGeneratorContent
/// <summary>
/// 顯示數據GeneratorPosition信息
/// </summary>
public void DumpGeneratorContent()
{
    IItemContainerGenerator generator = this.ItemContainerGenerator;
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);

    Console.WriteLine("Generator positions:");
    for (int i = 0; i < itemsControl.Items.Count; i++)
    {
        GeneratorPosition position = generator.GeneratorPositionFromIndex(i);
        Console.WriteLine("Item index=" + i + ", Generator position: index=" + position.Index + ", offset=" + position.Offset);
    }
    Console.WriteLine();
}
DumpGeneratorContent

 

第7行經過ItemsControl的靜態方法GetItemsOwner能夠找到容器所在的ItemsControl,這樣就能夠訪問到數據項集合,第12行代碼調用generator 的GeneratorPositionFromIndex方法,經過數據項的索引獲得數據項的GeneratorPosition 信息。

 
數據項實例化:
 
/// <summary>
/// 實例化子元素
/// </summary>
/// <param name="itemIndex">數據條目索引</param>
public void RealizeChild(int itemIndex)
{
    IItemContainerGenerator generator = this.ItemContainerGenerator;
    GeneratorPosition position = generator.GeneratorPositionFromIndex(itemIndex);

    using (generator.StartAt(position, GeneratorDirection.Forward, allowStartAtRealizedItem: true))
    {
        bool isNewlyRealized;
        var child = (UIElement)generator.GenerateNext(out isNewlyRealized); // 實例化(構造出空的子元素UI容器)

        if (isNewlyRealized)
        {
            generator.PrepareItemContainer(child); // 填充UI容器數據
        }
    }
}

 

第10行調用generator 的StartAt方法肯定準備實例化元素的數據項位置,第13行調用generator的GenerateNext方法進行數據項的實例化,輸出參數isNewlyRealized爲ture則代表該元素是從虛擬化狀態實例化出來的,false則代表該元素已被實例化。注意,該方法只是構造出了子元素的UI容器,只有調用了17行的PrepareItemContainer方法,UI容器的實際內容纔會根據ItemsControl的ItemTemplate定義進行渲染。

 
數據項虛擬化:
 
VirtualizeChild
/// <summary>
/// 虛擬化子元素
/// </summary>
/// <param name="itemIndex">數據條目索引</param>
public void VirtualizeChild(int itemIndex)
{
    IItemContainerGenerator generator = this.ItemContainerGenerator;
    var childGeneratorPos = generator.GeneratorPositionFromIndex(itemIndex);
    if (childGeneratorPos.Offset == 0)
    {
        generator.Remove(childGeneratorPos, 1); // 虛擬化(從子元素UI容器中清除數據)
    }
}
VirtualizeChild

 

經過數據條目索引得出GeneratorPosition 信息,以後在11行調用generator的Remove方法便可實現元素的虛擬化。

 
經過幾張圖片來有一個直觀的認識,數據條目一共有10個,初始化時所有都爲虛擬化狀態:
1
 
實例化第二個元素:
2
 
增長實例化第3、七個元素:
3
 
虛擬化第二個元素:
4
經過觀察能夠發現, 實例化的數據項位置信息按順序從0開始依次增長,全部實例化的數據項位置信息的offset屬性都是0,虛擬化數據項index和前一個最近的實例化元素index保持一致,offset依次增長
 

3、實戰-實現一個虛擬化分頁容器

瞭解了子元素自定義佈局、數據項GeneratorPosition信息、虛擬化、實例化相關概念和實現方法後,離實現一個自定義虛擬容器還剩一步重要的工做:計算當前應該顯示的數據項起止索引,實例化這些數據項,虛擬化再也不顯示的數據項。

再前進一步,實現一個虛擬化分頁容器:

5

這個虛擬化分頁容器有ChildWidth和ChildHeight兩個依賴屬性,用來定義容器中子元素的寬和高,這樣在容器佈局尺寸肯定的狀況下能夠計算出可用佈局下一共能顯示多少個子元素,也就是PageSize屬性。爲容器指定一個有5000個數據的數據源,再提供一個分頁控件用來控制分頁容器的PageIndex,用來達到分頁顯示的效果。

貼出主要代碼:

 

計算須要實例化數據項的起止索引
/// <summary>
/// 計算但是元素起止索引
/// </summary>
/// <param name="availableSize">可用佈局尺寸</param>
/// <param name="firstVisibleChildIndex">第一個顯示的子元素索引</param>
/// <param name="lastVisibleChildIndex">最後一個顯示的子元素索引</param>
private void ComputeVisibleChildIndex(Size availableSize, out int firstVisibleChildIndex, out int lastVisibleChildIndex)
{
    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);

    if (itemsControl != null && itemsControl.Items != null && ChildWidth > 0 && ChildHeight > 0)
    {
        ChildrenCount = itemsControl.Items.Count;

        _horizontalChildMaxCount = (int)(availableSize.Width / ChildWidth);
        _verticalChildMaxCount = (int)(availableSize.Height / ChildHeight);

        PageSize = _horizontalChildMaxCount * _verticalChildMaxCount;

        // 計算子元素顯示起止索引
        firstVisibleChildIndex = PageIndex * PageSize;
        lastVisibleChildIndex = Math.Min(ChildrenCount, firstVisibleChildIndex + PageSize) - 1;

        Debug.WriteLine("firstVisibleChildIndex:{0}, lastVisibleChildIndex{1}", firstVisibleChildIndex, lastVisibleChildIndex)
    }
    else
    {
        ChildrenCount = 0;
        firstVisibleChildIndex = -1;
        lastVisibleChildIndex = -1;
        PageSize = 0;
    }
}
計算須要實例化數據項的起止索引
測量子元素佈局指望尺寸及數據項實例化
/// <summary>
/// 測量子元素佈局,生成須要顯示的子元素
/// </summary>
/// <param name="availableSize">可用佈局尺寸</param>
/// <param name="firstVisibleChildIndex">第一個顯示的子元素索引</param>
/// <param name="lastVisibleChildIndex">最後一個顯示的子元素索引</param>
private void MeasureChild(Size availableSize, int firstVisibleChildIndex, int lastVisibleChildIndex)
{
    if (firstVisibleChildIndex < 0)
    {
        return;
    }

    // 注意,在第一次使用 ItemContainerGenerator以前要先訪問一下InternalChildren, 
    // 不然ItemContainerGenerator爲null,是一個Bug
    UIElementCollection children = InternalChildren;
    IItemContainerGenerator generator = ItemContainerGenerator;

    // 獲取第一個可視元素位置信息
    GeneratorPosition position = generator.GeneratorPositionFromIndex(firstVisibleChildIndex);
    // 根據元素位置信息計算子元素索引
    int childIndex = position.Offset == 0 ? position.Index : position.Index + 1;

    using (generator.StartAt(position, GeneratorDirection.Forward, true))
    {
        for (int itemIndex = firstVisibleChildIndex; itemIndex <= lastVisibleChildIndex; itemIndex++, childIndex++)
        {
            bool isNewlyRealized;   // 用以指示新生成的元素是不是新實體化的

            // 生成下一個子元素
            var child = (UIElement)generator.GenerateNext(out isNewlyRealized);

            if (isNewlyRealized)
            {
                if (childIndex >= children.Count)
                {
                    AddInternalChild(child);
                }
                else
                {
                    InsertInternalChild(childIndex, child);
                }
                generator.PrepareItemContainer(child);
            }

            // 測算子元素佈局
            child.Measure(availableSize);
        }
    }
}
View Code

 

清理再也不顯示的子元素
/// <summary>
/// 清理不須要顯示的子元素
/// </summary>
/// <param name="firstVisibleChildIndex">第一個顯示的子元素索引</param>
/// <param name="lastVisibleChildIndex">最後一個顯示的子元素索引</param>
private void CleanUpItems(int firstVisibleChildIndex, int lastVisibleChildIndex)
{
    UIElementCollection children = this.InternalChildren;
    IItemContainerGenerator generator = this.ItemContainerGenerator;

    // 清除不須要顯示的子元素,注意從集合後向前操做,以避免形成操做過程當中元素索引起生改變
    for (int i = children.Count - 1; i > -1; i--)
    {
        // 經過已顯示的子元素的位置信息得出元素索引
        var childGeneratorPos = new GeneratorPosition(i, 0);
        int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos);

        // 移除再也不顯示的元素
        if (itemIndex < firstVisibleChildIndex || itemIndex > lastVisibleChildIndex)
        {
            generator.Remove(childGeneratorPos, 1);
            RemoveInternalChildRange(i, 1);
        }
    }
}
清理再也不顯示的子元素

 

 

 

本文章摘自: http://www.cnblogs.com/talywy/archive/2012/09/07/CustomVirtualizingPanel.html  很是棒,很是感謝博主

相關文章
相關標籤/搜索