WPF自学入门(一) WPF-XAML基本知识

一、基本概念

1、XAML派生自XML,在WPF中用来开发用户界面,继承了XML的Tag、Attribute等语法。

2、在WPF中,XAML运行在CLR之上,但它不编译为IL,而是编译为BAML代码,在运行时会被解析成CLR类型(Types)。

3、XAML中大小写敏感。

二、基本语法

要学习WPF,我们需要采用webform的思维来考虑问题。在WPF之中,XAML是很重要的一个元素,它是用来构造WPF的UI界面的,正是因为WPF有了XAML这标记语言,它才能实现把界面和逻辑分离开来的一种设计模式,逻辑程序员写后台代码,而前台的界面设计由设计人员用XAML来负责,这样,就很好的进行了分工,这就是WPF吸引人的地方之一。

任何一个新建的WPF应用程序,项目默认文件结构,引用中导入了如下4个WPF开发必备的dll,这也是XAML中默认的命名空间。

WPF基础 - 图1

注意:在目录结构中我们没有看到program的主入口类,WPF中是通过APP文件是程序的入口,后面再详谈启动界面的不同方式。

1、标签语法=对象元素(Object Elements)

每个标签即是一个对象元素,将会被解析成一个WPF Framework(主要来自PresentationFramework.dll)中类的实例。下面的代码就是一个对象元素,运行时会被解析为一个Button对象实例。

WPF基础 - 图2

2、Attribute赋值语法

2.1、普通字符串赋值

下面代码给Button的Property和Event赋值。字符串将被通过包装好的TypeConverter来转换成对象。TypeConverter是用C#的Attribute技术来实现的。

WPF基础 - 图3

2.2、标记扩展赋值

用花括号这种方式赋值就叫标记扩展赋值。常用的地方是用到绑定和资源的时候。

WPF基础 - 图4

2.3、属性元素赋值

有的时候简单字符串赋值不了的,就用属性元素赋值。

WPF基础 - 图5

2.4、内容赋值

控件要有Content这个属性,也就是要继承自ContentControl这个类,才能这么赋值。夹在标签中间。

WPF基础 - 图6

2.5、集合赋值

下面例子是给StackPanel.Children这个属性赋值一个集合,被省略了。这个属性的类型是UIElementCollection。在这个例子,我么也看到WPF支持一些省略的写法,在实际开发中应用,可以使XAML代码更简洁。

WPF基础 - 图7

3、命名空间

在WPF中一般用在Root元素上(Application,Window,UserControl,Page,ResourceDictionary等)。如下:

xmlns为引入命名空间的attribute。

第一行xmlns是没有指定别名的,是默认命名空间,它指定的命名空间包含了微软提供的所有的XAML控件的程序集。

第二、三、四行xmlns别名是x,d,mc,也是微软提供的命名空间,这里面主要包含了一些关于解析XAML语言的程序集。

第五行xmlns是自己引入的,开发人员自己写的空间等,可以通过命名空间这种方式引用到XAML文档来使用。

WPF基础 - 图8

4、WPF中的两棵树

XAML文档是树状结构的。在WPF中有逻辑树(Logical Tree)和可视树(Visual Tree)的概念。在运行时会维护这两棵树。逻辑树是以我们看到的控件为节点的,逻辑树表示UI的核心结构。和XAML文件中定义的元素近乎相等,排除掉内部生成的那些用来帮助渲染的可视化元素。WPF用逻辑树来决定依赖属性,值继承,资源解决方案等。逻辑树用起来不像可视化树那么简单。对于新手来说,逻辑树可以包含类型对象,这一点与可视化树不同,可视化树只包含Dependency子类的实例。遍历逻辑树时,要记住逻辑树的叶子可以是任何类型。由于LogicTreeHelper只对DependencyObject有效,遍历逻辑树时需要非常小心,最好做类型检查。而可视化树能看到控件内部的元素,这些元素一般继承自Visual类。可视化树代表你界面上所有的渲染在屏幕上的元素。可视化树用于渲染,事件路由,定位资源(如果该元素没有逻辑父元素)等。遍历可视化树可以简单地使用VisualTreeHelper和简单的递归方法,WPF提供了两个辅助类(LogicalTreeHelper and VisualTreeHelper)来操作这两棵树。

5、附加属性、事件

WPF基础 - 图9

Panel.ZIndex就是附加属性,而附加事件在界面上用不了。在后面的具体实现项目时再来写附加事件。

WPF自学入门(二) WPF-XAML布局控件

本章节介绍WPF的布局容器,布局容器可以使控件按照分类显示。

在WPF中,布局是由布局容器来完成的,容器里面是可以放控件,容器里面也可以放容器。而在WPF中,布局容器有很多,这里只讲几种常见的。

比如:StackPanel、WrapPanel、DockPanel、Grid、Canvas五种布局容器。

一、StackPanel

在WPF中StackPanel的功能是,紧凑地把子控件按照一定的规律排列在一起,基本的排列方式有两种,一种是横排列,一种是竖排列

WPF基础 - 图10

这里顺便说一下Margin属性,Margin属性定义控件的外边缘,可以通过以下几种方式来设置:

  1. 1. Margin="10":各边缘均为10
  2. 2. Margin="10,20,30,40":设定左、上、右、下各边缘分别为10203040
  3. 3. 使用拆分式方式设定Margin="20,10",如上下为10,左右为20

二、WrapPanel

StackPanel是比较有局限性的,WPF的解决方案就是WrapPanel和DockPanel,他们是用来补充StackPanel功能的布局容器,下面通过观察来对比一下两个布局容器的区别。

WPF基础 - 图11

WrapPanel是可以根据容器的大小变化,来滚动控件的排布的。而StackPanel只是死死盯住控件,容器小了,就会遮挡内容。

三、DockPanel

DockPanel布局容器是以”上、下、左、右、中”为基本结构的布局方式,主要是控件的停靠方式。有类似于港口停船的方式。我们可以利用DockPanel.Dock这个附件属性来设置控件的停泊方式的。有四个方式,上下左右。

WPF基础 - 图12

四、Grid

在WPF中可以说最强大的布局容器就是Grid了,我们刚才演示的容器都可以放置在Grid中,因为Grid可以模仿除了WrapPanel之外的所有布局容器的功能。Grid其实就是把一个页面分为一格格,然后在这些格上放东西。

在布局控件之前,我们首先要做的是布局Grid容器,因为Grid容器是由小网格组成,那么我们在布局容器的时候就要设置好有多少行,有多少列。然后就形成了用这些行和列分隔开的网格了。

WPF基础 - 图13

我们用<ColumnDefinition>标签来定义列的,用<RowDefinition>来定义行的。上图我们定义的5×5的Grid布局容器。

注意:默认情况下是平均分,但是有很多情况不是平均分的,我们可以设置高度或者宽度的值,有三种方式。我们利用列来说明:

第一就是Width=”“,这种是按比例分的,2就是2倍的意思了。

第二就是Width=”auto”,自动分配,就是根据内容分配空间。

第三就是Width=”Value”一个确定的值。这个属性可以这样写:

定义Grid的行和列以后,网格就出来了就可以在里面放内容了。

我们通过Grid.Row和Grid.Column来把控件放进去。当然,我们也可以在容器里面的控件标签中加入各种属性来改变一些东西,如Margin,当我们不想看到分割线的话,也可以设置ShowGridLines=false来取消。我们可以在方格里面再镶嵌一个布局容器,在里面继续布局。

五、Canvas

Canvas布局容器就好像传统的布局一样,基于坐标的布局,利用Canvas.Left,Canvas.Top,Canvas.Right,Canvas.Bottom这四个附加属性来定位控件坐标。

WPF中使用的坐标是以左上角为原点,向右为X轴,向下为Y轴

WPF基础 - 图14

坐标点就是控件的左上角的位置(就是上面图中灰色方格的左上角)。至于它为什么会显示坐标,是因为Button有Content属性,第一篇—>二、基本语法—>2.4。

WPF自学入门(三) WPF路由事件之内置路由事件

.Net中已经有了事件机制,为什么在WPF中不直接使用.Net事件而要加入路由事件来取代事件呢?最直观的原因就是典型的WPF应用程序是用很多元素关联和组合起来的。之前提到过的两棵树——逻辑树(LogicalTree)和可视化树(VisualTree),那么它们分别是什么?

举例:WPF基础 - 图15

上面的代码就是逻辑树LogicalTree,一个Grid里面镶嵌了其他控件或布局组件,相当于一棵树中的叶子。而可视化树VisualTree是什么?它就是一个树中的树叶里面的结构,用放大镜看一下,其实叶子里面的结构也是一棵树结构。

举例:WPF基础 - 图16

既然WPF中使用这样的一个设计理念,路由事件就是特别为WPF为生的,它的功能就是可以把一个事件从触发点沿着树向上或者向下传播,需要对这个事件作出反应的地方就添加一个监听器,就会有相应的反应,当然,它的传播是可以用代码来停止

下面来了解一下WPF内置的路由事件和原理,然后我们来创建一个属于自己的路由事件。

1、WPF内置的路由事件

新建WPF项目,在页面上放置按钮。然后在Window,Grid,Button标签上使用MouseDown事件,如下图:

