C# wpf
学习网址:控件的父类们 - WPF中文网 - 从小白到大佬
控件的父类:
由此我们可以得出结论,控件的父类们(准确的说,应该叫父类的父类的父类),至少有如下几个类型:
- DispatcherObject
- DependencyObject
- Visual
- UIElement
- FrameworkElement
WPF几乎所有的控件都从上面这五个父类继承,它们的相互继承关系,形成了一棵树。
DispatcherObject类
在UI线程上给你们提供一个中间商Dispatcher(调度员),将Dispatcher放到一个抽象类DispatcherObject,然后我向你保证,我所有的控件都从这个DispatcherObject类继承,这样当你们在后台线程中要访问控件时,就可以从控件中找到那位中间商Dispatcher,由中间商来完成你要对控件的操作访问。
从此,DispatcherObject在WPF的世界中,便登上了至高无上的宝座,成为了几乎所有类型的终极基类。
需要在后台线程中去操作控件,于是Dispatcher调度员提供了Invoke和BeginInvoke两个方法,供我们可以安全的访问UI线程中的控件。
官方解释:
在 WPF 中, DispatcherObject 只能由 Dispatcher 它与之关联的访问。 例如,后台线程无法更新与 Dispatcher UI 线程上关联的内容Button。 为了使后台线程访问该 Content 属性 Button,后台线程必须将工作委托给 Dispatcher 与 UI 线程关联的工作。 这是通过使用 Invoke 或BeginInvoke。 Invoke 是同步的, BeginInvoke 是异步的。 操作将添加到指定DispatcherPriority位置的队列Dispatcher中。
namespace HelloWorld
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Task.Factory.StartNew(() =>
{
//Task.Delay(3000)
:创建一个表示 3 秒延迟的Task
对象。它不会阻塞当前线程,而是返回一个表示延迟的异步任务。
//.Wait()
:阻塞当前线程,直到Task
完成。在这里,Wait()
会等待Task.Delay(3000)
的 3 秒延迟完成。Task.Delay(3000).Wait();
//button:前端UI//在后端想改变前端数据时使用:
button.Dispatcher.Invoke(() =>
{
button.Content = "www.wpfsoft.com";
});
});
}
}
}
- 提供方法以检查 (CheckAccess) 和验证 (VerifyAccess) 某个线程是否有权访问对象(派生于 DispatcherObject)。CheckAccess 与 VerifyAccess 的区别在于 CheckAccess 返回一个布尔值,表示当前线程是否有可以使用的对象,而 VerifyAccess 则在线程无权访问对象的情况下引发异常。
DependencyObject类
控件的属性不再被直接赋值,而是绑定了另一个”变量“,当这个”变量“发生改变时,控件的属性也会跟着改变,这样的属性也被称为依赖属性。
DependencyObject 类表示参与依赖属性系统的对象。属性系统的主要功能是计算属性的值,并提供有关已更改的值的系统通知。 参与属性系统的另一个类 DependencyProperty。 DependencyProperty 允许将依赖属性注册到属性系统,并提供有关每个依赖属性的标识和信息,而 DependencyObject 为基类,使对象能够使用此依赖属性。
INotifyPropertyChanged 类用于通知UI刷新,注重的仅仅是数据更新后的通知。DependencyObject 类用于给UI添加依赖和附加属性,注重数据与UI的关联。如果简单的数据通知,两者都可以实现的。
Visual类
Visual类是WPF框架中第三个父类,主要是为 WPF 中的呈现提供支持,其中包括命中测试、坐标转换和边界框计算。它位于程序集:PresentationCore.dll库文件中,它的命名空间:System.Windows.Media。
Visual 类是派生每个 FrameworkElement 对象的基本抽象。 该类还用作在 WPF 中编写新控件的入口点,在 Win32 应用程序模型中,该类在许多方面可视为窗口句柄 (HWND)。Visual 对象是一个核心 WPF 对象,它的主要作用是提供呈现支持。 用户界面控件如 Button 和 TextBox)派生自 Visual 类,并使用该类来保存它们的呈现数据。 Visual 对象为以下项提供支持:
输出显示:呈现视觉对象的持久、序列化的绘图内容。
转换:针对视觉对象执行转换。
剪裁:为视觉对象提供剪裁区域支持。
命中测试:确定坐标或几何形状是否包含在视觉对象的边界内。
边框计算:确定视觉对象的边框。
所有控件都继承了Visual类,控件在绘制到界面的过程中,涉及到转换、裁剪、边框计算等功能,都是使用了Visual父类的功能。
UIElement类
UIElement类继承了Visual类,在WPF框架中排行老四(第4个基类)。它位于程序集:PresentationCore.dll之中,命名空间:System.Windows。
第一部分 路由事件
UIElement基类定义了大量的路由事件。什么是路由事件?路由事件和xaml的可视化树概念相关,控件的事件被触发后,会沿着这棵树广播,有两个方向,要么往树的根部广播,要么往树的枝叶广播,如果不广播就是直接事件。
所以,路由事件分为冒泡事件和隧道事件,冒泡,是从触发源为出发点,依次传递到父节点,直到最后的根节点。隧道事件是不管谁是触发源,都是从根节点触发,到子节点,直到触发节点。
从空间上来说,冒泡事件和隧道事件是成对出现的。从时间来说,都是先触发隧道事件,然后是冒泡事件。从命名来说,隧道事件都是以Preview开头的事件。
根据命名规则,我们可以大致猜测出一个结果,带Key的基本都是与键盘相关的事件(如按下键位、抬起键位),带Mouse的基本都是与鼠标相关的事件(如左键单击、双击),带Stylus的基本都是与触摸相关的事件,具体用到哪一类型的事件,再详细查阅一下相关说明文档即可。
重点:关于这些事件的回调函数,即以On开头的方法成员,都被声明成了protected virtual,意思是他们都可以被重载,这使得我们在开发业务时更加方便。
第二部分 依赖属性
UIElement基类还定义了大量的依赖属性。前面的章节中,在DependencyObject类中我们简单提到过依赖属性。在这里我们以UIElement基类的Visibility属性为例。
public Visibility Visibility { get; set; } | |
public static readonly DependencyProperty VisibilityProperty; |
上面有两个成员,Visibility是普通的属性成员,VisibilityProperty是WPF的依赖属性成员,以Property结尾的字样作为WPF的依赖属性命名规则。而这两个成员合起来,才能被称为一个完整的依赖属性。这个Visibility 属性表示设置或获取控件的可见性。当我们要设置控件的可见性时,只需要如下设置即可。
<TextBlock Text="WPF中文网" | |
Visibility="Visible" | |
FontSize="48" | |
HorizontalAlignment="Center" | |
VerticalAlignment="Center"/> |
Visibility实际上是一个枚举,它包含3个值,分别是Visible、Hidden、Collapsed。其含义分别是显示、隐藏、彻底隐藏(不占布局位置)。
Visibility 状态会影响该元素的所有输入处理。 不可见的元素不会参与命中测试,也不会接收输入事件,即使鼠标位于元素可见时所在的边界上也是如此。
但是在这一节中,我们只是探讨UIElement基类提供了哪些方面的属性,并不详细探讨依赖属性,所以下面我们把目光聚焦到UIElement基类的常用属性上。另外由于WPF中几乎所有控件都继承了这个基类,意思就是说所有的控件都有这些属性可以使用。下面我在描述的时候将采用“控件”两字来解释一些技术细节。
Uid属性:获取或设置控件的唯一标识符,像人们的身份证一样。这个值默认是string.Empty。
Visibility属性:获取或设置控件的可见性。默认是Visible。
ClipToBounds属性:如果该值为true,表示进行裁剪,以适配它的父控件。比如有时候我们外面有一个Panel,里面的控件尺寸太大,势必会“撑破”外面的父控件,为了布局美观,只好削足适履。
Clip属性:用于剪裁区域大小的几何图形。需要注意的是,这个属性和上面的ClipToBounds属性是有区别的。ClipToBounds是裁剪控件自身,Clip是裁剪控件里面的内容。比如Image图像控件,我们在显示一张图时,就可以运用Clip进行裁剪后显示,通常在显示用户头像时裁剪成圆形时使用。如下所示
<Image
Source="sampleImages\Waterlilies.jpg"
Width="200" Height="150" HorizontalAlignment="Left">
<Image.Clip>
<EllipseGeometry
RadiusX="100"
RadiusY="75"
Center="100,75"/>
</Image.Clip>
</Image>
SnapsToDevicePixels属性:如果该值为true,表示控件的呈现是否应使用特定于设备的像素设置。意思是开启后可以最大限度的防锯齿效果,默认为false。
IsFocused属性:这是一个只读属性,表示当前控件是否有焦点。
IsEnabled属性:如果该值为true,表示禁用控件,反之启用控件。
IsHitTestVisible属性:获取或设置一个值,该值声明是否可以返回此元素作为其呈现内容的某些部分的点击测试结果。
IsVisible属性:这是一个只读属性,表示当前控件是否显示。
Focusable属性:如果该值为true,表示控件可以得到焦点,大部份内容控件都默认可以获得焦点。
IsKeyboardFocused属性:表示该控件是否具有键盘焦点。
IsMouseOver属性:表示鼠标是否在控件上面。通常在设计控件的样式(Style)时会用到。
IsStylusOver属性:表示触笔指针是否在控件的上方。
IsSealed属性:表示当前类型是否为只读类。
Opacity属性:设置控件的透明度,取值范围是0-1之间的double值。
OpacityMask属性:设置一个画笔,作为控件的蒙板。比如我们给一张图片设置一个掩码,就可以使用ImageBrush这种图片画笔来实现。
AllowDrop属性:表示控件是否允许拖拽操作。
RenderTransform属性:(非常重要)如果要设置控件的移动、缩放、旋转,需要这此属性进行设置。
总结
通过上述的代码分析,我们大致可以得出以下结论,UIElement基类为我们提供了一系列的鼠标、键盘和触摸事件,并提供了一些常用的依赖属性。它可以呈现继承它的所有控件,为控件布局时调整位置和大小,响应用户的输入,引发一系列的路由事件,并继承了IAnimatable动画接口,用于支持动画的某些方面。
FrameworkElement类
它是WPF控件的众多父类中最核心的基类,从这里开始,继承树开始分支,分别是Shape图形类、Control控件类和Panel布局类三个方向。
FrameworkElement类本质上也是提供了一系列属性、方法和事件。同时又扩展 UIElement 并添加了以下功能:
官方文档
1.布局系统定义: FrameworkElement 为中 UIElement定义为虚拟成员的某些方法提供特定的 WPF 框架级实现。 最值得注意的是, FrameworkElement 会密封某些 WPF 核心级布局替代,并改为提供派生类应替代的 WPF 框架级别的等效项。 例如,密封但 FrameworkElementArrangeCore 提供 ArrangeOverride。 这些更改反映了这样一个事实,即在 WPF 框架级别,有一个可以呈现任何 FrameworkElement 派生类的完整布局系统。 在 WPF 核心级别,将构建基于 WPF 的常规布局解决方案的某些成员已就位,但未定义布局系统的实际引擎。
2.逻辑树: 常规 WPF 编程模型通常以元素树的方式表示。 支持将元素树表示为逻辑树,以及支持在标记中定义该树的支持是在 级别实现的 FrameworkElement 。 但请注意, FrameworkElement 故意不定义内容模型,并将该责任留给派生类。
3.对象生存期事件: 了解何时初始化元素 (调用构造函数) 或首次将元素加载到逻辑树中时,这通常很有用。 FrameworkElement 定义多个与对象生存期相关的事件,这些事件为涉及元素的代码隐藏操作(例如添加更多子元素)提供有用的挂钩。
4.支持数据绑定和动态资源引用: 对数据绑定和资源的属性级支持由 DependencyProperty 类实现,并体现在属性系统中,但解析存储为 Expression (数据绑定和动态资源的编程构造) 中存储的成员值的能力由 FrameworkElement实现。
5.风格:FrameworkElement 定义 Style 属性。 但是, FrameworkElement 尚未定义对模板的支持或支持修饰器。 这些功能由控件类(如 和 ContentControl)Control引入。
6.更多动画支持: 某些动画支持已在 WPF 核心级别定义,但 FrameworkElement 通过实现 BeginStoryboard 和相关成员扩展了此支持。
属性分析
1.LayoutTransform 属性:获取或设置在执行布局时应应用于此元素的图形转换。这个属性与UIElement类中的RenderTransform属性有相似之处,所以我们在此将两者进行对比说明一下。两个属性都是Transform类型,而Transform是一个抽象类,这个类可以实现控件在平面中的各种转换,包括
- 旋转 (System.Windows.Media.RotateTransform)
- 缩放 (System.Windows.Media.ScaleTransform)、
- 倾斜 (System.Windows.Media.SkewTransform) 、
- 平移 (System.Windows.Media.TranslateTransform)。
虽然两个属性都可以达到控件的变换效果,但是两者还是有区别的。LayoutTransform属性是在控件布局之前对控件进行变换,而RenderTransform属性是在布局结束后执行控件的变换,LayoutTransform开销比RenderTransform要大,所以,尽量使用RenderTransform属性去实现控件的变换。(我们会在后面专门探讨控件的变换)
2.Width属性:这是表示控件的宽度。与之相关的还有以下几个属性。
- ActualWidth:获取此元素的呈现宽度。只读属性。
- MaxWidth:获取或设置一个控件的最大宽度。
- MinWidth:获取或设置一个控件的最小宽度。
3.Height属性:这是表示控件的高度,与之相关的还有以下几个属性。
- ActualHeight:获取此元素的呈现高度。只读属性。
- MaxHeight:获取或设置一个控件的最大高度。
- MinHeight:获取或设置一个控件的最小高度。
4.Tag属性:这个属性非常重要,它是object类型,意味着可以保存任意类型的对象值。它就像FrameworkElement类身上的一个小口袋,但确能容纳万物。我们通常会将一些与控件相关的数据临时存放在Tag属性中,当把控件作为参数传递时,小口袋里面的对象也随之传递过去了。
5.Name属性:获取或设置控件的标识名称。在同一个窗体、页、用户控件中,Name标识是唯一的。设置了控件的名称后,我们就可以在后端代码直接以这个标识去引用控件。
6.Margin属性:获取或设置控件的外边距。
Padding属性说明
与Margin相对应的是Padding,表示设置控件的内边距。但是这个属性并不在FrameworkElement中,而在Control类中,从本节第一张图所示,说明只有内容控件才具有Padding,而Shape和Panel是没有Padding属性的。
7.HorizontalAlignment属性:设置控件的水平对齐方式。这个对齐方式是相对于父元素而言的,比如我们有一个Button控件,在外面还包裹了一层Grid控件,那么,设置Button控件的HorizontalAlignment属性,可以将Button控件分别显示在Grid控件的左边、中间、右边三个位置。
8.VerticalAlignment属性:设置控件的垂直对齐方式。与HorizontalAlignment属性类似,只是对方的方向不同,可以设置控件在垂直方向上是居于顶部、中间、还是底部三个位置。
总结:上述两个属性的值都是枚举型,它们都有一个共同的值,那就是stretch,表示是拉伸的方式填充父元素的布局。
9.ToolTip属性:获取或设置用户界面 (UI) 中为此元素显示的工具提示对象。指鼠标移到控件上方时显示的提示内容,它是一个object类型,意味着可以显示任意布局外观。
10.Parent属性:获取此元素的逻辑父元素。它是一个只读属性。
接下来,我们将介绍几个比较重要的属性,这些属性是WPF框架中非常核心的知识概念.
WPF样式(Style)
11.Style属性:获取或设置此元素呈现时所使用的样式。与Style相关的还有一个属性,叫FocusVisualStyle,顾名思义,控件在获得焦点时的样式。
WPF资源(ResourceDictionary)
什么是资源?资源,也就是资源字典,也就是ResourceDictionary类,它提供一个哈希表/字典实现,其中包含组件所使用的 WPF 资源以及 WPF 应用程序的其他元素。我们可以把WPF的控件、窗体、Application应用所用到的一切资源都放在其中,将多个ResourceDictionary元素合并起来形成一个ResourceDictionary元素(ResourceDictionary也是一个隐式集合)。所以FrameworkElement类设计一个资源属性。
12.Resources 属性:获取或设置本地定义的资源字典。关于Resources资源我们会专门拿一章节来探讨)
WPF的数据上下文(DataContext)
假定我们有一个View窗体,窗体有一个TextBox控件;又假如我们还有一个ViewModel实体,这个实体中有一个叫Name的属性。如果我们要将TextBox控件的Text属性和ViewModel实体的Name属性成功的建立绑定关系,必备的条件是什么?
首先,由于View窗体继承于FrameworkElement类,所以每个窗体(或控件)都有一个叫DataContext的数据上下文属性。所以必备的条件是:ViewModel实体必须先赋值给View窗体的DataContext,ViewModel的Name属性才能绑定到TextBox控件的Text属性。换言之,领导之间要先搭好桥,下属和下属才好配合工作。这就是DataContext的概念和用途。
13.DataContext属性:获取或设置元素参与数据绑定时的数据上下文。
14.ContextMenu属性:设置与获取控件的上下文菜单 ,就是鼠标在控件上右键时弹出来的菜单。
15.Cursor属性:获取或设置在鼠标指针位于此元素上时显示的光标。
事件分析
FrameworkElement类提供了12个事件,一般比较常用的是:Initialized、Loaded、Unloaded、SizeChanged等事件。
1.FindName(String):表示查找某个元素。比如我们在窗体中要查找某个控件。
2.FindResource(Object):查找某个资源。如果在调用对象上找不到该资源,则接下来搜索逻辑树中的父元素,然后搜索应用程序、主题,最后搜索系统资源。实在找不到就抛出异常。
3.TryFindResource(Object):尝试去找某个资源。建议使用这个方法。
4.RegisterName (string , object );注册控件的名称到父控件上。
5.SetBinding(DependencyProperty, BindingBase)和SetBinding(DependencyProperty, String),这两个成员都和绑定相关,我们将在后面做专题介绍。
布局控件概述
控件名称 | 布局方式 |
Grid | 网格,根据自定义行和列来设置控件的布局 |
StackPanel | 栈式面板,包含的元素在竖直或水平方向排成一条直线 |
WrapPanel | 自动折行面板,包含的元素在排满一行后,自动换行 |
DockPanel | 泊靠式面板,内部的元素可以选择泊靠方向 |
UniformGrid | 网格,UniformGrid就是Grid的简化版,每个单元格的大小相同。 |
Canvas | 画布,内部元素根据像素为单位绝对坐标进行定位 |
Border | 装饰的控件,此控件用于绘制边框及背景,在Border中只能有一个子控件 |
Panel基类
Panel其实是一个抽象类,不可以实例化,WPF所有的布局控件都从Panel继承而来,
它有一个Background属性,意味着所有的布局控件都可以设置背景颜色。另外,它还有一个Children属性,这是一个集合属性,也就是说,所有的布局控件都可以添加多个子元素。这一点从它继承的IAddChild接口也能得到印证。
Panel提供了GetZIndex和SetZIndex方法成员,分别表示获取某个元素的ZIndex顺序和设置某个元素的ZIndex顺序。
什么是ZIndex?这是Panel提供的一个附加属性。假如一个单行单列的Grid布局控件中有两个Button,正常情况下,这两个Button都会以撑满Grid的方式呈现在Grid中,那么,到底哪一个Button在上面,哪一个Button在下面呢?就看这两个Button的Panel.ZIndex附加属性的值,值越大越在上面,而值较小的那个Button将被上面的Button遮盖,从而在视觉上,用户只能看到一个Button。
附加属性
附加属性的一个用途是允许子元素存储实际上由父元素定义的属性的唯一值。 此功能的一项应用是让子元素通知父级它们希望如何在用户界面 (UI) 中呈现,这对应用程序布局非常有用。
<Grid > |
<Button Content="WPF中文网1" Panel.ZIndex="1" Margin="20 40 60 80" Padding="50" /> |
<Button Content="WPF中文网2" Panel.ZIndex="0" Margin="20 40 60 80" Padding="50" /> |
</Grid> |
WPF 提供了一套全面的派生 Panel 实现,可实现许多复杂的布局。这里有一个非常非常需要注意的事项,那就是Panel的Background属性。有时候我们希望在布局控件上实现鼠标点击事件的获取,请记得一定要给Background属性设置一个颜色值,如果不希望有具体的颜色,那就设置成Transparent 。不然,您会踩坑的!因为布局控件的Background属性没有值时,是不能引发鼠标相关事件的。
只要继承于UIElement的类(或控件),都可以添加到Panel或Panel子类的Children中,从而在前端呈现出来
一个Panel 的呈现就是测量和排列子控件,然后在屏幕上绘制它们。所以在布局的过程中会经过一系列的计算,那么子控件越多,执行的计算次数就越多,则性能就会变差。如果不需要进行复杂的布局,则尽量少用复杂布局控件(如 Grid和自定义复杂的Panel);如果能简单布局实现就尽量使用构造相对简单的布局(如 Canvas、UniformGrid等),这种布局可带来更好的性能。 如果有可能,我们应尽量避免调用 UpdateLayout方法。
布局系统为Panel中的每个子控件完成两个处理过程:测量处理过程(Measure)和排列处理过程(Arrange)。每个子 Panel 均提供自己的 MeasureOverride 和 ArrangeOverride 方法,以实现自己特定的布局行为。
Grid控件(网格布局)
Grid有两个非常关键的属性ColumnDefinitions和RowDefinitions,分别表示列的数量集合和行的数量集合。ColumnDefinitions集合中的元素类型是ColumnDefinition类,RowDefinitions集合中元素类型是RowDefinition类。默认的Gridr控件没有定义行数和列数,也就是说,Grid默认情况下,行数和列数都等于1,那么它就只有一个单元格。
//一行两列
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Content="WPF中文网1" Panel.ZIndex="1" Margin="20" Padding="50" />
<Button Grid.Column="1" Content="WPF中文网2" Panel.ZIndex="0" Margin="20" Padding="50" />
</Grid>//两行一列
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Grid.Row="0" Content="WPF中文网1" Panel.ZIndex="1" Margin="20" Padding="50" />
<Button Grid.Row="1" Content="WPF中文网2" Panel.ZIndex="0" Margin="20" Padding="50" />
</Grid>
跨列排列:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="WPF中文网1" Panel.ZIndex="1" Margin="20" Grid.ColumnSpan="2"/>//跨2列
<Button Grid.Row="1" Grid.Column="0" Content="WPF中文网3" Panel.ZIndex="1" Margin="20" />
<Button Grid.Row="1" Grid.Column="1" Content="WPF中文网4" Panel.ZIndex="0" Margin="20" />
</Grid>
固定列宽:
我们只需要设置第一行ColumnDefinition的Width属性,让其宽度固定为120像素,那么第二列的宽度等于Grid的宽度减去120像素,其内部的Button宽度也随之自适应。这就是WPF布局自适应的好处。
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Content="WPF中文网1" Panel.ZIndex="1" Margin="20" />
<Button Grid.Row="0" Grid.Column="1" Content="WPF中文网2" Panel.ZIndex="0" Margin="20" />
<Button Grid.Row="1" Grid.Column="0" Content="WPF中文网3" Panel.ZIndex="1" Margin="20" />
<Button Grid.Row="1" Grid.Column="1" Content="WPF中文网4" Panel.ZIndex="0" Margin="20" />
</Grid>
调整行高和列宽
Grid控件的行高和列宽的设置十分丰富,了解它们的用法,有助于设计出更出色的布局。
名称 | 说明 |
绝对设置尺寸 | 使用设备无关单位准确地设置尺寸,就是给一个实际的数字,但通常将此值指定为整数(像素)。如:<ColumnDefinition Width="100"></ColumnDefinition> |
自动设置尺寸 | 值为Auto,实际作用就是取实际控件所需的最小值,每行和每列的尺寸刚好满足需要,这是最有用的尺寸设置方式。如:<ColumnDefinition Width="Auto"></ColumnDefinition> |
按比例设置设置尺寸 | 按比例将空间分割到一组行和列中。这是对所有行和列的标准设置。通常值为*或N*,实际作用就是取尽可能大的值,当某一列或行被定义为*则是尽可能大,当出现多列或行被定义为*则是代表几者之间按比例方设置尺寸。如:<ColumnDefinition Width="*"></ColumnDefinition> |
指定权重,即第2列的宽度是第1列的两倍
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="2*"></RowDefinition>
Grid显示网格线
<Grid ShowGridLines="True" Margin="5">
</Grid>
Border
是一个常用的控件,用于在界面上绘制一个矩形边框,通常用于装饰或分组其他控件。Border
控件可以包含一个子元素,并在该子元素周围绘制边框。
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" Grid.ColumnSpan="2" BorderBrush="Gray" BorderThickness="1"/>
<Border Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" BorderBrush="Gray" BorderThickness="0 0 0 1"/>
<Border Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" BorderBrush="Gray" BorderThickness="0 0 1 0"/>
<Button Grid.Row="0" Grid.Column="0" Content="WPF中文网1" Panel.ZIndex="1" Margin="20" />
<Button Grid.Row="0" Grid.Column="1" Content="WPF中文网2" Panel.ZIndex="0" Margin="20" />
<Button Grid.Row="1" Grid.Column="0" Content="WPF中文网3" Panel.ZIndex="1" Margin="20" />
<Button Grid.Row="1" Grid.Column="1" Content="WPF中文网4" Panel.ZIndex="0" Margin="20" />
</Grid>
我们在Grid内部增加了3个Border,第一个Border用来显示外边框,第二个Border显示中间的横线,第三个Border显示中间的竖线,这时所用的知识点几乎都是Grid的跨行和跨列属性,另外还有边框颜色刷子BorderBrush和边框厚度BorderThickness。
*其他布局(待学)
Control基类
通常用于展示程序的数据或获取用户输入的数据,我们可以将这一类型的控件称为内容控件或数据控Control类虽然可以实例化,但是在界面上是不会有任何显示的。只有那些继承了Control的子类(控件)才会在界面上显示,Control类提供了一个控件模板(ControlTemplate),而几乎所有的子类都对这个ControlTemplate进行了各自的实现。
Control的Template定义了控件的外观,Control类的Template属性是ControlTemplate类型的。
<Window x:Class="HelloWorld.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:HelloWorld"
mc:Ignorable="d"
Title="HelloWorld - www.wpfsoft.com" Height="350" Width="500">
<Control Margin="10">
<Control.Template>
<ControlTemplate TargetType="Control">
<Border Background="LightBlue">
<TextBlock Text="WPF中文网" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Control.Template>
</Control>
</Window>
事件
在这一小节里,您只要能明白Template的概念就行了。除了这个属性,Control类还提供了两个事件,它们分别是PreviewMouseDoubleClick和MouseDoubleClick。
事件名称 | 说明 |
PreviewMouseDoubleClick | 表示鼠标双击或多次单击时触发的事件 |
MouseDoubleClick | 表示鼠标双击或多次单击时触发的事件 |
以Preview开头的事件叫隧道事件或预览事件,而MouseDoubleClick没有以Preview开头,所以它叫冒泡事件。
*其他(待学)
数据绑定
数据绑定是在应用 UI 与其显示的数据之间建立连接的过程。 如果绑定具有正确的设置,并且数据提供适当的通知,则在数据更改其值时,绑定到该数据的元素会自动反映更改。 数据绑定还意味着,如果元素中数据的外部表示形式发生更改,则基础数据可以自动进行更新以反映更改。
这就是数据绑定!
图的左边是一个DependencyObject对象,表明它是一个依赖对象。那么,WPF控件是不是一个DependencyObject对象?是的,因为WPF所有的控件都继承了DependencyObject基类。
图的右边是一个普通的对象,所以这里标注成object类型。右边的对象表示绑定的数据源——也就是一个ViewModel。
DataContext数据上下文
DataContext是FrameworkElement基类的一个属性。其含义:获取或设置元素参与数据绑定时的数据上下文。通常情况下,我们把它设计成一个class——也就是所谓的ViewModel。
View负责数据的输入与输出;ViewModel负责业务逻辑;Model则表示程序中具体要处理的数据。所以,Model将作为属性存在于ViewModel中,而Model最终是要显示在Ul界面(View)上的,怎么办呢?将ViewModel赋值给View的DataContext(数据上下文)属性,View就可以引用ViewModel中的那些Model了。
总结:Model在ViewModel中,ViewModel在View的DataContext中,View引用Model。
WPF所有的控件(包括Window窗体)都有DataContext属性,因为它们都继承于FrameworkElement基类。
通常一个Window窗体中有很多控件,我们只需要给Window的DataContext赋值一个ViewModel,窗体中其它的控件的DataContext会共享Window的DataContext。
Model类
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
ViewModel类
public class MainViewModel
{
public Person Person { get; set; }
public MainViewModel()
{
Person = new Person
{
Name = "张三",
Age = 50,
Address = "居无定所",
};
}
}
DataContext属性赋值
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
我们给MainWindow主窗体的DataContext赋值了一个叫MainViewModel的视图模型。而MainViewModel中有一个叫Person的属性,它就代表了程序运行中要处理的数据实体。
现在MainWindow的DataContext(数据上下文)有了一个对象后,我们怎么将这个Person属性显示到前端呢,这需要使用Binding——数据绑定。
Binding(绑定)
Binding类架起了控件和ViewModel之间的桥梁,它就像一个媒婆,指示了哪个控件的哪个属性与哪个ViewModel类的哪个属性建立绑定关系。提供对绑定定义的高级访问,该绑定连接绑定目标对象(通常为 WPF 元素)的属性和任何数据源(例如数据库、XML 文件,或包含数据的任何对象)。
第一种数据源
也就是ViewModel中的Model。在写法上直接如下所示:
Text="{Binding Person.Name}"
这里实例化了一个Binding对象,后面紧跟的Person.Name表示一个Path路径,指的是当前的DataContext中那个ViewModel对象的Person.Name,注意看,Binding的有一个带参数的构造函数:public Binding(string path);实际上,就是将Person.Name路径传给了path形参。
第二种数据源
指明某个具体的数据源对象及对象的属性。这种绑定方式要用了Binding类的Source属性和Path属性。通常写法如下:
Text="{Binding Source={StaticResource RedBrush},Path=Color}"
在这里,Source属性表示数据源对象,它是一个静态资源对象,Path=Color表示要绑定这个静态资源对象的Color属性。我们已经提前在资源里定义好了这个资源对象。资源对象的实例名叫RedBrush,它确实也有一个叫Color的属性。
<Window.Resources>
<SolidColorBrush x:Key="RedBrush" Color="Red"/>
</Window.Resources>
这样绑定的效果如下图所示。
看起来Color=Red,实则是一个代表颜色的16进制字符串。
第三种数据源
利用ElementName属性指明另一个控件作为数据源,这里仍然要用到Path来指定另一个控件的某个属性路径。
<StackPanel x:Name="panel" VerticalAlignment="Center" Margin="100,0">
<TextBlock Margin="5">
<Run Text="Source示例:"/>
<Run Text="{Binding Source={StaticResource RedBrush},Path=Color}"/>
</TextBlock>
<TextBlock Margin="5">
<Run Text="ElementName示例:"/>
<Run Text="{Binding ElementName=panel,Path=Margin}"/>
</TextBlock>
</StackPanel>
通过Binding类的ElementName去指定当前XAML中的另一个StackPanel控件,并绑定其Margin属性,这样,TextBlock就显示了StackPanel控件的Margin属性值。
第四种数据源
利用RelativeSource属性绑定一个相对的数据源。这种绑定方式也经常使用,且实用价值很高,作为开发者,一定要掌握它的用法。
<StackPanel x:Name="panel" VerticalAlignment="Center" Margin="80,0">
<TextBlock Margin="5">
<Run Text="Source示例:"/>
<Run Text="{Binding Source={StaticResource RedBrush},Path=Color}"/>
</TextBlock>
<TextBlock Margin="5">
<Run Text="ElementName示例:"/>
<Run Text="{Binding ElementName=panel,Path=Margin}"/>
</TextBlock>
<TextBlock Margin="5">
<Run Text="RelativeSource示例:"/>
<Run Text="{Binding RelativeSource={RelativeSource Mode=Self},Path=Foreground}"/>
<Run Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=StackPanel},Path=Margin}"/>
</TextBlock>
</StackPanel>
RelativeSource类有3个重要的属性,它们分别是Mode、AncestorType和AncestorLevel。
Mode:表示寻找相对数据源的模式,一共有4种模式
模式 | 说明 |
PreviousData | 允许在当前显示的数据项列表中绑定上一个数据项(不是包含数据项的控件)。 |
TemplatedParent | 引用应用了模板的元素,其中此模板中存在数据绑定元素。 |
Self | 引用正在其上设置绑定的元素,并允许你将该元素的一个属性绑定到同一元素的其他属性上。 |
FindAncestor | 引用数据绑定元素的父链中的上级。 这可用于绑定到特定类型的上级或其子类。 |
在上面的例子中,我们演示了Self(自身控件)和FindAncestor(找上级控件)两种模式。
AncestorType:当Mode=FindAncestor时,这时就要指示要找的这个上级是什么类型,AncestorType用来表示上级的类型。
AncestorLevel:获取或设置要查找的上级级别。 使用 1 指示最靠近绑定目标元素的项。
Text="{Binding RelativeSource={RelativeSource Mode=Self},Path=Foreground}"
表示将自己的前景色显示到Text属性上。
Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=StackPanel},Path=Margin}"
表示从当前控件开始找上级,一个类型为StackPanel的控件,并把这个StackPanel控件的Margin显示到当前控件的Text属性上。
Binding的绑定模式
当一个实体的属性绑定到控件的属性之后,还需要指明这两者之间的绑定关系。这个就是Binding类的Mode属性,Mode属性是一个枚举类型。它有如下几个情况。
枚举值 | 说明 |
TwoWay | 双向绑定,即导致更改源属性或目标属性时自动更新另一方。 |
OneWay | 单向绑定,在更改绑定源(源)时更新绑定目标(目标)。 |
OneTime | 一次绑定,在应用程序启动或数据上下文更改时,更新绑定目标。 |
OneWayToSource | 在目标属性更改时,更新源属性。 |
Default | 默认绑定,文本框的默认绑定是双向的,而其他大多数属性默认为单向绑定。 |
这里面常用的是OneWay和TwoWay。如果是TwoWay(双向绑定),意味着前端控件的属性改变时,后端的Model也跟着改变,反之亦然。就拿前端改变去影响后端的Model值来说,这里会多出来一个概念——改变时机。
其实就是如果前端的值发生改变,后端的值在什么时候跟着改变。它由Binding类的UpdateSourceTrigger属性的值决定 。这个属性也是一个枚举类型。
枚举值 | 说明 |
Default | 采用控件各自的UpdateSourceTrigger默认值。 |
PropertyChanged | 每当绑定目标属性发生更改时,都会更新绑定源。 |
LostFocus | 每当绑定目标元素失去焦点时,都会更新绑定源。 |
Explicit | 仅在调用 System.Windows.Data.BindingExpression.UpdateSource 方法时更新绑定源。 |
TextBox.Text 属性的默认值为 LostFocus,我们经常会把TextBox的UpdateSourceTrigger改成PropertyChanged,即文本框在输入内容时,实时的更新后端的Model属性值。
前端属性值发生改变后,由UpdateSourceTrigger决定什么时候更新后端的Model属性值,但是,后端那些Model的属性值发生改变后,前端什么时候跟着改变?需不需要做特殊的处理?
答案是需要!如果Model中的那些属性没有实现属性通知,就算前后端成功的建立了绑定关系,后端Model属性值改变时,前端的显示是没有变化的,如果要实时跟着变化,则需要掌握WPF的INotifyPropertyChanged接口以及属性通知的相关知识点。
INotifyPropertyChanged接口
通知客户端属性值已更改。
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
这个接口只有一个PropertyChanged事件,该事件专门用来触发属性更改通知。通常情况下,我们会单独编写一个服务类(例如ObservableObject),以实现INotifyPropertyChanged接口的业务。这样做的好处是,将来的ViewModel、Model都可以继承这个ObservableObject,从而调用属性通知接口。
一、实现INotifyPropertyChanged接口
public class ObservableObject : INotifyPropertyChanged {public event PropertyChangedEventHandler PropertyChanged;public void RaisePropertyChanged([CallerMemberName] string propertyName = ""){PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));} }
使用了CallerMemberName特性后,就不必再传入属性名字符串。
接下来,我们来编写一个ViewModel和一个Model,让它们都继承这个属性通知基类。
Person实体
public class Person : ObservableObject
{
private string name;
public string Name
{
get { return name; }
set { name = value;RaisePropertyChanged(); }
}
private int age;
public int Age
{
get { return age; }
set { age = value; RaisePropertyChanged(); }
}
private string address;
public string Address
{
get { return address; }
set { address = value; RaisePropertyChanged(); }
}
}
MainViewModel实体
public class MainViewModel : ObservableObject
{
private Person person;
public Person Person
{
get { return person; }
set { person = value; RaisePropertyChanged(); }
}
public MainViewModel()
{
person = new Person
{
Name = "张三",
Age = 50,
Address = "居无定所",
};
}
}
<StackPanel Orientation="Horizontal">
<TextBlock Text="姓名:" Margin="5"/>
<TextBox Text="{Binding Person.Name,UpdateSourceTrigger=PropertyChanged}" Width="200" Height="25"/>
</StackPanel>
除了INotifyPropertyChanged接口可以实现属性通知,还有一个接口也可以实现属性通知,它就是INotifyCollectionChanged。它的作用是:当添加和删除项或清除整个列表时,向侦听器通知动态更改。也就是说,它是专门来解决集合的元素数量发生变化时的通知问题的。我们在下一节来演示它的用法。
ObservableCollection泛型集合
表示一个动态数据集合,它可在添加、删除项目或刷新整个列表时提供通知。
<ListView ItemsSource="{Binding Persons}" SelectedItem="{Binding Person}">
<ListView.View>
<GridView>
<GridViewColumn Header="姓名" DisplayMemberBinding="{Binding Name}" Width="60"/>
<GridViewColumn Header="年龄" DisplayMemberBinding="{Binding Age}" Width="auto"/>
<GridViewColumn Header="地址" DisplayMemberBinding="{Binding Address}" Width="auto"/>
</GridView>
</ListView.View>
</ListView>
public class MainViewModel : ObservableObject
{
private Person person;
public Person Person
{
get { return person; }
set { person = value; RaisePropertyChanged(); }
}
public ObservableCollection<Person> Persons { get; set; } = new ObservableCollection<Person>();
public MainViewModel()
{
}
}
ValidationRule验证规则
ValidationRule是一个抽象类,提供创建自定义规则的一个方式,旨在检查用户输入的有效性。所以,我们要验证前端输入的各项数据的有效性时,需要自己定义各自的验证规则。
在数据绑定时,Binding类有一个ValidationRules属性,这个属性专门用来存放开发者自定义的验证规则。
例如,我们假定用户名的长度必须在1-10个字符之间,且用户的年龄在1-100之前,那么就可以围绕这两个条件自定义两个不同的验证规则,它们定义如下:
用户名验证规则
public class NameValidationRule : ValidationRule {public override ValidationResult Validate(object value, CultureInfo cultureInfo){if (value != null && value.ToString().Length > 1 && value.ToString().Length <= 10){return new ValidationResult(true, "通过");}return new ValidationResult(false, "用户名长度1-10个字符");} }
年龄验证规则
public class AgeValidationRule : ValidationRule {public override ValidationResult Validate(object value, CultureInfo cultureInfo){double myValue = 0;if (double.TryParse(value.ToString(), out myValue)){if (myValue >= 1 && myValue <= 100){return new ValidationResult(true, null);}}return new ValidationResult(false, "请输入 1 至 100的年龄");} }
在XAML前端代码中,TextBox输入框分别绑定了用户名和年龄,它们在绑定时如何调用验证规则呢?
<StackPanel Orientation="Horizontal">
<TextBlock Text="姓名:" Margin="5"/>
<TextBox Width="145" Height="25">
<TextBox.Text>
<Binding Path="Person.Name" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:NameValidationRule ValidatesOnTargetUpdated="True" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
<Validation.ErrorTemplate>
<ControlTemplate>
<DockPanel>
<Grid DockPanel.Dock="Right" Width="auto" Height="auto" VerticalAlignment="Center" Margin="3 0 0 0">
<TextBlock Width="auto" Height="auto" Foreground="Red"
Text="{Binding ElementName=AdornedElementPlaceholder, Path=AdornedElement.(Validation.Errors).CurrentItem.ErrorContent}"/>
</Grid>
<Border BorderBrush="Red" BorderThickness="0" CornerRadius="2">
<AdornedElementPlaceholder x:Name="AdornedElementPlaceholder"/>
</Border>
</DockPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
</StackPanel>
ValidationRule会把验证结果保存在AdornedElementPlaceholder的AdornedElement属性中,所以,需要利用绑定的方法去绑定下面这个路径。