轉(zhuǎn)帖|使用教程|編輯:龔雪|2025-01-07 10:14:16.150|閱讀 117 次
概述:本文主要介紹如何使用WPF開發(fā)自定義用戶控件及實(shí)現(xiàn)相關(guān)自定義事件的處理,希望對(duì)大家有所幫助和啟示~
# 界面/圖表報(bào)表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
相關(guān)鏈接:
對(duì)于Winform自定義的用戶控件來說,它的呈現(xiàn)方式主要就是基于GDI+進(jìn)行渲染的,對(duì)于數(shù)量不多的控件呈現(xiàn),一般不會(huì)覺察性能有太多的問題,隨著控件的數(shù)量大量的增加,就會(huì)產(chǎn)生性能問題,比較緩慢,或者句柄創(chuàng)建異常等問題。本文將為大家介紹WPF技術(shù)處理的自定義用戶控件,引入虛擬化技術(shù)的處理,較好的解決這些問題。
PS:給大家推薦一個(gè)C#開發(fā)可以用到的界面組件——DevExpress WPF,它擁有120+個(gè)控件和庫(kù),將幫助您交付滿足甚至超出企業(yè)需求的高性能業(yè)務(wù)應(yīng)用程序。通過DevExpress WPF能創(chuàng)建有著強(qiáng)大互動(dòng)功能的XAML基礎(chǔ)應(yīng)用程序,這些應(yīng)用程序?qū)W⒂诋?dāng)代客戶的需求和構(gòu)建未來新一代支持觸摸的解決方案。
DevExpress技術(shù)交流群11:749942875 歡迎一起進(jìn)群討論
前面例子我測(cè)試一次性在界面呈現(xiàn)的控件總數(shù)接近2k左右的時(shí)候,句柄就會(huì)創(chuàng)建異常。由于Winform控件沒有引入虛擬化技術(shù)來重用UI控件的資源,因此控件呈現(xiàn)量多的話,就會(huì)有嚴(yán)重的性能問題。而WPF引入的虛擬化技術(shù)后,對(duì)于UI資源的重用就會(huì)降低界面的消耗,而且即使數(shù)量再大,也不會(huì)有卡頓的問題。其原理就是UI變化還是那些內(nèi)容,觸發(fā)滾動(dòng)的時(shí)候,也只是對(duì)可見控件的數(shù)據(jù)進(jìn)行更新,從而大量減少UI控件創(chuàng)建刷新的消耗。
如果接觸過IOS開發(fā)的時(shí)候,它們的處理也是一樣,在介紹列表處理綁定的時(shí)候,它本身就強(qiáng)制重用列表項(xiàng)的資源,從而達(dá)到降低UI資源消耗 的目的。
我們來介紹自定義控件之前,我們先來了解一下虛擬化的技術(shù)處理。
在WPF應(yīng)用程序開發(fā)過程中,大數(shù)據(jù)量的數(shù)據(jù)展現(xiàn)通常都要考慮性能問題。
例如對(duì)于WPF程序來說,原始數(shù)據(jù)源數(shù)據(jù)量很大,但是某一時(shí)刻數(shù)據(jù)容器中的可見元素個(gè)數(shù)是有限的,剩余大多數(shù)元素都處于不可見狀態(tài),如果一次性將所有的數(shù)據(jù)元素都渲染出來則會(huì)非常的消耗性能。因而可以考慮只渲染當(dāng)前可視區(qū)域內(nèi)的元素,當(dāng)可視區(qū)域內(nèi)的元素需要發(fā)生改變時(shí),再渲染即將展現(xiàn)的元素,最后將不再需要展現(xiàn)的元素清除掉,這樣可以大大提高性能。
WPF列表控件提供的最重要功能是UI虛擬化(UI Virtaulization),UI 虛擬化是列表僅為當(dāng)前顯示項(xiàng)創(chuàng)建容器對(duì)象的一種技術(shù)。
在WPF中System.Windows.Controls命名空間下的VirtualizingStackPanel可以實(shí)現(xiàn)數(shù)據(jù)展現(xiàn)的虛擬化功能,ListBox的默認(rèn)元素展現(xiàn)容器就是它。但有時(shí)VirtualizingStackPanel的布局并不能滿足我們的實(shí)際需要,此時(shí)就需要實(shí)現(xiàn)自定義布局的虛擬容器了。
要想實(shí)現(xiàn)一個(gè)虛擬容器,并讓虛擬容器正常工作,必須滿足以下兩個(gè)條件:
我在這里首先介紹如何使用虛擬化容器控件即可,自定義的處理可以在熟悉后,參考一些代碼進(jìn)行處理即可。
VirtualizingPanel從一開始就存在于 WPF 中,這提供了不必立即為可視化創(chuàng)建ItemsControl的所有 UI 元素的可能性。
VirtualizingPanel類中實(shí)現(xiàn)以下幾項(xiàng)依賴屬性。
VirtualizingPanel 可以通過CacheLengthUnit 設(shè)置緩存單元。可能的有:Item、Page、Pixel 幾個(gè)不同的項(xiàng)目,這確定了視口之前和之后的緩存大小。這樣可以避免 UI 元素只在可見時(shí)才生成。
例如對(duì)于ListBox控件的虛擬化處理,代碼如下所示。
<ListBox ItemsSource="{Binding VirtualizedBooks}" ItemTemplate="{StaticResource BookTemplate}" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.CacheLength="1,2" VirtualizingPanel.CacheLengthUnit="Page"/>
在我之前的WPF相關(guān)隨筆中,我介紹過UI部分,采用了lepoco/wpfui 的項(xiàng)目界面來集成處理的。
GitHub地址:
文檔地址:
lepoco/wpfui 的項(xiàng)目控件組中也提供了一個(gè)類似流式布局(類似Winform的FlowLayoutPanel)的虛擬化控件VirtualizingItemsControl,比較好用,我們借鑒來介紹一下。
<ui:VirtualizingItemsControl Foreground="{DynamicResource TextFillColorSecondaryBrush}" ItemsSource="{Binding ViewModel.Colors, Mode=OneWay}" VirtualizingPanel.CacheLengthUnit="Item"> <ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type models:DataColor}"> <ui:Button Width="80" Height="80" Margin="2" Padding="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Appearance="Secondary" Background="{Binding Color, Mode=OneWay}" FontSize="25" Icon="Fluent24" /> </DataTemplate> </ItemsControl.ItemTemplate> </ui:VirtualizingItemsControl>
這個(gè)界面的效果如下所示,它的后端ViewModel的數(shù)據(jù)模型中綁定9k左右個(gè)記錄對(duì)象,而在UI虛擬化的加持下,滾動(dòng)處理沒有任何卡頓,這就是其虛擬化優(yōu)勢(shì)所在。
我們上面為了簡(jiǎn)單介紹呈現(xiàn)的效果,主要在模板里面放置了一個(gè)簡(jiǎn)單的按鈕控件來定義顏色塊,開發(fā)的界面往往相對(duì)會(huì)復(fù)雜一些,如果不太考慮重用界面元素,簡(jiǎn)單的對(duì)象組裝可以在這個(gè) DataTemplate 模板里面進(jìn)行處理,如下代碼所示。
<ui:VirtualizingItemsControl Foreground="{DynamicResource TextFillColorSecondaryBrush}" ItemsSource="{Binding ViewModel.Colors, Mode=OneWay}" VirtualizingPanel.CacheLengthUnit="Item"> <ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type models:DataColor}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="auto" /> <RowDefinition Height="50" /> </Grid.RowDefinitions> <ui:Button Grid.Row="0" Width="80" Height="80" Margin="2" Padding="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Appearance="Secondary" Background="{Binding Color, Mode=OneWay}" FontSize="25" Icon="Fluent24" /> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="20*" /> <ColumnDefinition Width="20*" /> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" FontWeight="Bold" Foreground="Red" Text="左側(cè)" TextAlignment="Center" /> <TextBlock Grid.Column="1" FontWeight="Black" Foreground="Blue" Text="右側(cè)" TextAlignment="Center" /> </Grid> </Grid> </DataTemplate> </ItemsControl.ItemTemplate> </ui:VirtualizingItemsControl>
通過我們自定義的Grid布局,很好的組織起來相關(guān)的自定義控件的界面效果,會(huì)得到項(xiàng)目的界面效果。
前面介紹了一些基礎(chǔ)的虛擬化控件容器和一些常規(guī)的自定義控件內(nèi)容的只是,我們?cè)陂_發(fā)桌面程序的時(shí)候,為了方便重用等原因,往往把一些復(fù)雜的界面元素逐層分解,組合成一些自定義的控件,然后組裝層更高級(jí)的自定義控件,這樣就可以構(gòu)建界面和邏輯比較復(fù)雜的一些界面元素了。
前面文章中介紹,為了使用戶控件更加規(guī)范化,我們可以定義一個(gè)接口,聲明相關(guān)的屬性和處理方法,如下代碼所示。(這部分WPF和Winform自定義控件開發(fā)一樣處理)
/// <summary> /// 自定義控件的接口 /// </summary> public interface INumber { /// <summary> /// 數(shù)字 /// </summary> string Number { get; set; } /// <summary> /// 數(shù)值顏色 /// </summary> Color Color { get; set; } /// <summary> /// 顯示文本 /// </summary> string Animal { get; set; } /// <summary> /// 顯示文本 /// </summary> string WuHan { get; set; } /// <summary> /// 設(shè)置選中的內(nèi)容的處理 /// </summary> /// <param name="data">事件數(shù)據(jù)</param> void SetSelected(ClickEventData data); }
和WInform開發(fā)一樣,WPF也是創(chuàng)建一個(gè)自定義的控件,在項(xiàng)目上右鍵添加自定義控件,如下界面所示。
我們同樣命名為NumberItem,最終后臺(tái)Xaml的C#代碼生成如下所示(我們讓它繼承接口 INumber )。
/// <summary> /// NumberItem.xaml 的交互邏輯 /// </summary> public partial class NumberItem : UserControl, INumber
WPF自定義控件實(shí)現(xiàn)接口的屬性定義,不是簡(jiǎn)單的處理,需要按照WPF的屬性處理規(guī)則,這里和Winform處理有些小差異。
/// <summary> /// NumberItem.xaml 的交互邏輯 /// </summary> public partial class NumberItem : UserControl, INumber { #region 控件屬性定義 /// <summary> /// 數(shù)字 /// </summary> public string Number { get { return (string)GetValue(NumberProperty); } set { SetValue(NumberProperty, value); } } /// <summary> /// 顏色 /// </summary> public Color Color { get { return (Color)GetValue(ColorProperty); } set { SetValue(ColorProperty, value); } } /// <summary> /// 顯示文本 /// </summary> public string Animal { get { return (string)GetValue(AnimalProperty); } set { SetValue(AnimalProperty, value); } } /// <summary> /// 顯示文本 /// </summary> public string WuHan { get { return (string)GetValue(WuHanProperty); } set { SetValue(WuHanProperty, value); } } public static readonly DependencyProperty ColorProperty = DependencyProperty.Register( nameof(Color), typeof(Color), typeof(NumberItem), new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static readonly DependencyProperty NumberProperty = DependencyProperty.Register( nameof(Number), typeof(string), typeof(NumberItem), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(OnNumberPropertyChanged))); public static readonly DependencyProperty AnimalProperty = DependencyProperty.Register( nameof(Animal), typeof(string), typeof(NumberItem), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static readonly DependencyProperty WuHanProperty = DependencyProperty.Register( nameof(WuHan), typeof(string), typeof(NumberItem), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); #endregion
我們可以看到屬性名稱的取值和賦值,通過GetValue、SetValue 的操作實(shí)現(xiàn),同時(shí)需要定義一個(gè)靜態(tài)變量 DependencyProperty 的屬性定義,如 ***Property。
這個(gè)是WPF屬性的常規(guī)處理,沒增加一個(gè)屬性名稱,就增加一個(gè)對(duì)應(yīng)類型DependencyProperty 的**Property,如下所示。
public static readonly DependencyProperty ColorProperty = DependencyProperty.Register( nameof(Color), typeof(Color), typeof(NumberItem), new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
自定義控件的事件通知,有兩種處理方法,可以通過常規(guī)事件的冒泡層層推送到界面頂端處理,也可以使用MVVM的消息通知(類似消息總線的處理),我們先來介紹MVVM的消息通知,因?yàn)樗顬楹?jiǎn)單易用。
而這里所說的MVVM包,是指微軟的 CommunityToolkit.Mvvm的組件包,有興趣可以全面了解一下。
CommunityToolkit.Mvvm (又名 MVVM 工具包,以前名為 Microsoft.Toolkit.Mvvm) 是一個(gè)現(xiàn)代、快速且模塊化的 MVVM 庫(kù)。 它是 .NET 社區(qū)工具包的一部分,圍繞以下原則構(gòu)建:
MVVM 工具包由 Microsoft 維護(hù)和發(fā)布,是 .NET Foundation 的一部分,它還由內(nèi)置于 Windows 中的多個(gè)第一方應(yīng)用程序使用。
此包面向 .NET Standard,因此可在任何應(yīng)用平臺(tái)上使用:UWP、WinForms、WPF、Xamarin、Uno 等;和在任何運(yùn)行時(shí)上:.NET Native、.NET Core、.NET Framework或 Mono。 它在所有它們上運(yùn)行。 API 圖面在所有情況下都是相同的,因此非常適合生成共享庫(kù)。
官網(wǎng)介紹地址:
CommunityToolkit.Mvvm 類型包括如下列表,它的便利之處,主要通過標(biāo)記式的特性(Attribute)來實(shí)現(xiàn)相關(guān)的代碼的生成,簡(jiǎn)化了原來的代碼。
例如我們對(duì)于自定義控件的文本信息,雙擊觸發(fā)自定義控件事件處理,它的Xaml界面代碼如下所示。
<TextBlock x:Name="labelNumber" Background="{Binding Color, Converter={StaticResource ColorConverter}, ElementName=Item}" FontSize="18" FontWeight="Bold" Foreground="White" Text="{Binding Number, ElementName=Item}" TextAlignment="Center"> <TextBlock.InputBindings> <MouseBinding Command="{Binding DoubleClickCommand, ElementName=Item}" CommandParameter="Number" MouseAction="LeftDoubleClick" /> </TextBlock.InputBindings> </TextBlock>
我們雙擊文本的時(shí)候,觸發(fā)一個(gè)DoubleClickCommand 的命令。其里面主要核心就是利用MVVM推送一條消息即可,如下代碼所示。
//發(fā)送MVVM消息信息通知方式(一) WeakReferenceMessenger.Default.Send(new ClickEventMessage(eventData));
而其中 ClickEventMessage 是我們根據(jù)要求定義的一個(gè)消息對(duì)象類,如下代碼所示。
完整的Command命令如下所示。
/// <summary> /// 雙擊觸發(fā)MVVM消息通知 /// </summary> /// <param name="typeName">處理類型:Number、Animal、WuHan</param> /// <returns></returns> [RelayCommand] private async Task DoubleClick(string typeName) { var clickType = ClickEventType.Number; var clickValue = this.Number; ..............//處理不同typeName值邏輯//事件數(shù)據(jù) var eventData = new ClickEventData(clickType, clickValue); //發(fā)送MVVM消息信息通知方式(一) WeakReferenceMessenger.Default.Send(new ClickEventMessage(eventData)); }
通過這樣的消息發(fā)送,就需要有個(gè)地方來接收這個(gè)信息的,我們?cè)谛枰幚硎录母复翱谥袛r截處理消息即可。
//處理MVVM的消息通知 WeakReferenceMessenger.Default.Register<ClickEventMessage>(this, (r, m) => { var data = m.Value; var list = ControlHelper.FindVisualChildren<LotteryItemControl>(this.listControl); foreach (var lottery in list) { lottery.SetSelected(data); } });
其中ControlHelper.FindVisualChildren 的輔助類主要就是根據(jù)父對(duì)象,遞歸獲得下面指定類型的控件集合,其主要是通過系統(tǒng)輔助類VisualTreeHelper進(jìn)行控件遞歸的查詢處理,這里不再深入介紹。
上面的邏輯,就是獲得控件的消息后,對(duì)該容器的控件遞歸獲得指定類型的控件,然后對(duì)容器中的控件逐一進(jìn)行SetSelected的選中處理,從而改變控件的繪制狀態(tài)。
而LotteryItemControl就是一個(gè)比NumberItem自定義控件,更高一層的界面組織者,也是一個(gè)自定義用戶控件。
里面就是放置多個(gè)NumberItem自定義控件,組織起來呈現(xiàn)一定的規(guī)則排列即可。
自定義控件同樣需要綁定一個(gè)屬性LotteryInfo,以及WPF屬性LotteryInfoProperty。在屬性變化的時(shí)候,觸發(fā)界面控件數(shù)據(jù)的綁定處理即可。
其中InitData就是對(duì)里面的控件內(nèi)容逐一更新顯示即可,這里由于篇幅原因不再介紹太細(xì)節(jié)的地方。
完成了較高層次的自定義控件開發(fā)后,我們最后一步就是把這些自定義控件,通過虛擬化的控件容器方式來呈現(xiàn)出來,如下代碼所示。
<ui:VirtualizingItemsControl x:Name="listControl" Grid.Row="1" Foreground="{DynamicResource TextFillColorSecondaryBrush}" ItemsSource="{Binding ViewModel.LotteryList, Mode=OneWay}"> <ItemsControl.ItemTemplate> <DataTemplate> <control:LotteryItemControl Margin="0,0,10,5" LotteryInfo="{Binding Mode=OneWay}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ui:VirtualizingItemsControl>
通過在容器中綁定ViewModel中的 LotteryList集合,在容器模板中,自定義控件通過Binding 綁定獲得對(duì)應(yīng)的屬性值,從而層層往下處理,最終呈現(xiàn)出所需要的組合型界面效果。
由于虛擬化控件容器的引入,單次展現(xiàn)幾千個(gè)記錄也不會(huì)受任何UI性能的影響,因?yàn)榻缑鎸?shí)際上就是僅僅呈現(xiàn)可見空間內(nèi)的一些控件,滾動(dòng)視圖的時(shí)候,變化了數(shù)據(jù),只是更新了已有的UI部件,因此性能不在受太大的影響,這也是我們?cè)诖罅匡@示界面元素的時(shí)候,最佳的方式了。
本文對(duì)照Winform自定義控件的開發(fā)模式和WPF自定義控件的開發(fā)模式,可以看到WPF利用虛擬化技術(shù),減少了對(duì)界面UI消耗的性能;而對(duì)于Winform GDI+的大量控件渲染導(dǎo)致性能低下的問題,唯一的方式應(yīng)該也是借鑒虛擬化容器的技術(shù)來改進(jìn)了,只是可惜目前沒有找到合適的解決方案。
在前面我介紹了常規(guī)的事件消息通知,可以采用MVVM(CommunityToolkit.Mvvm )的處理方式來實(shí)現(xiàn)消息的發(fā)送,接收處理,比較簡(jiǎn)單的解決思路。
不過如果沒有采用MVVM的,也可以考慮采用常規(guī)的WPF路由事件來處理,可以同樣達(dá)到相同的效果,只是代碼多幾行而已。
我們回顧一下,之前在介紹了Winform中,自定義控件通過自定義事件處理方式的操作,如下代碼所示。
/// <summary> /// 事件處理 /// </summary> public EventHandler<ClickEventData> ClickEventHandler { get; set; }
而WPF里面,我們采用路由事件的方式來處理相對(duì)應(yīng)的事件冒泡。
我們先為最底層的NumberItem自定義控件定義一個(gè)雙擊事件處理,如下代碼所示(由于截圖效果較好,就截圖了)。
和WPF控件的屬性定義類似,這里定義事件,需要定義屬性和注冊(cè)一個(gè)事件說明的配套。
這樣我們?cè)诳丶|發(fā)雙擊處理的時(shí)候,我們冒泡一個(gè)路由事件,并帶有事件的數(shù)據(jù),如下代碼所示 :
//事件數(shù)據(jù) var eventData = new ClickEventData(clickType, clickValue); //觸發(fā)事件通知 var args = new RoutedEventArgs(ClickHandlerEvent, eventData); this.RaiseEvent(args);
控件的路由事件,需要層層冒泡,也就是NumberItem的父控件,在攔截了事件后,需要進(jìn)行繼續(xù)冒泡的處理。因此我們?cè)贜umberItem的父控件LotteryItemControl上定義類似的事件,如下代碼所示:
我們?cè)诟缚丶袆?dòng)態(tài)創(chuàng)建子控件(NumberItem自定義控件)的時(shí)候,需要為它的事件進(jìn)行一個(gè)攔截處理,如下代碼所示。
上面代碼就是攔截了控件的事件,重新拋出封裝的事件給父容器處理 :
<ui:VirtualizingItemsControl x:Name="listControl" Grid.Row="1" Foreground="{DynamicResource TextFillColorSecondaryBrush}" ItemsSource="{Binding ViewModel.LotteryList, Mode=OneWay}" VirtualizingPanel.CacheLengthUnit="Item" VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"> <ItemsControl.ItemTemplate> <DataTemplate> <control:LotteryItemControl Margin="0,0,10,5" ClickHandler="LotteryItemControl_ClickHandler" LotteryInfo="{Binding Mode=OneWay}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ui:VirtualizingItemsControl>
上面容器模板代碼中的ClickHandler="LotteryItemControl_ClickHandler" 就是對(duì)自定義控件的事件進(jìn)行處理的邏輯。
private void LotteryItemControl_ClickHandler(object sender, RoutedEventArgs e) { if (e.OriginalSource is ClickEventData data) { //MessageDxUtil.ShowTips($"用戶單擊【{data.Value}】,類型為【{data.ClickEventType}】 "); var list = ControlHelper.FindVisualChildren<LotteryItemControl>(this.listControl); foreach (var lottery in list) { lottery.SetSelected(data); } } }
以上就是WPF中對(duì)于自定義控件的一些處理經(jīng)驗(yàn)總結(jié),在利用虛擬化容器處理的性能外,對(duì)于自定義控件的開發(fā)處理,如屬性的定義,事件的定義,或者利用MVVM消息總線的處理方式,來實(shí)現(xiàn)更彈性的WPF界面開發(fā),從而能夠?yàn)槲覀兌x復(fù)雜界面元素,重用元素的WPF應(yīng)用開發(fā)提供更好的支持。
對(duì)于其中一些自定義控件的開發(fā)場(chǎng)景,純粹是為了更好解析自定義控件的逐步封裝處理,介紹控件的逐層細(xì)化封裝,以及事件的層層通知效果,如有誤導(dǎo)敬請(qǐng)諒解。
本文轉(zhuǎn)載自
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請(qǐng)務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請(qǐng)郵件反饋至chenjj@ke049m.cn
文章轉(zhuǎn)載自: