您的位置:首頁 > 軟件教程 > 教程 > WPF性能優(yōu)化之UI虛擬化

WPF性能優(yōu)化之UI虛擬化

來源:好特整理 | 時(shí)間:2024-09-03 09:56:34 | 閱讀:140 |  標(biāo)簽:   | 分享到:

@目錄前言一、VirtualizingStackPanel1.1 虛擬化功能介紹1、在Window中添加一個(gè)ListBox控件。2、在設(shè)計(jì)視圖中用鼠標(biāo)選中ListBox控件并右健依次單擊“編輯其他模板”-“編輯項(xiàng)的布局模板”-“編輯副本”。3、查看生成的模板代碼。1.2 虛擬化參數(shù)介紹二、Custo

@


前言

相信很多WPF開發(fā)者都碰到過這種情況,當(dāng)在一個(gè)ItemsControl(或繼承自ItemsControl)控件中綁定一個(gè)集合的時(shí)候,如果集合中的條目過多,那么界面就會(huì)變得卡頓甚至停止響應(yīng),特別是在容器或窗口大小發(fā)生改變時(shí),界面的渲染就會(huì)給人一種慢半拍的感覺,體驗(yàn)感非常差,這時(shí)我們就可以用虛擬化技術(shù)來解決這個(gè)問題。

UI虛擬化的核心思想就是只渲染可視范圍內(nèi)的控件,所以它通常會(huì)搭配ScrollViewer控件一起使用,通過ScrollViewer控件中的VerticalOffset、HorizontalOffset、ViewportWidth、ViewportHeight等參數(shù)可以計(jì)算出在可視范圍內(nèi)應(yīng)該顯示的控件,當(dāng)控件不被顯示時(shí)將它從Panel中移出,這樣就可以保證同一時(shí)間只渲染了有限的控件,而不是渲染所有控件,從而達(dá)到性能提升的目的。

一、VirtualizingStackPanel

1.1 虛擬化功能介紹

VirtualizingStackPanel是WPF中的一個(gè)內(nèi)置控件,它提供了UI虛擬化的功能,在ListBox、ListView、DataGrid等控件中它是默認(rèn)布局控件,我們可以通過查看控件模板的方式來看看它是如何定義的。

1、在Window中添加一個(gè)ListBox控件。

WPF性能優(yōu)化之UI虛擬化

2、在設(shè)計(jì)視圖中用鼠標(biāo)選中ListBox控件并右健依次單擊“編輯其他模板”-“編輯項(xiàng)的布局模板”-“編輯副本”。

WPF性能優(yōu)化之UI虛擬化

3、查看生成的模板代碼。

WPF性能優(yōu)化之UI虛擬化
通過以上代碼可以看出,ListBox有一個(gè)名為ItemsPanel的屬性,在該屬性中指定了一個(gè)Panel控件,ListBox在渲染時(shí)用該P(yáng)anel來布局子項(xiàng),我們要實(shí)現(xiàn)虛擬化只需要在ItemsPanel中指定VirtualizingStackPanel控件即可。

1.2 虛擬化參數(shù)介紹

如果你自己實(shí)現(xiàn)一個(gè)繼承自ItemsControl的控件,并按1.1的步驟操作,你會(huì)發(fā)現(xiàn)還是無法實(shí)現(xiàn)虛擬化功能,原因是沒有開啟虛擬化功能(ListBox、ListView、DataGrid等控件是默認(rèn)開啟的),要開啟ItemsControl控件的虛擬化功能我們還需要設(shè)置VirtualizingPanel.IsVirtualizing附加屬性,以下為示例:


    
        
            
        
    

VirtualizingPanel中除了IsVirtualizing參數(shù)以外還有很多其它參數(shù)可以控制更多的虛擬化細(xì)節(jié),以下是參數(shù)說明:

  1. VirtualizingPanel.CacheLength="10"
    作用:CacheLength 屬性指定了在虛擬化過程中,控件需要緩存的項(xiàng)目數(shù)。這意味著在視口之外的區(qū)域中,面板會(huì)保留一定數(shù)量的項(xiàng)目以提高滾動(dòng)平滑度。當(dāng)用戶滾動(dòng)視圖時(shí),緩存的項(xiàng)目可以更快地重新使用,從而減少重新創(chuàng)建和布局的開銷。
    值:10 表示視口外會(huì)緩存 10 個(gè)項(xiàng)目。這是一個(gè)相對(duì)的值,具體數(shù)目可能會(huì)根據(jù)實(shí)際實(shí)現(xiàn)有所不同。
  2. VirtualizingPanel.CacheLengthUnit="Item"
    作用:CacheLengthUnit 屬性定義 CacheLength 的單位?梢赃x擇 Pixel 或 Item,其中 Item 表示緩存的長度以項(xiàng)目的數(shù)量為單位,Pixel 表示緩存的長度以像素為單位。
    值:Item 表示緩存的長度是以項(xiàng)目的數(shù)量為單位。這適用于項(xiàng)目大小固定或數(shù)據(jù)量較小的情況。
  3. VirtualizingPanel.IsContainerVirtualizable="True"
    作用:IsContainerVirtualizable 屬性指示面板是否允許對(duì)其子項(xiàng)的容器進(jìn)行虛擬化。設(shè)置為 True 表示面板可以對(duì)其容器進(jìn)行虛擬化,從而優(yōu)化性能,特別是在處理大量數(shù)據(jù)時(shí)。
    值:True 表示啟用容器虛擬化。
  4. VirtualizingPanel.IsVirtualizing="True"
    作用:IsVirtualizing 屬性指示面板是否啟用虛擬化。這是虛擬化的核心設(shè)置,設(shè)置為 True 表示面板會(huì)僅對(duì)視口內(nèi)的項(xiàng)目進(jìn)行渲染和處理,而不是一次性加載所有項(xiàng)目。
    值:True 表示啟用虛擬化,減少不必要的控件實(shí)例化和布局計(jì)算。
  5. VirtualizingPanel.IsVirtualizingWhenGrouping="True"
    作用:IsVirtualizingWhenGrouping 屬性控制面板在分組時(shí)是否繼續(xù)進(jìn)行虛擬化。當(dāng)設(shè)置為 True 時(shí),面板在分組數(shù)據(jù)時(shí)仍然會(huì)應(yīng)用虛擬化策略,以保持性能優(yōu)化。
    值:True 表示即使在數(shù)據(jù)分組時(shí),也保持虛擬化。
  6. VirtualizingPanel.ScrollUnit="Item"
    作用:ScrollUnit 屬性定義滾動(dòng)的單位?梢赃x擇 Item 或 Pixel,其中 Item 表示每次滾動(dòng)一個(gè)項(xiàng)目,Pixel 表示每次滾動(dòng)一定像素。
    值:Item 表示每次滾動(dòng)一個(gè)項(xiàng)目的單位,而不是固定像素?cái)?shù),這對(duì)于項(xiàng)目高度一致的情況尤其有效。
  7. VirtualizingPanel.VirtualizationMode="Recycling"
    作用:VirtualizationMode 屬性指定虛擬化模式。Recycling 模式表示控件會(huì)重用已經(jīng)不再可見的項(xiàng)目的容器,而不是銷毀它們。這種方式可以減少控件的創(chuàng)建和銷毀開銷,從而提升性能。
    值:Recycling 表示啟用重用模式,使面板更高效地管理控件實(shí)例,適合動(dòng)態(tài)數(shù)據(jù)變化的場(chǎng)景。

二、CustomVirtualizingPanel

2.1 基礎(chǔ)知識(shí)

要開發(fā)自己的虛擬化Panel我們需要繼承自VirtualizingPanel類,并實(shí)現(xiàn)IScrollInfo接口,VirtualizingPanel中提供了操作Panel子控件的相關(guān)的方法,IScrollInfo接口定義了ScrollViewer控件的自定義行為,我們實(shí)現(xiàn)了IScrollInfo就可以接管ScrollViewer控件的相關(guān)操作。
代碼如下(示例):

public class CustomVirtualizingPanel : VirtualizingPanel, IScrollInfo
{
}

2.1.1 VirtualizingPanel

VirtualizingPanel中有一個(gè)名為“ItemContainerGenerator”的屬性,該屬性提供了對(duì)虛擬化Panel子控件創(chuàng)建及銷毀的方法,它的工作流程大致如下:

  1. 當(dāng)ScrollViewer控件滾動(dòng)條移動(dòng)時(shí),獲取滾動(dòng)條的偏移量,通過偏移量和視口大小計(jì)算出Panel中應(yīng)該顯示的子控件位置;
  2. 調(diào)用ItemContainerGenerator.GenerateNext();從指定位置生成Panel子控件;
  3. 調(diào)用ItemContainerGenerator.Remove();刪除可見范圍以外的Panel子控件;

2.1.2 IScrollInfo

public class CustomVirtualizingPanel : VirtualizingPanel, IScrollInfo
{
    public ScrollViewer ScrollOwner { get; set; }   //當(dāng)前ScrollViewer控件
    public bool CanVerticallyScroll { get; set; }   //是否可以在垂直方向滾動(dòng)
    public bool CanHorizontallyScroll { get; set; } //是否可以在水平方向滾動(dòng)

    public double ExtentWidth { get; }              //滾動(dòng)內(nèi)容的總寬度(包括可見部分和不可見部分)
    public double ExtentHeight { get; }             //滾動(dòng)內(nèi)容的總高度(包括可見部分和不可見部分)
    public double ViewportWidth { get; }            // ScrollViewer控件可以看到的那部分區(qū)域的寬度
    public double ViewportHeight { get; }           //ScrollViewer控件可以看到的那部分區(qū)域的高度
    public double HorizontalOffset { get; }         //水平滾動(dòng)條的偏移量
    public double VerticalOffset { get; }           //垂直滾動(dòng)條的偏移量

    public void LineDown() { }                      //鼠標(biāo)點(diǎn)擊滾動(dòng)條下箭頭的操作
    public void LineLeft() { }                      //鼠標(biāo)點(diǎn)擊滾動(dòng)條左箭頭的操作
    public void LineRight() { }                     //鼠標(biāo)點(diǎn)擊滾動(dòng)條右箭頭的操作
    public void LineUp() { }                        //鼠標(biāo)點(diǎn)擊滾動(dòng)條上箭頭的操作

    public void MouseWheelDown() { }                //鼠標(biāo)滾輪向下時(shí)的操作
    public void MouseWheelLeft() { }                //鼠標(biāo)滾輪向左時(shí)的操作
    public void MouseWheelRight() { }               //鼠標(biāo)滾輪向右時(shí)的操作
    public void MouseWheelUp() { }                  //鼠標(biāo)滾輪向上時(shí)的操作

    public void PageDown() { }                      //在滾動(dòng)條上按鍵盤上下頁的操作
    public void PageLeft() { }                      //在滾動(dòng)條上按鍵盤上左頁的操作
    public void PageRight() { }                     //在滾動(dòng)條上按鍵盤上右頁的操作
    public void PageUp() { }                        //在滾動(dòng)條上按鍵盤上上頁的操作

    public void SetHorizontalOffset(double offset) { }                          //設(shè)置滾動(dòng)條水平偏移量
    public void SetVerticalOffset(double offset) { }                            //設(shè)置滾動(dòng)條垂直偏移量
    public Rect MakeVisible(Visual visual, Rect rectangle) { return default; }  //強(qiáng)制滾動(dòng)Panel子控件(比如只有部分區(qū)域顯示在可視范圍內(nèi),點(diǎn)擊之后完全滾動(dòng)到可視范圍內(nèi))
}

2.2 實(shí)戰(zhàn)案例

2.2.1 需求分析

  1. CustomVirtualizingPanel應(yīng)該具有高度的靈活性,以最小的代價(jià)滿足不同的虛擬化布局需求,不需要每次都要重寫一個(gè)CustomVirtualizingPanel控件。
  2. 最好是可以通過屬性切換布局,這樣可以實(shí)現(xiàn)布局切換時(shí)的過渡效果。

2.2.2 代碼實(shí)現(xiàn)

通過分析要想實(shí)現(xiàn)以上效果,最好的方法就是將CustomVirtualizingPanel中需要計(jì)算的關(guān)鍵部分抽象出來做成一個(gè)接口,當(dāng)需要布局計(jì)算的時(shí)候我們可以直接通過接口獲取到關(guān)鍵計(jì)算結(jié)果。
1) 定義接口

public interface IVirtualizingPanelBuilder 
{     
	void Initialize(CustomVirtualizingPanel virtualizingPanel);     
	double GetItemWidth(Size availableSize);     
	double GetItemHeight(Size availableSize);     
	int CalculateItemsPerRowCount(Size availableSize);     
	int CalculateRowCount(Size availableSize);     
	Size CalculateExtent(Size availableSize);     
	ItemRange CalculateItemRange(Size availableSize); 
}

2) 在CustomVirtualizingPanel類中添加屬性VirtualizingPanelBuilder

public IVirtualizingPanelBuilder VirtualizingPanelBuilder 
{     
	get { return (IVirtualizingPanelBuilder)GetValue(VirtualizingPanelBuilderProperty); }     
	set { SetValue(VirtualizingPanelBuilderProperty, value); } 
}

3) 實(shí)現(xiàn)VirtualizingPanelBuilder

public class VirtualizingStackPanelBuilder : DependencyObject, IVirtualizingPanelBuilder
{
    /// 
    /// 虛擬面板
    /// 
    private CustomVirtualizingPanel _virtualizingPanel;

    /// 
    /// 初始化
    /// 
    /// 
    public void Initialize(CustomVirtualizingPanel virtualizingPanel)
    {
        _virtualizingPanel = virtualizingPanel;
    }
    /// 
    /// 獲取Item高度
    /// 
    /// 
    /// 
    public double GetItemHeight(Size availableSize)
    {
        if (ItemHeight > 0)
            return ItemHeight;
        else if (_virtualizingPanel.Children.Count != 0)
            return _virtualizingPanel.Children[0].DesiredSize.Height;
        else
            return _virtualizingPanel.CalculateChildSize(availableSize).Height;
    }
    /// 
    /// 獲取Item寬度
    /// 
    /// 
    /// 
    public double GetItemWidth(Size availableSize)
    {
        return availableSize.Width;
    }
    /// 
    /// 計(jì)算每行顯示的Item數(shù)
    /// 
    /// 
    /// 
    public int CalculateItemsPerRowCount(Size availableSize)
    {
        return 1;
    }
    /// 
    /// 計(jì)算行數(shù)
    /// 
    /// 
    /// 
    public int CalculateRowCount(Size availableSize)
    {
        return _virtualizingPanel.Items.Count;
    }
    /// 
    /// 計(jì)算滾動(dòng)面積
    /// 
    /// 
    /// 
    public Size CalculateExtent(Size availableSize)
    {
        var height = GetItemHeight(availableSize);
        var rowCount = CalculateRowCount(availableSize);
        return new Size(availableSize.Width, height * rowCount);
    }
    /// 
    /// 計(jì)算可見區(qū)域內(nèi)的Item范圍
    /// 
    /// 
    /// 
    public ItemRange CalculateItemRange(Size availableSize)
    {
        if (!this._virtualizingPanel.IsVirtualizing)
        {
            return new ItemRange(0, this._virtualizingPanel.Items.Count - 1);
        }

        var viewportHeight = _virtualizingPanel.ViewportHeight;
        var offsetY = _virtualizingPanel.VerticalOffset;

        var rowCount = this.CalculateRowCount(availableSize);
        var itemHeight = this.GetItemHeight(availableSize);

        var firstVisibleItemIndex = (int)Math.Floor(offsetY / itemHeight);
        var lastVisibleItemIndex = (int)Math.Ceiling((offsetY + viewportHeight) / itemHeight) - 1;

        if (lastVisibleItemIndex >= rowCount)
            lastVisibleItemIndex = rowCount - 1;
        return new ItemRange(firstVisibleItemIndex, lastVisibleItemIndex);
    }

    /// 
    /// Item高度
    /// 
    public double ItemHeight
    {
        get { return (double)GetValue(ItemHeightProperty); }
        set { SetValue(ItemHeightProperty, value); }
    }

    public static readonly DependencyProperty ItemHeightProperty =
        DependencyProperty.Register("ItemHeight", typeof(double), typeof(VirtualizingStackPanelBuilder), new PropertyMetadata(ItemHeightPropertyChangedCallback));

    public static void ItemHeightPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((VirtualizingStackPanelBuilder)d)._virtualizingPanel?.InvalidateVisual();
    }
}

4) 設(shè)置參數(shù)

     
           
         
   

2.2.3 運(yùn)行效果

為了能夠演示布局切換的過渡效果,這里除了上面的StackPanel布局以外還實(shí)現(xiàn)了UniformGrid布局,以下分別演示1億條數(shù)據(jù)布局切換及非虛擬化狀態(tài)下的布局切換過渡效果。
1) 虛擬化切換布局
WPF性能優(yōu)化之UI虛擬化
2) 非虛擬化切換過渡效果
WPF性能優(yōu)化之UI虛擬化

小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認(rèn)同期限觀點(diǎn)或證實(shí)其描述。

相關(guān)視頻攻略

更多

掃二維碼進(jìn)入好特網(wǎng)手機(jī)版本!

掃二維碼進(jìn)入好特網(wǎng)微信公眾號(hào)!

本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請(qǐng)發(fā)郵件[email protected]

湘ICP備2022002427號(hào)-10 湘公網(wǎng)安備:43070202000427號(hào)© 2013~2024 haote.com 好特網(wǎng)