使用WPF开发自定义用户控件,以及实现相关自定义事件的处理
在前面随笔《
使用Winform开发自定义用户控件,以及实现相关自定义事件的处理
》中介绍了Winform用户自定义控件的处理,对于Winform自定义的用户控件来说,它的呈现方式主要就是基于GDI+进行渲染的,对于数量不多的控件呈现,一般不会觉察性能有太多的问题,随着控件的数量大量的增加,就会产生性能问题,比较缓慢,或者句柄创建异常等问题。本篇随笔介绍WPF技术处理的自定义用户控件,引入虚拟化技术的处理,较好的解决这些问题。
前面例子我测试一次性在界面呈现的控件总数接近2k左右的时候,句柄就会创建异常。由于Winform控件没有引入虚拟化技术来重用UI控件的资源,因此控件呈现量多的话,就会有严重的性能问题。而WPF引入的虚拟化技术后,对于UI资源的重用就会降低界面的消耗,而且即使数量再大,也不会有卡顿的问题。其原理就是UI变化还是那些内容,触发滚动的时候,也只是对可见控件的数据进行更新,从而大量减少UI控件创建刷新的消耗。
如果接触过IOS开发的时候,它们的处理也是一样,在介绍列表处理绑定的时候,它本身就强制重用列表项的资源,从而达到降低UI资源消耗 的目的。
1、WPF对于简单的用户控件和虚拟化的处理
我们来介绍自定义控件之前,我们先来了解一下虚拟化的技术处理。
在WPF应用程序开发过程中,大数据量的数据展现通常都要考虑性能问题。
例如对于WPF程序来说,原始数据源数据量很大,但是某一时刻数据容器中的可见元素个数是有限的,剩余大多数元素都处于不可见状态,如果一次性将所有的数据元素都渲染出来则会非常的消耗性能。因而可以考虑只渲染当前可视区域内的元素,当可视区域内的元素需要发生改变时,再渲染即将展现的元素,最后将不再需要展现的元素清除掉,这样可以大大提高性能。
WPF列表控件提供的最重要功能是UI虚拟化(UI Virtaulization),UI 虚拟化是列表仅为当前显示项创建容器对象的一种技术。
在WPF中System.Windows.Controls命名空间下的VirtualizingStackPanel可以实现数据展现的虚拟化功能,ListBox的默认元素展现容器就是它。但有时VirtualizingStackPanel的布局并不能满足我们的实际需要,此时就需要实现自定义布局的虚拟容器了。
要想实现一个虚拟容器,并让虚拟容器正常工作,必须满足以下两个条件:
1、容器继承自System.Windows.Controls.VirtualizingPanel,并实现子元素的实例化、虚拟化及布局处理。
2、虚拟容器要做为一个System.Windows.Controls.ItemsControl(或继承自ItemsControl的类)实例的ItemsPanel(实际上是定义一个ItemsPanelTemplate)
我在这里首先介绍如何使用虚拟化容器控件即可,自定义的处理可以在熟悉后,参考一些代码进行处理即可。
VirtualizingPanel从一开始就存在于 WPF 中。这提供了不必立即为可视化创建ItemsControl的所有 UI 元素的可能性。
VirtualizingPanel类中实现以下几项依赖属性。
- CacheLength/CacheLengthUnit
- IsContainerVirtualizable
- IsVirtualizing
- IsVirtualizingWhenGrouping
- ScrollUnit
- VirtualizationMode
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地址:
https://github.com/lepoco/wpfui
文档地址:
https://wpfui.lepo.co/documentation/
lepoco/wpfui 的项目控件组中也提供了一个类似流式布局(类似Winform的FlowLayoutPanel)的虚拟化控件VirtualizingItemsControl,比较好用,我们借鉴来介绍一下。
<ui:VirtualizingItemsControlForeground="{DynamicResource TextFillColorSecondaryBrush}"ItemsSource="{Binding ViewModel.Colors, Mode=OneWay}"VirtualizingPanel.CacheLengthUnit="Item"> <ItemsControl.ItemTemplate> <DataTemplateDataType="{x:Type models:DataColor}"> <ui:ButtonWidth="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:VirtualizingItemsControlForeground="{DynamicResource TextFillColorSecondaryBrush}"ItemsSource="{Binding ViewModel.Colors, Mode=OneWay}"VirtualizingPanel.CacheLengthUnit="Item"> <ItemsControl.ItemTemplate> <DataTemplateDataType="{x:Type models:DataColor}"> <Grid> <Grid.RowDefinitions> <RowDefinitionHeight="auto" /> <RowDefinitionHeight="50" /> </Grid.RowDefinitions> <ui:ButtonGrid.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" /> <GridGrid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinitionWidth="20*" /> <ColumnDefinitionWidth="20*" /> </Grid.ColumnDefinitions> <TextBlockGrid.Column="0"FontWeight="Bold"Foreground="Red"Text="左侧"TextAlignment="Center" /> <TextBlockGrid.Column="1"FontWeight="Black"Foreground="Blue"Text="右侧"TextAlignment="Center" /> </Grid> </Grid> </DataTemplate> </ItemsControl.ItemTemplate> </ui:VirtualizingItemsControl>
通过我们自定义的Grid布局,很好的组织起来相关的自定义控件的界面效果,会得到项目的界面效果。
2、WPF自定义控件的处理
前面介绍了一些基础的虚拟化控件容器和一些常规的自定义控件内容的只是,我们在开发桌面程序的时候,为了方便重用等原因,我们往往把一些复杂的界面元素逐层分解,组合成一些自定义的控件,然后组装层更高级的自定义控件,这样就可以构建界面和逻辑比较复杂的一些界面元素了。
在前面随笔《
使用Winform开发自定义用户控件,以及实现相关自定义事件的处理
》中介绍了Winform用户自定义控件的处理,其实WPF的处理思路也是类似,只是具体细节有所差异而已。
前面文章中介绍,为了使用户控件更加规范化,我们可以定义一个接口,声明相关的属性和处理方法,如下代码所示。(这部分WPF和Winform自定义控件开发一样处理)
/// <summary> ///自定义控件的接口/// </summary> public interfaceINumber
{/// <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> voidSetSelected(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 classNumberItem : UserControl, INumber
{#region 控件属性定义 /// <summary> ///数字/// </summary> public stringNumber
{get { return (string)GetValue(NumberProperty); }set{ SetValue(NumberProperty, value); }
}/// <summary> ///颜色/// </summary> publicColor Color
{get { return(Color)GetValue(ColorProperty); }set{ SetValue(ColorProperty, value); }
}/// <summary> ///显示文本/// </summary> public stringAnimal
{get { return (string)GetValue(AnimalProperty); }set{ SetValue(AnimalProperty, value); }
}/// <summary> ///显示文本/// </summary> public stringWuHan
{get { return (string)GetValue(WuHanProperty); }set{ SetValue(WuHanProperty, value); }
}public static readonly DependencyProperty ColorProperty =DependencyProperty.Register(
nameof(Color),typeof(Color), typeof(NumberItem), newFrameworkPropertyMetadata(Colors.Transparent, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));public static readonly DependencyProperty NumberProperty =DependencyProperty.Register(
nameof(Number),typeof(string), typeof(NumberItem), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, newPropertyChangedCallback(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 DependencyPropertyColorProperty =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 社区工具包的一部分,围绕以下原则构建:
平台和运行时独立 - .NET Standard 2.0、 .NET Standard 2.1 和 .NET 6