WPF基础 - 图17

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. using System.Windows.Data;
  9. using System.Windows.Documents;
  10. using System.Windows.Input;
  11. using System.Windows.Media;
  12. using System.Windows.Media.Imaging;
  13. using System.Windows.Navigation;
  14. using System.Windows.Shapes;
  15. namespace WPFMouseDown
  16. {
  17. /// <summary>
  18. /// MainWindow.xaml 的交互逻辑
  19. /// </summary>
  20. public partial class MainWindow : Window
  21. {
  22. public MainWindow()
  23. {
  24. InitializeComponent();
  25. }
  26. private void Window_MouseDown(object sender, MouseButtonEventArgs e)
  27. {
  28. MessageBox.Show("Window被点击");
  29. }
  30. private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
  31. {
  32. MessageBox.Show("Grid被点击");
  33. }
  34. private void Button_MouseDown(object sender, MouseButtonEventArgs e)
  35. {
  36. MessageBox.Show("Button被点击");
  37. }
  38. }
  39. }

点击运行后,鼠标右键点击鼠标,会依次弹出下列三个对话框。

Button_MouseDown事件被触发:

WPF基础 - 图18

Grid_MouseDown事件被触发:

WPF基础 - 图19

Window_MouseDown事件被触发:

WPF基础 - 图20

我点击的是按钮,为什么Grid和Window也会引发事件呢?

其实这是路由事件的机制,引发的事件由源元素逐级传到上层的元素,Button—>Grid—>Window,这样就导致这几个元素都接受到了事件。

如果想Grid和Window不处理这个事件,只需要在Button_MouseDown这个方法中加上e.Handled = true;这样就表示事件已经被处理,其他元素不需要再处理这个事件了。

  1. private void Button_MouseDown(object sender, MouseButtonEventArgs e)
  2. {
  3. MessageBox.Show("Button被点击");
  4. e.Handled = true;
  5. }
  1. 如果想要Grid参与事件处理只需要给它AddHandler即可。
  1. Grid.AddHandler(Grid.MouseDownEvent, new RoutedEventHandler(Grid_MouseDown), true);

路由事件实际上分为两类:气泡事件和预览事件(也叫做隧道事件)。上文中的例子就是气泡事件。

2、内置路由事件学习总结

气泡事件是WPF路由事件中最为常见的,它表示事件从源元素扩散传播到可视树,直到它被处理或到达根元素。这样我们就可以针对源元素的上方层级对象处理事件。(例如MouseDown)

预览事件采用另一种方式,从根元素开始,向下遍历元素树,直到被处理或到达事件的源元素。这样上游元素就可以在事件到达源元素之前先行截取并进行处理。根据命名惯例,**预览事件带有前缀Preview**(例如PreviewMouseDown)。


气泡事件和预览事件的区别:

气泡事件:在Button上点击,首先弹出”Button”,再弹出”Grid”,最后弹出”Window”。

预览事件:在Button上点击,首先弹出”Window”,再弹出”Grid”,最后弹出”Button”。

看到了这个顺序区别,那么我们加入e.Handler=true的时机也要不同。

WPF自学入门(四) WPF路由事件之自定义路由事件

创建自定义路由事件分为三个步骤:

  1. 1. 声明并注册路由事件
  2. 2. 利用CLR事件包装路由事件(封装路由事件)
  3. 3. 创建可以激发路由事件的方法。

现在创建一个能够报告当前时间和当前位置信息的路由事件。

一、声明自定义路由事件

创建继承RoutedEventArgs类的派生类ReportCurrentLocationEventArgs用来携带时间和位置消息,ClickTime属性是用来存储时间,CurrentLocation属性是用来存放位置。

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. namespace WPFDIYRouterTest
  8. {
  9. /// <summary>
  10. /// 报告当前位置
  11. /// </summary>
  12. public class ReportCurrentLocationEventArgs : RoutedEventArgs
  13. {
  14. public ReportCurrentLocationEventArgs(RoutedEvent routedEvent , object source) : base(routedEvent,source)
  15. {
  16. }
  17. /// <summary>
  18. /// 按钮点击时间
  19. /// </summary>
  20. public DateTime ClickTime { get; set; }
  21. /// <summary>
  22. /// 到达当前位置
  23. /// </summary>
  24. public string CurrentLocation { get; set; }
  25. }
  26. }

二、定义注册路由事件

我们用EventManger.RegisterRoutedEvent方法来注册的参数有4个。代码如下:

  1. public static readonly RoutedEvent ReportCurrentLocationEvent = EventManager.RegisterRoutedEvent
  2. ("ReportCurrentLocation", RoutingStrategy.Bubble, typeof(EventHandler<ReportCurrentLocationEventArgs>),
  3. typeof(ButtonReportCurrentLocation));

第一个参数是路由事件的名称Name。

第二个参数是路由事件的传递方式(RoutingStrategy是一个枚举类型,Bubble是使用冒泡策略:从事件元素到根),有三种方式:

  1. 1. Bubble是冒泡模式,这种模式是从触发点向上传递,直到最外层。
  2. 2. Direct就和传统的事件一样的,不会通过元素树。
  3. 3. Tunnel是预览模式(隧道模式),这与冒泡相反,向下传递。

第三个参数是路由事件处理器类型,传递的参数是自定义类。

第四个参数是拥有这个路由事件的类型。

三、封装路由事件

CLR事件的封装器,不同于依赖属性的GetValue和SetValue,这里是利用Add和Remove两个函数来给路由事件分配事件处理器。

  1. public event RoutedEventHandler ReportCurrentLocation
  2. {
  3. add { this.AddHandler(ReportCurrentLocationEvent, value); }
  4. remove { this.RemoveHandler(ReportCurrentLocationEvent, value); }
  5. }

四、创建可以激发路由事件的方法

重写OnClick方法触发设定路由事件,这是使用 RaiseEvent()方法来触发

  1. protected override void OnClick()
  2. {
  3. base.OnClick();
  4. ReportCurrentLocationEventArgs args = new ReportCurrentLocationEventArgs(ReportCurrentLocationEvent, this);
  5. args.ClickTime = DateTime.Now;
  6. this.RaiseEvent(args);
  7. }

完整的代码:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. namespace WPFDIYRouterTest
  9. {
  10. public class ButtonReportCurrentLocation : Button
  11. {
  12. public static readonly RoutedEvent ReportCurrentLocationEvent = EventManager.RegisterRoutedEvent
  13. ("ReportCurrentLocation", RoutingStrategy.Bubble, typeof(EventHandler<ReportCurrentLocationEventArgs>),
  14. typeof(ButtonReportCurrentLocation));
  15. public event RoutedEventHandler ReportCurrentLocation
  16. {
  17. add { this.AddHandler(ReportCurrentLocationEvent, value); }
  18. remove { this.RemoveHandler(ReportCurrentLocationEvent, value); }
  19. }
  20. protected override void OnClick()
  21. {
  22. base.OnClick();
  23. ReportCurrentLocationEventArgs args = new ReportCurrentLocationEventArgs(ReportCurrentLocationEvent, this);
  24. args.ClickTime = DateTime.Now;
  25. this.RaiseEvent(args);
  26. }
  27. }
  28. }
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. namespace WPFDIYRouterTest
  8. {
  9. /// <summary>
  10. /// 报告当前位置
  11. /// </summary>
  12. public class ReportCurrentLocationEventArgs : RoutedEventArgs
  13. {
  14. public ReportCurrentLocationEventArgs(RoutedEvent routedEvent , object source) : base(routedEvent,source)
  15. {
  16. }
  17. /// <summary>
  18. /// 按钮点击时间
  19. /// </summary>
  20. public DateTime ClickTime { get; set; }
  21. /// <summary>
  22. /// 到达当前位置
  23. /// </summary>
  24. public string CurrentLocation { get; set; }
  25. }
  26. }
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. using System.Windows.Data;
  9. using System.Windows.Documents;
  10. using System.Windows.Input;
  11. using System.Windows.Media;
  12. using System.Windows.Media.Imaging;
  13. using System.Windows.Navigation;
  14. using System.Windows.Shapes;
  15. namespace WPFDIYRouterTest
  16. {
  17. /// <summary>
  18. /// MainWindow.xaml 的交互逻辑
  19. /// </summary>
  20. public partial class MainWindow : Window
  21. {
  22. public MainWindow()
  23. {
  24. InitializeComponent();
  25. }
  26. private void ButtonReportCurrentLocationHandler(object sender,ReportCurrentLocationEventArgs e)
  27. {
  28. e.CurrentLocation = (sender as FrameworkElement).Name;
  29. string Message = e.ClickTime + "到达了" + e.CurrentLocation;
  30. this.ListBox.Items.Add(Message);
  31. }
  32. }
  33. }
  1. <Window x:Class="WPFDIYRouterTest.MainWindow"
  2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  6. xmlns:local="clr-namespace:WPFDIYRouterTest"
  7. mc:Ignorable="d"
  8. Title="MainWindow" Height="450" Width="800" x:Name="Window"
  9. local:ButtonReportCurrentLocation.ReportCurrentLocation="ButtonReportCurrentLocationHandler">
  10. <Grid ShowGridLines="False" x:Name="Grid"
  11. local:ButtonReportCurrentLocation.ReportCurrentLocation="ButtonReportCurrentLocationHandler">
  12. <StackPanel x:Name="StackPanel" local:ButtonReportCurrentLocation.ReportCurrentLocation="ButtonReportCurrentLocationHandler">
  13. <ListBox x:Name="ListBox" local:ButtonReportCurrentLocation.ReportCurrentLocation="ButtonReportCurrentLocationHandler"></ListBox>
  14. <local:ButtonReportCurrentLocation x:Name="ButtonReportCurrentLocation" Height="50" Content="报告当前位置和时间"
  15. local:ButtonReportCurrentLocation.ReportCurrentLocation="ButtonReportCurrentLocationHandler" Margin="226,0,105,0"
  16. HorizontalAlignment="Center" VerticalAlignment="Center"/>
  17. </StackPanel>
  18. </Grid>
  19. </Window>

这里在首次运行前一定会报错,在MainWindow.xaml文件中。在点击运行后,报错就会消失,并且正常运行。

WPF自学入门(五) WPF依赖属性

在.NET中有事件也有属性,WPF中加入了路由事件,也加入了依赖属性。为什么WPF中要加入依赖属性?

一、什么是依赖属性

WPF中的依赖属性有别于.NET中的属性,因为在WPF中有几个重要的特征都是需要依赖项属性的支持,例如数据绑定、动画、样式设置等。WPF对大多数属性都是依赖项属性,只不过它是用了普通的.NET属性过程进行了包装,通过这种包装,就可以像使用属性一样使用依赖项属性了,在后面会说一下怎么通过这种方式包装的,这就使用了旧技术来包装新技术的设计理念就不会干扰.NET。WPF中的依赖属性主要有以下三个优点:

  1. 1. 依赖属性加入了属性变化通知、限制、验证等功能。这样可以使我们更方便地实现应用,同时大大减少了代码量。
  2. 2. 节约内存:在WinForm中,每个UI控件地属性都赋予了初始值,这样每个相同的控件在内存中都会保存一份初始值。而WPF依赖属性很好地解决了这个问题,它内部实现使用哈希表存储机制,对多个相同控件的相同属性的值都只保存一份。
  3. 3. 支持多种提供对象:可以通过多种方式来设置依赖属性的值。可以配合表达式、样式和绑定来对依赖属性设置值。

如何创建依赖属性:

  1. 1. 依赖属性的所在类型继承自DependencyObject类。
  2. 2. 使用public static 声明一个DependencyProperty的变量,该变量就是真正的依赖属性。
  3. 3. 类型的静态构造函数中通过Register方法完成依赖属性的元数据注册。
  4. 4. 提供依赖属性的包装属性,通过这个属性来完成对依赖属性的读写操作。

创建代码如下:

  1. //依赖属性必须在依赖对象DependencyObject
  2. public class Person : DependencyObject
  3. {
  4. //CLR属性包装器,使得依赖属性NameProperty在外部能够像普通属性那样使用
  5. public string Name
  6. {
  7. get{return (string)GetValue(NameProperty);}
  8. set{SetValue(NameProperty,value);}
  9. }
  10. //依赖属性必须为static readonly
  11. //DependencyProperty.Register 参数说明
  12. //第一个参数是string类型的,是属性名。
  13. //第二个参数是这个依赖项属性的类型。
  14. //第三个参数是这个拥有这个依赖项属性的类型。
  15. //第四个参数是具有附加属性设置的FramWorkPropertyMetadata对象。
  16. public static readonly DependencyProperty NameProperty =
  17. DependencyProperty.Register("Name",typeof(string),typeof(Person),new PropertyMetadata("DefaultName"));
  18. }

从上面代码可以看出,依赖属性是通过调用DependencyObject的GetValue和SetValue来对依赖属性进行读写的。它使用哈希表来进行存储的,对应的Key就是属性的HashCode值,而值(Value)则是注册的DependencyProperty;而C#中的属性是类私有字段的封装,可以通过对该字段进行操作来对属性进行读写。属性是字段的包装,WPF中使用属性对依赖属性进行包装。

二、依赖属性的优先级

WPF属性系统提供一种强大的方法,使得依赖属性的值由多种因素决定,从而实现诸如实时属性验证,后期绑定以及向相关属性发出有关其他属性值发生更改的通知等功能。用来确定依赖属性的确切顺序和逻辑相当复杂。了解此顺序有助于避免不必要的属性设置,并且还有可能澄清混淆,使你正确了解为何某些影响或预测依赖属性值的尝试最终却没有得出所期望的值。依赖属性可以在多个位置”设置”,界面代码如下:

WPF基础 - 图21

本地属性集在设置时具有最高优先级,动画值和强制除外。如果在本地设置某个值,你可以期待该值优先得到应用,甚至期待其优先级高于任何样式或控件模板。在上面示例中,此处作用域中定义的样式不是最高优先级给予Background属性及其值。如果从该Button实例中删除本地值(Button里面的)红色,样式(Style)将获得优先级,而按钮将从该样式中获得Background值。在该样式中,触发器具有优先级,因此当鼠标位于按钮上时,按钮为蓝色,其他情况下则为绿色。

下图是依赖属性优先级列表图。

WPF基础 - 图22

三、依赖属性的继承

依赖属性的继承是WPF属性系统的一项功能。属性值继承使元素树中的子元素可以从父元素获取特定属性的值,并继承该值,就如同它是在最近的父元素中任意位置设置的一样。父元素可能也已通过属性值继承获得了其值,因此系统有可能一直递归到页面根。属性值继承不是默认属性系统行为;属性必须用特定的元数据设置来建立,以便使该属性对子元素启动属性值继承

WPF基础 - 图23

从上图中可以看到,StatusBar没有显式设置FontSize值,但它的字体大小没有继承Window.FontSize的值,而是保持了系统的默认值。导致这样的问题是因为不是所有元素都支持属性值继承的,如StatusBar、Tooptip和Menu控件。另外,StatusBar等控件截获了从父元素继承来的属性,并且该属性也不会影响StatusBar控件的子元素。例如,如果我们在StatusBar中添加一个Button。那么这个Button的FontSize属性也不会发生改变,其值为默认值。

四、自定义依赖属性

如果想要依赖属性继承,我们可以进行自定义依赖属性继承属性值。

自定义属性步骤:

1、创建派生类CustomStackPanel

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. namespace WpfApplicationDemo
  9. {
  10. public class CustomStackPanel : StackPanel
  11. {
  12. public static readonly DependencyProperty MinDateProperty;
  13. static CustomStackPanel()
  14. {
  15. MinDateProperty = DependencyProperty.Register("MinDate", typeof(DateTime), typeof(CustomStackPanel),
  16. new FrameworkPropertyMetadata(DateTime.MinValue,FrameworkPropertyMetadataOptions.Inherits));
  17. }
  18. public DateTime MinDate
  19. {
  20. get { return (DateTime)GetValue(MinDateProperty); }
  21. set { SetValue(MinDateProperty, value); }
  22. }
  23. }
  24. }

创建派生类CustomButton

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Windows;
  7. using System.Windows.Controls;
  8. namespace WpfApplicationDemo
  9. {
  10. public class CustomButton :Button
  11. {
  12. private static readonly DependencyProperty MinDateProperty;
  13. static CustomButton()
  14. {
  15. //AddOwner方法指定依赖属性的所有者,从而实现依赖属性的继承,即CustomStackPanel的MinDate属性被CustomButton控件继承。
  16. MinDateProperty = CustomStackPanel.MinDateProperty.AddOwner(typeof(CustomButton),
  17. new FrameworkPropertyMetadata(DateTime.MinValue,FrameworkPropertyMetadataOptions.Inherits));
  18. }
  19. public DateTime MinDate
  20. {
  21. get { return (DateTime)GetValue(MinDateProperty); }
  22. set { SetValue(MinDateProperty, value); }
  23. }
  24. }
  25. }

2、在Window控件中引入命名空间

  1. xmlns:sys="clr-namespace:System;assembly=mscorlib"

3、在页面添加Button

<local:CustomStackPanel x:Name="customStackPanel" MinDate="{x:Static sys:DateTime.Today}">
  <!--自定义依赖属性-->
  <ContentPresenter Content="{Binding Path=MinDate, Element=customStackPanel}"/>
  <local:CustomButton Content="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=MinDate}"
                      HorizontalAlignment="Left" Width="150" Height="25"/>
</local:CustomStackPanel>

显示效果:一直报错,不知道怎么回事

五、依赖属性验证和强制功能

在写代码时都会考虑可能发生的错误。在定义属性时,也需要考虑错误设置属性的可能性。对于传统.NET属性,可以在属性的设置器中进行属性值的验证,不满足条件的值可以抛出异常。但对于依赖属性来说,这种方法不合适,因为依赖属性通过SetValue方法来直接设置其值的。然而,WPF有其代替的方式,WOF中提供了两种方法来用于验证依赖属性的值。

1、ValidateValueCallback:该回调函数可以接受或拒绝新值。该值可作为DependencyProperty.Register方法的一个参数。

2、CoerceValueCallback:该回调函数可将新值强制修改为可被接受的值。例如某个依赖属性工作年龄的值范围是25到55,在该回调函数中,可以对设置的值进行强制修改,对于不满足条件的值,强制修改为满足条件的值。如当设置为负值时,可强制修改为0。该回调函数PropertyMetadata构造函数参数进行传递。

WPF自学入门(六) WPF带标题的内容控件简单介绍

在入门(二)中分别介绍StackPanel、WrapPanel、DockPanel、Canvas五种布局容器的使用,可以让我们大致了解容器可以使用在什么地方。本章节就简单了解一下三个带标题的内容控件,分别是GroupBox、TabControl和Expander。

一、GroupBox控件

这个控件可以叫做分组控件,可以把已经用布局控件包装好的一系列控件放到里面分为一个组。例如:我们可以放一个RadioButton进去,那么就不用设置GroupName也能对单选按钮进行分组了。这个GroupBox是带有圆角的和一个标签和内容的控件,大致是这样的:

WPF基础 - 图24

这就是创建一个GroupBox的过程与使用。当然,Header可以使用更复杂的逻辑,这就要发挥了。

控件使用注意事项:笔者在刚使用WPF时,在使用GroupBox做登录框的布局,发现放一个标签后无法再次放标签。后来在GroupBox控件上放布局控件StackPanel后,再放多个标签就没问题了。所以在放多个控件时,需要先放布局控件再放需要放置的常规控件

二、TabControl控件

TabItem是代表TabControl中的一页,在一个TabControl中需要多少页就要靠自己添加。默认的页签是字符串,例如:页签一。

看一下TabControl长什么样:

WPF基础 - 图25

页签可以是字符串,也可以是图片。接下来,我们可以将页签一的文字更改为图片。看看实际效果:

WPF基础 - 图26

只添加图片无法满足实际设计需要,WPF中的TabControl也可以弄成图文形式的组合。看看实际效果:

WPF基础 - 图27

这是默认模式的,选择栏是在顶部,也可以设置为侧边显示,可以通过设置TabStripPlacement属性有四种模式:Left、Right、Bottom、Top(默认)。

WPF基础 - 图28

三、Expander控件

Expander控件时可扩展的控件,它内容默认开始是隐藏好的,很像帮助界面那样,不会在界面给出全部的信息,全部隐藏好,如果你想知道哪个方面的东西,就扩展哪一个Expander,系统默认的样式个人觉得不是很好的,我们可以通过自定义模板来改进样式,首先,一起来看一下Expander控件的使用方法。

WPF基础 - 图29

这里定义了4个Expander,展开内容使用了4种不同颜色标注。分别是不同方向的展开方式,注意展开方向是用ExpandDirection控制着,然后内容各自镶嵌着自己的东西。

WPF基础 - 图30

WPF使编写程序的核心回到了数据驱动程序的模式上面。

WPF自学入门(七) WPF初识Binding

在传统的Windows软件中,大部分都是UI驱动程序的模式,也可以说事件驱动程序。WPF作为Winform的升级,它把UI驱动程序彻底改变了,核心回到了数据驱动程序的模式上面,这样,程序就回到了算法和数据。数据,才是真正需要重点处理的。

Binding最为重要的一个特点是通讯,连接着前台与后台。首先看一下Binding最简单的使用方法:

一、元素之间的绑定

WPF基础 - 图31

这里有3个控件,Slider、TextBox、Label,其中TextBox和Label都作为目标,Slider都作为数据源,把Slider中的值交由两个控件体现,移动滑块,TextBox会自动显示Value的值,也就是FontSize的值。因为两个绑定都设置了双向绑定,所以可以在文本框中输入值,然后丢失焦点,也能反馈回去。

看一下XAML中的绑定语句,这里用的ElementName就是制定要绑定的对象的名字,Path就是要绑定的依赖项属性,Mode就是绑定方式,这里需要说明的是Mode有五种方式:

    1. OneWay 单向绑定
    2. TwoWay 双向绑定
    3. OneTime,最初根据源属性值设置目标属性,以后就忽略所有改变,就是说,只进行初始化。
    4. OneWayToSource,这和OneWay相反。
    5. Default,这是默认形式,它根据目标属性自动设置。

如果把TextBox中的值修改成其他的,滑条位置没有改变,字体大小也没有改变,这是怎么回事呢?当TextBox失去焦点的时候,就会发生相应的改变了。这是因为这个绑定中的默认更新机制,更新机制Binding.UpdateSourceTrigger,这个属性有4个枚举值:

    1. PropertyChange,当值改变的时候,就更新。
    2. LostFocus,当失去焦点的时候更新。
    3. Explicit,当调用BindingExpression.UpdateSource()方法的时候更新,其他情况不会更新。
    4. Default,默认形式。

注意:以上四种更新机制的设定,只会影响源数据,而不会影响目标数据。

二、元素自身的绑定

除了可以绑定别的元素,也可以绑定自身的其他属性,例如Slider自身的Opacity属性和自身的Value属性绑定,当滑块向左移动的时候,会逐渐隐藏起来。

WPF基础 - 图32

三、后台数据与元素之间的绑定

前面说了元素之间的绑定和元素自身的绑定,最后重点来了,后台数据和前台元素的绑定,这种绑定方式很好地体现了数据驱动程序的运行模式。

首先新建Person类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace WpfHouTaiBindingQianTai
{
    public class Person
    {
        /// <summary>
        /// 姓名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 年龄
        /// </summary>
        public int Age { get; set; }
    }
}

页面后台代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfHouTaiBindingQianTai
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Person person = new Person { Name = "SoftEasy" };
            Binding binding = new Binding() { Path = new PropertyPath("Name"), Source = person };
            this.TxtName.SetBinding(TextBox.TextProperty, binding);
        }
    }
}
<Window x:Class="WpfHouTaiBindingQianTai.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:WpfHouTaiBindingQianTai"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <TextBox x:Name="TxtName" HorizontalAlignment="Left" Margin="24,69,0,0"
                 TextWrapping="Wrap" VerticalAlignment="Top" Height="60" Width="600"/>
    </Grid>
</Window>

显示效果:

WPF基础 - 图33

数据绑定的方式已经写完了。Binding是一条高速公路,那么为了提高数据传递的合法性和有效性,我们要在这条高速公路中建立起一系列的关卡,有的用来转换数据,有的用来校验数据,下面说一下Binding对数据的校验和转换。

(一)Binding的数据校验

Binding的数据校验工作是派生自ValidationRule类,并且对Validate方法进行重写的自定义类!

看一下实例:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;

namespace WpfHouTaiBindingQianTai
{
    public class DataValidationRule : ValidationRule
    {
        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            double d = 0;
            if (double.TryParse(value.ToString(),out d))
            {
                if(d >= 0 && d<= 100)
                {
                    return new ValidationResult(true, null);
                }
            }
            return new ValidationResult(false, "验证失败!");
        }
    }
}

先设计一个校验类,它继承ValidationRule类并且重写Validate方法。使用这个类的时候是创建Binding的时候设置校验的。

代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfHouTaiBindingQianTai
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            //Person person = new Person { Name = "SoftEasy" };
            //Binding binding = new Binding() { Path = new PropertyPath("Name"), Source = person };
            //this.TxtName.SetBinding(TextBox.TextProperty, binding);
            Binding binding = new Binding("Value") { Source = this.slider };
            binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
            DataValidationRule dataValidationRule = new DataValidationRule();
            dataValidationRule.ValidatesOnTargetUpdated = true;
            binding.ValidationRules.Add(dataValidationRule);
            binding.NotifyOnValidationError = true;
            this.TxtFontSize.SetBinding(TextBox.TextProperty, binding);
            this.TxtFontSize.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(this.ValidationError));
        }
        /// <summary>
        /// 验证错误
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void ValidationError(object sender,RoutedEventArgs e)
        {
            if(Validation.GetErrors(this.TxtFontSize).Count > 0)
            {
                this.TxtFontSize.ToolTip = Validation.GetErrors(this.TxtFontSize)[0].ErrorContent.ToString();
            }
        }
    }
}
<Window x:Class="WpfHouTaiBindingQianTai.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:WpfHouTaiBindingQianTai"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
  <Grid>
    <Label x:Name="LblFontSize" Content="SoftEasy" FontSize="{Binding ElementName=slider ,Path=Value,Mode=TwoWay}" 
           HorizontalAlignment="Left" Margin="24,18,0,0" VerticalAlignment="Top"/>
    <Slider x:Name="slider" HorizontalAlignment="Left" Margin="24,117,0,0"
            VerticalAlignment="Top" Width="330" Maximum="200" Minimum="20"/> //这里的最大值改成了200,为了测试让它报错
    <TextBox x:Name="TxtFontSize" HorizontalAlignment="Left" Height="23" Margin="24,69,0,0"
             TextWrapping="Wrap" Text="{Binding ElementName=slider ,Path=Value ,Mode=TwoWay}"
             VerticalAlignment="Top" Width="120"/>
  </Grid>
</Window>

显示效果:

WPF基础 - 图34

因为设置了传过去的值不能是超过0~100,所以当超过了就显示红色边框。在Binding中,默认是会认为数据源是肯定正确的,所以如果将TextBox作为数据源,而Slider作为目标,数据源输入错误是没有显示的,那么怎么解决这个问题呢?设置 dataValidationRule.ValidatesOnTargetUpdated = true;

(二)Binding的数据转换

Binding还有另外一种机制称为数据转换,当Source端指定的Path属性值和Target端指定的目标属性不一致的时候,我们可以添加数据转换器(Convert)。上面我们提到的问题实际上就是double和string类型相互转换的问题,因为处理起来比较简单,所以WPF类库就自己帮我们做了,但有些数据类型转换就不是WPF能帮我们做的了,当遇到这些情况,我们只能自己动手来写Converter,方法是创建一个类并让这个类实现 IValueConverter接口。

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace WpfHouTaiBindingQianTai
{
    /// <summary>
    /// 自定义事件转换
    /// </summary>
    public class TimeConver : IValueConverter
    {
        //当值从绑定源传播给绑定目标时,调用方法Convert
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if(value == null)
            {
                return DependencyProperty.UnsetValue;
            }
            DateTime date = (DateTime)value;
            return date.ToString("yyyy-MM-dd");
        }

        //当值从绑定目标传播给绑定源时,调用此方法ConvertBack
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string str = value as string;
            DateTime txtDate;
            if(DateTime.TryParse(str,out txtDate))
            {
                return txtDate;
            }
            return DependencyProperty.UnsetValue;
        }
    }
}

这个就是日期转换类,它有两个方法:

    1. 当值从绑定源传播给绑定目标时,调用方法Convert
    2. 当值从绑定目标传播给绑定源时,调用此方法ConvertBack,方法ConvertBack的实现必须是方法Convert的反向实现。

这两个方法分别在里面写入怎么转换,转换成什么类型就是返回类型。

下面就是使用:

把这个绑定的Convert属性设置成我们设计的转换类的实例就可以了,效果: