轉帖|使用教程|編輯:龔雪|2025-01-07 10:14:16.150|閱讀 103 次
概述:本文主要介紹如何使用WPF開發自定義用戶控件及實現相關自定義事件的處理,希望對大家有所幫助和啟示~
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
相關鏈接:
對于Winform自定義的用戶控件來說,它的呈現方式主要就是基于GDI+進行渲染的,對于數量不多的控件呈現,一般不會覺察性能有太多的問題,隨著控件的數量大量的增加,就會產生性能問題,比較緩慢,或者句柄創建異常等問題。本文將為大家介紹WPF技術處理的自定義用戶控件,引入虛擬化技術的處理,較好的解決這些問題。
PS:給大家推薦一個C#開發可以用到的界面組件——DevExpress WPF,它擁有120+個控件和庫,將幫助您交付滿足甚至超出企業需求的高性能業務應用程序。通過DevExpress WPF能創建有著強大互動功能的XAML基礎應用程序,這些應用程序專注于當代客戶的需求和構建未來新一代支持觸摸的解決方案。
DevExpress技術交流群11:749942875 歡迎一起進群討論
前面例子我測試一次性在界面呈現的控件總數接近2k左右的時候,句柄就會創建異常。由于Winform控件沒有引入虛擬化技術來重用UI控件的資源,因此控件呈現量多的話,就會有嚴重的性能問題。而WPF引入的虛擬化技術后,對于UI資源的重用就會降低界面的消耗,而且即使數量再大,也不會有卡頓的問題。其原理就是UI變化還是那些內容,觸發滾動的時候,也只是對可見控件的數據進行更新,從而大量減少UI控件創建刷新的消耗。
如果接觸過IOS開發的時候,它們的處理也是一樣,在介紹列表處理綁定的時候,它本身就強制重用列表項的資源,從而達到降低UI資源消耗 的目的。
我們來介紹自定義控件之前,我們先來了解一下虛擬化的技術處理。
在WPF應用程序開發過程中,大數據量的數據展現通常都要考慮性能問題。
例如對于WPF程序來說,原始數據源數據量很大,但是某一時刻數據容器中的可見元素個數是有限的,剩余大多數元素都處于不可見狀態,如果一次性將所有的數據元素都渲染出來則會非常的消耗性能。因而可以考慮只渲染當前可視區域內的元素,當可視區域內的元素需要發生改變時,再渲染即將展現的元素,最后將不再需要展現的元素清除掉,這樣可以大大提高性能。
WPF列表控件提供的最重要功能是UI虛擬化(UI Virtaulization),UI 虛擬化是列表僅為當前顯示項創建容器對象的一種技術。
在WPF中System.Windows.Controls命名空間下的VirtualizingStackPanel可以實現數據展現的虛擬化功能,ListBox的默認元素展現容器就是它。但有時VirtualizingStackPanel的布局并不能滿足我們的實際需要,此時就需要實現自定義布局的虛擬容器了。
要想實現一個虛擬容器,并讓虛擬容器正常工作,必須滿足以下兩個條件:
我在這里首先介紹如何使用虛擬化容器控件即可,自定義的處理可以在熟悉后,參考一些代碼進行處理即可。
VirtualizingPanel從一開始就存在于 WPF 中,這提供了不必立即為可視化創建ItemsControl的所有 UI 元素的可能性。
VirtualizingPanel類中實現以下幾項依賴屬性。
VirtualizingPanel 可以通過CacheLengthUnit 設置緩存單元。可能的有:Item、Page、Pixel 幾個不同的項目,這確定了視口之前和之后的緩存大小。這樣可以避免 UI 元素只在可見時才生成。
例如對于ListBox控件的虛擬化處理,代碼如下所示。
<ListBox ItemsSource="{Binding VirtualizedBooks}" ItemTemplate="{StaticResource BookTemplate}" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.CacheLength="1,2" VirtualizingPanel.CacheLengthUnit="Page"/>
在我之前的WPF相關隨筆中,我介紹過UI部分,采用了lepoco/wpfui 的項目界面來集成處理的。
GitHub地址:
文檔地址:
lepoco/wpfui 的項目控件組中也提供了一個類似流式布局(類似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>
這個界面的效果如下所示,它的后端ViewModel的數據模型中綁定9k左右個記錄對象,而在UI虛擬化的加持下,滾動處理沒有任何卡頓,這就是其虛擬化優勢所在。
我們上面為了簡單介紹呈現的效果,主要在模板里面放置了一個簡單的按鈕控件來定義顏色塊,開發的界面往往相對會復雜一些,如果不太考慮重用界面元素,簡單的對象組裝可以在這個 DataTemplate 模板里面進行處理,如下代碼所示。
<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="左側" TextAlignment="Center" /> <TextBlock Grid.Column="1" FontWeight="Black" Foreground="Blue" Text="右側" TextAlignment="Center" /> </Grid> </Grid> </DataTemplate> </ItemsControl.ItemTemplate> </ui:VirtualizingItemsControl>
通過我們自定義的Grid布局,很好的組織起來相關的自定義控件的界面效果,會得到項目的界面效果。
前面介紹了一些基礎的虛擬化控件容器和一些常規的自定義控件內容的只是,我們在開發桌面程序的時候,為了方便重用等原因,往往把一些復雜的界面元素逐層分解,組合成一些自定義的控件,然后組裝層更高級的自定義控件,這樣就可以構建界面和邏輯比較復雜的一些界面元素了。
前面文章中介紹,為了使用戶控件更加規范化,我們可以定義一個接口,聲明相關的屬性和處理方法,如下代碼所示。(這部分WPF和Winform自定義控件開發一樣處理)
/// <summary> /// 自定義控件的接口 /// </summary> public interface INumber { /// <summary> /// 數字 /// </summary> string Number { get; set; } /// <summary> /// 數值顏色 /// </summary> Color Color { get; set; } /// <summary> /// 顯示文本 /// </summary> string Animal { get; set; } /// <summary> /// 顯示文本 /// </summary> string WuHan { get; set; } /// <summary> /// 設置選中的內容的處理 /// </summary> /// <param name="data">事件數據</param> void SetSelected(ClickEventData data); }
和WInform開發一樣,WPF也是創建一個自定義的控件,在項目上右鍵添加自定義控件,如下界面所示。
我們同樣命名為NumberItem,最終后臺Xaml的C#代碼生成如下所示(我們讓它繼承接口 INumber )。
/// <summary> /// NumberItem.xaml 的交互邏輯 /// </summary> public partial class NumberItem : UserControl, INumber
WPF自定義控件實現接口的屬性定義,不是簡單的處理,需要按照WPF的屬性處理規則,這里和Winform處理有些小差異。
/// <summary> /// NumberItem.xaml 的交互邏輯 /// </summary> public partial class NumberItem : UserControl, INumber { #region 控件屬性定義 /// <summary> /// 數字 /// </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 的操作實現,同時需要定義一個靜態變量 DependencyProperty 的屬性定義,如 ***Property。
這個是WPF屬性的常規處理,沒增加一個屬性名稱,就增加一個對應類型DependencyProperty 的**Property,如下所示。
public static readonly DependencyProperty ColorProperty = DependencyProperty.Register( nameof(Color), typeof(Color), typeof(NumberItem), new FrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
自定義控件的事件通知,有兩種處理方法,可以通過常規事件的冒泡層層推送到界面頂端處理,也可以使用MVVM的消息通知(類似消息總線的處理),我們先來介紹MVVM的消息通知,因為它最為簡單易用。
而這里所說的MVVM包,是指微軟的 CommunityToolkit.Mvvm的組件包,有興趣可以全面了解一下。
CommunityToolkit.Mvvm (又名 MVVM 工具包,以前名為 Microsoft.Toolkit.Mvvm) 是一個現代、快速且模塊化的 MVVM 庫。 它是 .NET 社區工具包的一部分,圍繞以下原則構建:
MVVM 工具包由 Microsoft 維護和發布,是 .NET Foundation 的一部分,它還由內置于 Windows 中的多個第一方應用程序使用。
此包面向 .NET Standard,因此可在任何應用平臺上使用:UWP、WinForms、WPF、Xamarin、Uno 等;和在任何運行時上:.NET Native、.NET Core、.NET Framework或 Mono。 它在所有它們上運行。 API 圖面在所有情況下都是相同的,因此非常適合生成共享庫。
官網介紹地址:
CommunityToolkit.Mvvm 類型包括如下列表,它的便利之處,主要通過標記式的特性(Attribute)來實現相關的代碼的生成,簡化了原來的代碼。
例如我們對于自定義控件的文本信息,雙擊觸發自定義控件事件處理,它的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>
我們雙擊文本的時候,觸發一個DoubleClickCommand 的命令。其里面主要核心就是利用MVVM推送一條消息即可,如下代碼所示。
//發送MVVM消息信息通知方式(一) WeakReferenceMessenger.Default.Send(new ClickEventMessage(eventData));
而其中 ClickEventMessage 是我們根據要求定義的一個消息對象類,如下代碼所示。
完整的Command命令如下所示。
/// <summary> /// 雙擊觸發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值邏輯//事件數據 var eventData = new ClickEventData(clickType, clickValue); //發送MVVM消息信息通知方式(一) WeakReferenceMessenger.Default.Send(new ClickEventMessage(eventData)); }
通過這樣的消息發送,就需要有個地方來接收這個信息的,我們在需要處理事件的父窗口中攔截處理消息即可。
//處理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 的輔助類主要就是根據父對象,遞歸獲得下面指定類型的控件集合,其主要是通過系統輔助類VisualTreeHelper進行控件遞歸的查詢處理,這里不再深入介紹。
上面的邏輯,就是獲得控件的消息后,對該容器的控件遞歸獲得指定類型的控件,然后對容器中的控件逐一進行SetSelected的選中處理,從而改變控件的繪制狀態。
而LotteryItemControl就是一個比NumberItem自定義控件,更高一層的界面組織者,也是一個自定義用戶控件。
里面就是放置多個NumberItem自定義控件,組織起來呈現一定的規則排列即可。
自定義控件同樣需要綁定一個屬性LotteryInfo,以及WPF屬性LotteryInfoProperty。在屬性變化的時候,觸發界面控件數據的綁定處理即可。
其中InitData就是對里面的控件內容逐一更新顯示即可,這里由于篇幅原因不再介紹太細節的地方。
完成了較高層次的自定義控件開發后,我們最后一步就是把這些自定義控件,通過虛擬化的控件容器方式來呈現出來,如下代碼所示。
<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 綁定獲得對應的屬性值,從而層層往下處理,最終呈現出所需要的組合型界面效果。
由于虛擬化控件容器的引入,單次展現幾千個記錄也不會受任何UI性能的影響,因為界面實際上就是僅僅呈現可見空間內的一些控件,滾動視圖的時候,變化了數據,只是更新了已有的UI部件,因此性能不在受太大的影響,這也是我們在大量顯示界面元素的時候,最佳的方式了。
本文對照Winform自定義控件的開發模式和WPF自定義控件的開發模式,可以看到WPF利用虛擬化技術,減少了對界面UI消耗的性能;而對于Winform GDI+的大量控件渲染導致性能低下的問題,唯一的方式應該也是借鑒虛擬化容器的技術來改進了,只是可惜目前沒有找到合適的解決方案。
在前面我介紹了常規的事件消息通知,可以采用MVVM(CommunityToolkit.Mvvm )的處理方式來實現消息的發送,接收處理,比較簡單的解決思路。
不過如果沒有采用MVVM的,也可以考慮采用常規的WPF路由事件來處理,可以同樣達到相同的效果,只是代碼多幾行而已。
我們回顧一下,之前在介紹了Winform中,自定義控件通過自定義事件處理方式的操作,如下代碼所示。
/// <summary> /// 事件處理 /// </summary> public EventHandler<ClickEventData> ClickEventHandler { get; set; }
而WPF里面,我們采用路由事件的方式來處理相對應的事件冒泡。
我們先為最底層的NumberItem自定義控件定義一個雙擊事件處理,如下代碼所示(由于截圖效果較好,就截圖了)。
和WPF控件的屬性定義類似,這里定義事件,需要定義屬性和注冊一個事件說明的配套。
這樣我們在控件觸發雙擊處理的時候,我們冒泡一個路由事件,并帶有事件的數據,如下代碼所示 :
//事件數據 var eventData = new ClickEventData(clickType, clickValue); //觸發事件通知 var args = new RoutedEventArgs(ClickHandlerEvent, eventData); this.RaiseEvent(args);
控件的路由事件,需要層層冒泡,也就是NumberItem的父控件,在攔截了事件后,需要進行繼續冒泡的處理。因此我們在NumberItem的父控件LotteryItemControl上定義類似的事件,如下代碼所示:
我們在父控件中動態創建子控件(NumberItem自定義控件)的時候,需要為它的事件進行一個攔截處理,如下代碼所示。
上面代碼就是攔截了控件的事件,重新拋出封裝的事件給父容器處理 :
<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" 就是對自定義控件的事件進行處理的邏輯。
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中對于自定義控件的一些處理經驗總結,在利用虛擬化容器處理的性能外,對于自定義控件的開發處理,如屬性的定義,事件的定義,或者利用MVVM消息總線的處理方式,來實現更彈性的WPF界面開發,從而能夠為我們定義復雜界面元素,重用元素的WPF應用開發提供更好的支持。
對于其中一些自定義控件的開發場景,純粹是為了更好解析自定義控件的逐步封裝處理,介紹控件的逐層細化封裝,以及事件的層層通知效果,如有誤導敬請諒解。
本文轉載自
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn
文章轉載自: