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


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

 

一、WPF中自定義布局 (已了解容器自定義布局的朋友可略過此節)

通常實現一個自定義布局的容器,需要繼承System.Windows.Controls.Panel, 並重寫下面兩個方法:

MeasureOverride —— 用來測量子元素期望的布局尺寸

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

 

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

2012090619124827

實現代碼如下:

SplitPanel
 1 /// <summary>
 2 /// 簡單的自定義容器
 3 /// 子元素在垂直方向布滿容器,水平方向平局分配容器寬度
 4 /// </summary>
 5 public class SplitPanel : Panel
 6 {
 7     protected override Size MeasureOverride(Size availableSize)
 8     {
 9         foreach (UIElement child in InternalChildren)
10         {
11             child.Measure(availableSize);   // 測量子元素期望布局尺寸(child.DesiredSize)
12         }
13 
14         return base.MeasureOverride(availableSize);
15     }
16 
17     protected override Size ArrangeOverride(Size finalSize)
18     {
19         if (double.IsInfinity(finalSize.Height) || double.IsInfinity(finalSize.Width))
20         {
21             throw new InvalidOperationException("容器的寬和高必須是確定值");
22         }
23 
24         if (Children.Count > 0)
25         {
26             double childAverageWidth = finalSize.Width / Children.Count;
27             for (int childIndex = 0; childIndex < InternalChildren.Count; childIndex++)
28             {
29                 // 計算子元素將被安排的布局區域
30                 var rect = new Rect(childIndex * childAverageWidth, 0, childAverageWidth, finalSize.Height);
31                 InternalChildren[childIndex].Arrange(rect);
32             }
33         }
34 
35         return base.ArrangeOverride(finalSize);
36     }
37 }

SplitPanel的MeasureOverride 方法參數availableSize是容器可以給出的總布局大小,在方法體中只依次調用了子元素的Measure方法,調用該方法后,子元素的DesiredSize屬性就會被賦值, 該屬性指明了子元素期望的布局尺寸。(在SplitPanel中並不需要知道子元素的期望布局尺寸,所以可以不必重寫MeasureOverride 方法,但是在一些比較復雜的布局中需要用到子元素的DesiredSize屬性時就必須重寫)

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

二、虛擬容器原理

要想實現一個虛擬容器,並讓虛擬容器正常工作,必須滿足以下兩個條件:

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

2、虛擬容器要做為一個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
 1 /// <summary>
 2 /// 顯示數據GeneratorPosition信息
 3 /// </summary>
 4 public void DumpGeneratorContent()
 5 {
 6     IItemContainerGenerator generator = this.ItemContainerGenerator;
 7     ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
 8 
 9     Console.WriteLine("Generator positions:");
10     for (int i = 0; i < itemsControl.Items.Count; i++)
11     {
12         GeneratorPosition position = generator.GeneratorPositionFromIndex(i);
13         Console.WriteLine("Item index=" + i + ", Generator position: index=" + position.Index + ", offset=" + position.Offset);
14     }
15     Console.WriteLine();
16 }

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

 
數據項實例化:
RealizeChild
 1 /// <summary>
 2 /// 實例化子元素
 3 /// </summary>
 4 /// <param name="itemIndex">數據條目索引</param>
 5 public void RealizeChild(int itemIndex)
 6 {
 7     IItemContainerGenerator generator = this.ItemContainerGenerator;
 8     GeneratorPosition position = generator.GeneratorPositionFromIndex(itemIndex);
 9 
10     using (generator.StartAt(position, GeneratorDirection.Forward, allowStartAtRealizedItem: true))
11     {
12         bool isNewlyRealized;
13         var child = (UIElement)generator.GenerateNext(out isNewlyRealized); // 實例化(構造出空的子元素UI容器)
14 
15         if (isNewlyRealized)
16         {
17             generator.PrepareItemContainer(child); // 填充UI容器數據
18         }
19     }
20 }

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

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

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

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

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

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

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

5

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

貼出主要代碼:

計算需要實例化數據項的起止索引
 1 /// <summary>
 2 /// 計算可是元素起止索引
 3 /// </summary>
 4 /// <param name="availableSize">可用布局尺寸</param>
 5 /// <param name="firstVisibleChildIndex">第一個顯示的子元素索引</param>
 6 /// <param name="lastVisibleChildIndex">最后一個顯示的子元素索引</param>
 7 private void ComputeVisibleChildIndex(Size availableSize, out int firstVisibleChildIndex, out int lastVisibleChildIndex)
 8 {
 9     ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
10 
11     if (itemsControl != null && itemsControl.Items != null && ChildWidth > 0 && ChildHeight > 0)
12     {
13         ChildrenCount = itemsControl.Items.Count;
14 
15         _horizontalChildMaxCount = (int)(availableSize.Width / ChildWidth);
16         _verticalChildMaxCount = (int)(availableSize.Height / ChildHeight);
17 
18         PageSize = _horizontalChildMaxCount * _verticalChildMaxCount;
19 
20         // 計算子元素顯示起止索引
21         firstVisibleChildIndex = PageIndex * PageSize;
22         lastVisibleChildIndex = Math.Min(ChildrenCount, firstVisibleChildIndex + PageSize) - 1;
23 
24         Debug.WriteLine("firstVisibleChildIndex:{0}, lastVisibleChildIndex{1}", firstVisibleChildIndex, lastVisibleChildIndex)
25     }
26     else
27     {
28         ChildrenCount = 0;
29         firstVisibleChildIndex = -1;
30         lastVisibleChildIndex = -1;
31         PageSize = 0;
32     }
33 }
測量子元素布局期望尺寸及數據項實例化
 1 /// <summary>
 2 /// 測量子元素布局,生成需要顯示的子元素
 3 /// </summary>
 4 /// <param name="availableSize">可用布局尺寸</param>
 5 /// <param name="firstVisibleChildIndex">第一個顯示的子元素索引</param>
 6 /// <param name="lastVisibleChildIndex">最后一個顯示的子元素索引</param>
 7 private void MeasureChild(Size availableSize, int firstVisibleChildIndex, int lastVisibleChildIndex)
 8 {
 9     if (firstVisibleChildIndex < 0)
10     {
11         return;
12     }
13 
14     // 注意,在第一次使用 ItemContainerGenerator之前要先訪問一下InternalChildren, 
15     // 否則ItemContainerGenerator為null,是一個Bug
16     UIElementCollection children = InternalChildren;
17     IItemContainerGenerator generator = ItemContainerGenerator;
18 
19     // 獲取第一個可視元素位置信息
20     GeneratorPosition position = generator.GeneratorPositionFromIndex(firstVisibleChildIndex);
21     // 根據元素位置信息計算子元素索引
22     int childIndex = position.Offset == 0 ? position.Index : position.Index + 1;
23 
24     using (generator.StartAt(position, GeneratorDirection.Forward, true))
25     {
26         for (int itemIndex = firstVisibleChildIndex; itemIndex <= lastVisibleChildIndex; itemIndex++, childIndex++)
27         {
28             bool isNewlyRealized;   // 用以指示新生成的元素是否是新實體化的
29 
30             // 生成下一個子元素
31             var child = (UIElement)generator.GenerateNext(out isNewlyRealized);
32 
33             if (isNewlyRealized)
34             {
35                 if (childIndex >= children.Count)
36                 {
37                     AddInternalChild(child);
38                 }
39                 else
40                 {
41                     InsertInternalChild(childIndex, child);
42                 }
43                 generator.PrepareItemContainer(child);
44             }
45 
46             // 測算子元素布局
47             child.Measure(availableSize);
48         }
49     }
50 }
清理不再顯示的子元素
 1 /// <summary>
 2 /// 清理不需要顯示的子元素
 3 /// </summary>
 4 /// <param name="firstVisibleChildIndex">第一個顯示的子元素索引</param>
 5 /// <param name="lastVisibleChildIndex">最后一個顯示的子元素索引</param>
 6 private void CleanUpItems(int firstVisibleChildIndex, int lastVisibleChildIndex)
 7 {
 8     UIElementCollection children = this.InternalChildren;
 9     IItemContainerGenerator generator = this.ItemContainerGenerator;
10 
11     // 清除不需要顯示的子元素,注意從集合后向前操作,以免造成操作過程中元素索引發生改變
12     for (int i = children.Count - 1; i > -1; i--)
13     {
14         // 通過已顯示的子元素的位置信息得出元素索引
15         var childGeneratorPos = new GeneratorPosition(i, 0);
16         int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos);
17 
18         // 移除不再顯示的元素
19         if (itemIndex < firstVisibleChildIndex || itemIndex > lastVisibleChildIndex)
20         {
21             generator.Remove(childGeneratorPos, 1);
22             RemoveInternalChildRange(i, 1);
23         }
24     }
25 }

 

參考文章:

http://blogs.msdn.com/b/dancre/archive/2006/02/06/526310.aspx (寫的非常好)

 

附上源代碼

 

版權說明:本文章版權歸本人及博客園共同所有,未經允許請勿用於任何商業用途。轉載請標明原文出處:

http://www.cnblogs.com/talywy/archive/2012/09/07/CustomVirtualizingPanel.html 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM