一、列表虛擬化與海量數(shù)據(jù)展示
在tds?中,當(dāng)用戶在關(guān)鍵詞后加了/a
參數(shù),會列出所有的文件。此時可能會有上百萬個。為了流暢操作和顯示這些數(shù)據(jù),只能借助列表虛擬化技術(shù)來實現(xiàn)。

列表虛擬化是一種優(yōu)化技術(shù),用于處理大量數(shù)據(jù)時提高性能和用戶體驗。它通過實時計算來模擬海量數(shù)據(jù)的展示,此時的性能流暢度與數(shù)據(jù)大小無關(guān),僅與實時計算需要的執(zhí)行時間有關(guān)。其核心思想是按需加載和按需渲染。
在用戶滾動列表時,虛擬化技術(shù)會自動將大量數(shù)據(jù)分成多個小塊(或頁面),每次只加載和渲染當(dāng)前視圖范圍內(nèi)的數(shù)據(jù)塊。這個過程是事件驅(qū)動,當(dāng)用戶滾動列表時,這些事件通知應(yīng)用程序加載和渲染新的數(shù)據(jù)塊。在后臺,虛擬化管理模塊還將即將進(jìn)入視圖范圍的數(shù)據(jù)項緩存起來,以便快速訪問。這減少了對數(shù)據(jù)源的頻繁訪問,提高了性能。
主要原理很簡單:
- 視口渲染:列表虛擬化技術(shù)的核心是只渲染用戶當(dāng)前可視區(qū)域內(nèi)的元素,而不是一次性渲染整個列表。當(dāng)用戶滾動時,動態(tài)地加載和卸載元素。
- 占位元素:為了保持列表的滾動條高度和布局正確,虛擬化列表會使用占位元素來表示未渲染部分的高度。
- 元素復(fù)用:虛擬化列表通常會重用相同的組件實例(數(shù)據(jù)或UI元素)來渲染不同的數(shù)據(jù)項,通過緩存從而減少開銷。
Winform和Avalonia各自的列表虛擬化技術(shù)實現(xiàn)不太一樣,Winform需要考慮對實時事件手動處理,Avalonia則可以依賴自帶的響應(yīng)式模式綁定自動完成。我一起來看看具體都實現(xiàn)吧。
Winform的虛擬列表的開啟需要將控件ListView
的VirtualMode
屬性設(shè)置為true
。在虛擬化過程中,用戶滾動列表時,ListView會觸發(fā)兩個關(guān)鍵事件,需要我們進(jìn)行實現(xiàn):
ListView.CacheVirtualItems
事件:當(dāng)用戶滾動列表時,此事件會被觸發(fā)。它通知我們哪些項即將進(jìn)入視圖范圍,我們可以在這個事件中緩存這些項。例如,可以使用一個數(shù)組來存儲這些即將顯示的項,作為我們的cache
。這樣可以減少對數(shù)據(jù)源的頻繁訪問,提高性能。ListView.RetrieveVirtualItem
事件:當(dāng)ListView
需要將某個項渲染到UI上時,會觸發(fā)此事件。我們可以通過從緩存中讀取對應(yīng)的項并返回給UI來實現(xiàn)。如果緩存中沒有找到對應(yīng)的項,我們也可以選擇動態(tài)生成它。
CacheVirtualItems
有時候也可以不實現(xiàn),RetrieveVirtualItem
也可以實時處理動態(tài)生成ListViewItem。
以下是簡化后的代碼實現(xiàn),需要詳細(xì)實現(xiàn)朋友們可以參考項目源碼。
private ListViewItem[] CurrentCacheItemsSource;
private int firstitem;
private bool refcache = false;
private void ListView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
if (CurrentCacheItemsSource != null && e.ItemIndex >= firstitem && e.ItemIndex < firstitem + CurrentCacheItemsSource.Length)
{
e.Item = CurrentCacheItemsSource[e.ItemIndex - firstitem];
}
else
{
e.Item = GenerateListViewItem(e.ItemIndex);
}
if (e.Item == null)
{
e.Item = new ListViewItem(new string[] { "加載失敗", "", "" });
}
}
private void ListView1_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e)
{
if (e.StartIndex >= firstitem && e.EndIndex <= firstitem + CurrentCacheItemsSource.Length)
{
return;
}
firstitem = e.StartIndex;
int length = e.EndIndex - e.StartIndex + 1;
CurrentCacheItemsSource = new ListViewItem[length];
for (int i = 0; i < length; i++)
{
CurrentCacheItemsSource[i] = GenerateListViewItem(firstitem + i);
}
refcache = false;
ListView1.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
}
private ListViewItem GenerateListViewItem(int index)
{
if (index < vresultNum)
{
FrnFileOrigin f = vlist[index];
return new ListViewItem(new string[] { f.Name, f.Path, f.Size.ToString() });
}
return null;
}
實際使用中,ListViewItem虛擬化的緩存是我們手動將數(shù)據(jù)關(guān)聯(lián)到一個數(shù)組或List中的。當(dāng)數(shù)據(jù)發(fā)生變化時,除了自動刷新各個索引處對象的值外,還需要控制長度。
如果說緩存有100個元素,我們可以通過設(shè)定ListView的VirtualListSize
屬性來改變要顯示的元素個數(shù),比如只顯示前10個,這樣就可以在數(shù)據(jù)變小的時避免重新創(chuàng)建數(shù)組/列表對象。
5.2 Avalonia的虛擬面板 以為例
與Winform不同,Avalonia的ListBox等控件中沒有VirtualMode
屬性,需要通過VirtualizingStackPanel
等方式開啟,xml代碼如下。
<ListBox x:Name="fileListBox" ItemsSource="{Binding Items.DisplayedData, Mode=OneWay}" >
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
綁定的核心思想是,我們將真實數(shù)據(jù)的引用存起來,Listbox則是綁定到一個IEnumerable<T>
對象接口中。當(dāng)真實數(shù)據(jù)需要更新時,我們控制IEnumerable<T>
的更新。
由于IEnumerable<T>
是延遲實現(xiàn)的(yield return
)因此不會產(chǎn)生額外數(shù)組開銷??梢酝ㄟ^Take(DisplayCount)的方法去動態(tài)控制所展示數(shù)據(jù)的長度,類似于Winform
的VirtualListSize
, 這樣能夠有效提升性能,尤其是在處理大量數(shù)據(jù)以及處理數(shù)據(jù)長度頻繁變化的場景。
在下面的代碼中給出了一個ViewModel
的示例實現(xiàn),需要用到Avalonia.ReactiveUI
庫(這個庫需要單獨在Nuget
上下載)。
public class DataViewModel : ReactiveObject
{
private IList<FrnFileOrigin> _allData = [];
private IEnumerable<FrnFileOrigin> _displayedData = [];
private int _displayCount = 100;
public IEnumerable<FrnFileOrigin> DisplayedData
{
get => _displayedData;
private set => this.RaiseAndSetIfChanged(ref _displayedData, value);
}
bool isShowOpenWith = true;
public bool IsShowOpenWith
{
get => isShowOpenWith;
set
{
this.RaiseAndSetIfChanged(ref isShowOpenWith, value);
}
}
public int DisplayCount
{
get => _displayCount;
private set
{
_displayCount = value;
}
}
public DataViewModel()
{
}
public void Bind(IList<FrnFileOrigin> _allData)
{
if (this._allData != _allData)
{
this._allData = _allData;
}
}
public void UpdateDisplayedData()
{
DisplayedData = _allData.Take(DisplayCount);
}
public void SetDisplayCount(int count)
{
DisplayCount = count;
}
}
在上述代碼中,DisplayedData
屬性通過 RaiseAndSetIfChanged
方法來實現(xiàn)屬性值的更新和通知,確保綁定到該屬性的界面元素能夠及時響應(yīng)數(shù)據(jù)的變化。而 DisplayCount
屬性在更新時會調(diào)用 UpdateDisplayedData()
方法,從而保證 DisplayedData
的內(nèi)容始終與 DisplayCount
保持一致。
還有一個問題就是,Avalonia在VirtualizingStackPanel
中,虛擬化后可能數(shù)據(jù)不是瓶頸,UI顯示同樣會造成卡頓,尤其是在應(yīng)用虛擬化時實時渲染發(fā)復(fù)雜的布局。因此我們可以設(shè)置VirtualizingStackPanel.CacheLength
屬性。
這個屬性是一個double
值,決定了在視口上方和下方(或左方和右方)要保持多少額外的空間。值為 0.5 表示系統(tǒng)在每一側(cè)(上下或左右)將緩沖視口大小的一半,此時將實例化更多UI元素。盡管會占更多內(nèi)存,但大大減少Measure-Arrange
循環(huán)的次數(shù)(measure
:確定控件所需的最小寬度和高度; arrange
:將控件放置在父控件中,并確定其最終的位置和大小;Arrange
:) 否則,在UI復(fù)雜程度較高時,GC壓力巨大。
三、最后
感謝您的耐心閱讀,希望各位從零開始的新朋友和老朋友有所收獲!如果你對這篇文章的內(nèi)容有任何建議或想法,歡迎隨時交流!本文中TDS文件搜索工具的Winform版本已在倉庫完全開源了!點個 Star ??支持一下!代碼倉庫地址 https://github.com/LdotJdot/TDS_Winform
轉(zhuǎn)自https://www.cnblogs.com/luojin765/p/19118610
該文章在 2025/10/11 17:09:24 編輯過