本书的风格还是很有意思的,作者是金庸迷,语言较为活泼,很适合入门,内容不会很枯燥。书中用的是VS2010。
因为使用VS2019也有一段时间了,这篇文档就算是查漏补缺。也是我学习WPF的入门第一本书。
第2章 WPF相关工具——十八般兵器
2.1.2 认识 Visual Studio
2. 第2招:认识VS的重要组成部分
新建应用程序后通常 VS 的界面如图所示:
左侧上方是工具箱,其中放置了一些常用的控件,如按钮及文本等,界面设计时可以将所需控件直接拖动到窗口中:左侧下方的Document Outline列表框用来显示用户界面的组成结构。
中间是代码编辑器,非常类似某些编写网页的环境。它提供了两种方式来设计界面:一是直接使用XAML语言(下半部分);二是在可视化环境中拖动控件(上半部分)。可以上下调换二者位置。也可以按照左右排列或只显示某一个页面,如下图:
最右侧是解决方案资源管理器(Solution Explorer)。
其中一个解决方案是一个树状结构,根节点是Solution,可以包含多个项目(Project)。
在Project下面有一个Reference节点,它是该项目需要引用的外部程序集,一个WPF程序至少需要引用5个程序集,即System、WindowBase、PresentationCore、PresentationFramework和System.Xaml。再下面是一个WPF应用程序的4个典型文件,即App.xaml、App.xaml.cs;MainWindow.xaml、MainWindow.xaml.cs。在WPF中往往是一个XAML文件和一个代码文件配合使用。这4个文件两两组合代表两个重要的类,一个是App,代表一个WPF的应用程序;另一个是MainWindow,代表应用程序的主窗口。
VS2019多了一个。
3. 第3招:调试WPF应用程序
调试程序首先要在执行的代码中设置断点。当程序执行到该行代码时就会中断。这样开发人员就可以查看代码中的变量,或者再继续一步一步地执行。
在调试时,VS环境左下方会出现Autos(自动窗口)、Locals(局部变量)和Watch(监视)窗口,其中Autos用于监视程序运行时最后访问的变量;Locals监视当前执行方法中的变量;Watch监视用户希望看到的变量,如可以将变量s拖放到Watch窗口中来查看其值,如下图:
跳过了通过记事本与命令行编写、编译程序章节
2.5 Reflector
如果希望探求源码,那么Reflector是一个非常不错的工具。它是利用.NET反射机制来分析程序集而实现的一款反编译及类浏览软件。PDF38页。
第3章 WPF体系结构——藏宝图
3.1 Windows体系结构
我们可以把整个Windows的体系架构比作一个恢弘的大厦,各种不同的技术,包括WPF都在某一层的某一个房间之内,如下图(不重要):
这座大厦的最上层是用户构建的应用程序,如Office程序、Maya软件或者是我们自己写的程序。其下是系统应用程序,如桌面窗口管理器(Desktop Window Manager)或者IE浏览器。其下是应用程序框架,是开发人员比较熟知的,如MFC/ATL,WinForms和WPF。其下是高——中——低层的抽象,如应用程序框架提供的控件、按钮、组合框、托管DirectX、OpenGL和非托管的DirectX。再其下是用户模式的驱动,即用户模式的底部。下面是内核模式,这一部分是操作系统的内核,如内存及设备管理。其下是硬件的驱动,最终到达Windows体系架构的底部。
把这个大厦分为3层,即用户应用程序、系统图形框架,以及操作系统核心和硬件驱动,如下图所示。WPF位于中间层。
第2层实际上是一个”复式结构”,如下图所示:
在系统图形框架中可以看到WPF的底层基于DirectX和User32,前者负责渲染,后者负责相应窗口消息。从中可以看出WPF是.NET Framework的一个组成部分。窗口中的按钮或者文本最终都要变化成DirectX可以接受的三角形、材质和其他Direct3D对象时,然后,DirectX会调用相应的渲染驱动模型(WDDM或者XPDM),使用图形显卡渲染这些数据。
3.2 WPF内部结构
揭开WPF内部结构的真相并非易事,需要层层剥离才能山高月小,水落石出。
3.2.1 切入点之一:托管和非托管的界限
在CLR(Common Language Runtime,公共语言运行时)之上的托管模块稳定性好,并且开发简便,可以迅速构建应用程序;非托管模块则更接近底层,会更为高效一些,二者分别位于WPF的上层和底层。WPF暴露给开发人员的都是托管的API接口,而在和DirectX交互时则需要用到非托管的代码,WPF的内部结构如下图所示:
图中加阴影的模块时WPF的主要组件,CLR之上是托管代码,之下是非托管代码。由此可见,WPF中只有组件Milcore是非托管的,是一个和DirectX交互的非托管组件,之所以使用非托管的方式实现,一方面是为了和DirectX结合更为紧密;另一方面则是不惜牺牲CLR的诸多优点来换取性能上的优势。CLR之上的PresentationCore组件提供了一组基本服务,如事件处理、输入及布局等。在其提供的基础功能之上,组件PresentationFramework实现了WPF中的各种外观,如按钮及图形效果的实现。
3.2.2 切入点之二:WPF如何实现绘制
要了解WPF如何实现绘制,先要清楚WPF绘制所用的数据结构。
1、WPF绘制所用的数据结构
使用XAML文件描述WPF应用程序界面时,其组织结构从根节点自顶向下看起来就像一棵树,称为”逻辑树”(Logic Tree),下图所示为页面中一个按钮的逻辑树结构。
逻辑树在描述用户界面的组成、依赖属性的继承(参见第6章)及路由事件(参见第7章)的传递时有相当大的作用,但是在界面可视化方面用其来描述界面元素的组成远远不够。实际上WPF将每一个小的可视化单元都看作是一个Visual,这个Visual类似早期程序世界中的窗口。每个小的Visual同样也是自顶向下组成了一棵树,称为”可视化树”(Visual Tree),其中的每个节点都保存绘制该节点所需要的命令。如下图所示,图中灰底的节点既是逻辑树节点,也是可视化树节点。不难发现,可视化树比逻辑树在描述用户界面时更为详尽。
WPF所认识的树实际上是一种称为”Composition Tree”的树,其中的每个节点称为”Composition Node”。
构成3D图形的基本图元是一个一个三角形,由三角形及其材质或者纹理构成复杂的几何图形,下图所示为三角形面片构成的地形表面。
下图为不同数量的三角形构成的兔子模型。
2、从绘制的角度分析WPF体系架构
一个WPF应用程序从两个线程开始,分别负责用户界面(UI)的管理和渲染。如前所述,托管代码为用户提供构建WPF所需要的各种功能,如布局和绑定等,通常用户界面管理线程使用该部分代码;非托管代码主要负责图形的叠加显示和渲染,那么使用该部分代码的是不能被用户接触的渲染线程,两个线程之间通过消息来传递数据。我们从绘制的角度将WPF内部结构图进一步细化,如下图:
说明如下:
(1)在最上层的是逻辑树,如在页面中添加一个按钮或者一个矩形,其实本质都是在逻辑树上添加一个节点。
(2)WPF的托管代码部分包含一个重要的子系统,即Visual System,它是托管代码与非托管代码沟通的渠道,其内部保存WPF程序需要显示的可视化树结构。从可视化树的角度来说,其实质是添加多个Visual到可视化树。
(3)前面两步均在用户界面线程中,添加Visual之后用户界面线程和渲染线程会通过消息传递可视化数据。
(4)非托管代码部分(Milcore)又称为”媒体整合层”(Media Integration Layer)。其中包含了合成子系统(Composition System)和渲染引擎(Rendering Enginee)。合成子系统接受Visual之后会将其转换为Composition节点,并添加到Composition Tree中。
(5)渲染引擎将Composition Tree转换为DirectX可以识别的三角形,这个过程称为”Tessellate”(三角剖分)。
(6)DirectX经过驱动(WDDM或者XPDM)通知显卡开始绘制像素到屏幕。
需要Composition Tree的原因如下:
(1)可视化树只存在于用户界面线程中,如果渲染线程需要绘制WPF的用户界面,则也需要一个数据结构。这个数据结构就是Composition Tree,它存在于渲染线程中;
(2)渲染引擎并不知道可视化树,它只知道Composition Tree,并将其转换为DirectX可以识别的三角形。
3.2.3 切入点之三:WPF类层次结构
WPF的类主要集中在WindowsBase.dll、PresentationCore.dll和PresentationFramework.dll程序集中。
WPF的命名空间均以System.Windows开头,如System.Windows.Controls是关于WPF控件的类;System.Windows.Media则是WPF中关于绘图、画刷、视频和音频相关的类。唯一例外的是System.Windows.Forms,它主要是Windows Form编程的相关类。但是命名空间下的类并不都属于Windows Form,也有一个是例外的,就是System.Windows.Forms.Integretation。这个命名空间中的类用来集成Windows Form和WPF程序。某一个命名空间也不是专属于某个程序集,它们之间是一种”多对多”的关系。如下图,System.Windows命名空间出现在WindowBase.dll,PresentationCore.dll和PresentationFramework.dll程序集中,WindowBase.dll则可以包含System.Windows及System.Windows.Data等若干命名空间。
WPF的类图虽然复杂,但主要的类结构如下图所示:
(1)Object(System.Object)
该类型是所有.NET对象的父类。
(2)DispatcherObject(System.Threading.DispatcherObject)
大多数的WPF类型都派生自DispatcherObject,如果不涉及多线程,也许并不用与其打交道。
(3)DependencyObject(System.Windows.DependencyObject)
这是所有能够支持依赖属性的基类,即如果希望一个类能够支持依赖属性,则必须从该类派生。
(4)Freezable(System.Windows.Freezable)
从该类型派生的类非常多,包括其中的画刷(Brush)、时间线(Timeline,用于动画)及几何(Geometry)等类型。Freezable有一个Changed事件,可以通知WPF其值发生改变。此外它的Freeze方法可以将自身“冻结”起来,变成一个只读状态的对象。通常在两个线程共享或者提高效率时,需要将对象“冻结”。
(5)Visual(System.Windows.Media.Visual)
Visual是可视化树的一个节点,包含绘制自身的所有命令。它是一个抽象类,主要作用是为WPF提供可视化支持,包括输出显示、透明度、坐标转换及区域剪切等。
(6)UIElement(System.Windows.UIElement)
UIElement派生自Visual,在Visual的基础上增强了3个方面的内容,即布局、输入和事件。从UIElement开始,可视化树慢慢淡去,逻辑树慢慢呈现。
(7)FrameworkElement(System.Windows.FrameworkElement)
FrameworkElement类派生自UIElement,在其基础上增强了数据绑定、样式及资源等WPF的核心功能。
(8)ContentElement(System.Windows.ContentElement)
ContentElement类似UIElement,不同的是该类型无法将自己可视化出来;必须借助于一个派生自Visual的类才能显示在屏幕上,它和FrameworkContentElement主要用在流文档模型中。
(9)FrameworkContentElement(System.Windows.FrameworkContentElement)
FrameworkContentElement类似FrameworkElement,也支持动画及数据绑定等特性。不同的是该类型无法将自己可视化出来,需要借助一个派生自Visual的类才能显示在屏幕上。
(10)Shape(System.Windows.Shapes.Shape)
这个类是基本形状的基类,用于创建基本的图形,如长方形、多边形、椭圆、线和路径等。由于它派生自FrameworkElement,因此也具备一些元素的特性。比如可以响应鼠标和键盘消息等,这一点WPF和以往的平台框架差别较大。
(11)Control(System.Windows.Controls.Control)
这个类是控件的基类,控件就是可以与用户交互的元素,如文本框、按钮、列表框等。WPF的控件可以支持控件模板,使得控件的外观可以随心所欲地变化。需要注意在以往的Windows编程中窗口中所有可视化内容都被称作“控件”。但在WPF中不再如此。可视化内容被称为“元素”(Element),对控件模板的支持是判别控件和元素的一个重要标准。
(12)ContentControl(System.Windows.Controls.ContentControl)
ContentControl派生自Control,是具有单一内容的控件,内容指可以在其放置文字、图片及视频等任何类型数据的控件。
(13)ItemsControl(System.Windows.Controls.ItemsControl)
这是所有能够支持多个条目显示的控件基类,如列表框和树形视图。与ContentControl不同的是,其内容是所有显示选项的集合,因此具有很好的灵活特性。实际上,WPF菜单、工具栏及状态栏都是特定的列表,并且实现它们的类都继承自ItemsControl类。
(14)System.Windows.Controls.Panel
面板(Panel)可以用作所有布局的容器,可以包含一个或多个子控件并且可以将其按照布局单位排列。这些容器是WPF布局系统的基础,并且合理使用容器是灵活布局界面内容的关键。