说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。
WPF与Win32系统的组成关系
WPF功能模块构成
WPF类库
WPF重要类的层次及说明,下图展示了WPF的核心类:
-
Object类 – .NET中的基类,不用多说
-
DispatcherObject类 – (System.Treading.DispatcherObject)只能在创建的线程上访问的对象的基类。大多数WPF类派生自它。其提供了处理并发和多线程的底层架构,主要处理如鼠标移动(中断),布局及与系统内核相关的消息指令。
-
使用派生自DispatcherObject类,即可构建一个单线程执行的对象(包含一个到Dispatcher的指针)。
-
DependcyObject类 – 支持依赖属性的任何一个对象的基类。其中定义了依赖属性的核心方法GetValue与SetValue。
-
Freezable类 – 可以被"冻结"的对象的基类,这些类均是只读的,这是出于性能的原因。冻结后的只读对象就可以在多个线程安全共享。一旦冻结就不能解冻,但可以复制冻结对象创建一个非冻结的副本。
-
Visual类 – (System.Windows.Media.Visual)可以控制自身呈现的对象的基类。从名称就可以看出,此类与WPF的展现有关。其负责由与DirectX通信的类库托管类库milcore中取得分层的树状展示的数据结构,这个树中每个节点表示一个指令(可以创建此类实例,并使用内部消息与milcore通信)。
-
UIElement – (System.Windows.UIElement)支持路由事件,命令绑定,布局和焦点的可视对象的基类。对于布局UIElement通过调整和排列这两步来实现"不同的"流布局,绝对布局及表格布局。(用户可以指定一个容器的大小(按其中控件所需),调整功能会自动根据容器大小决定组件的大小。)
-
(排列改变控件在屏幕上放置的方式(如横排,竖排等),这在调整操作完毕完后执行。)
-
ContentElement – 没有控制自身呈现逻辑,只有需要呈现的内容的对象的基类。ContentElement存在于派生自Visual的类中,以将自身呈现在屏幕上。
-
FrameworkElement类 – (System.Windows.FrameworkElement)主要提供与应用层相关的主要功能,FrameworkElement派生自 UIElement,其继承了父类良好的布局方式,同时更进一步允许程序员修改UIElement的自动布局以指定控件的对齐方式和布局属性。在这个级别的类中开始支持样式,数据绑定,资源及一些通用机制的Windows控件的基类。如ToolTips与ContextMenus。
-
FrameworkContentElement – 类似于FrameworkElement用于表示内容。
-
Control类 – 一些常见的如Button等控件的基类。Control在FrameworkElement基础上添加了Background等常见的属性。其也支持模版以替换其默认的可视树。
Application对象
类似于WinForm中的Application对象,在presentationframework.dll中,System.Windows命名空间下,存在着WPF应用程序的核心Application类。其负责应用程序与操作系统之间的沟通。具体说:
-
Application负责处理信息调度,不再需要实现消息循环
-
Application提供一个应用程序级别的全局对象供页面间数据的共享
-
Application中有一些应用程序级别的事件(如:Application_Start),程序员可以自定义这些事件的处理,最大化的配置程序。
后两条成立是因为Application本身对应用程序就是全局的,其在应用程序生命周期开始时即创建,在任何页面,生命周期的任何时刻访问到的Application均是相同的。
我们也可继承Application创建自己的Application对象,重写现有方法,事件的实现,并扩展自定义的方法。
在开发工具创建的WPF模版中,Application对象不再使用C#代码来创建,而是通过XAML声明的方式来创建(由XAML编译器完成Applicatioin对象的创建)。
定义Application的XAML如下(Silverlight中定义Application的XAML类似,不再列举):
<Application x:Class="WpfApplication1.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Window1.xaml"> <Application.Resources> </Application.Resources> </Application>
与普通页面的XAML的定义对比可以看出,这个XAML的根节点名为Application,而非一般页面XAML的Window/Page(WPF)或UserControl(Silverlight)。
上文XAML中的x:Class关键字定义了一个类,具体这个关键字的作用前文有介绍,这里定义了一个WpfApplication1命名空间下的App类,这个类派生自Application,这点由后置代码文件可以看出。
public partial class App : Application { }
当然,同普通的XAML文件,在不需后置代码时,无需用x:Class关键字显示指定类名。当需要编写后置代码处理时或者需要使用StartupUri指定启动页面时,才需要编写后置代码文件。
另外需要注意的是:StartupUri属性(Attribute)这个关键字定义了程序初始化导入的XAML文件(类似于WinForm中的启动窗体)。App.xaml这个文件中主要用来定义一些整个应用程序会用的样式等资源,这些会被应用程序中所有的页面继承。App.xaml.cs这个文件主要定义Application事件(如程序的开始与退出)的处理函数,全局导航事件(Application对象支持所有发生在任意应用程序窗体导航所触发的事件)及定制的属性与方法。
当编译时,App.xaml用来建立Application对象,其StartupUri属性指定的页面首先启动。
Application的属性
使用Application.Current静态属性可以访问当前的Application对象,主要服务于需要在同一程序中不同的Window中获取当前应用(Application)实例的需要(如通过此实例访问Property属性,详见下文)。由于Current定义于Application中,类似于Property,可以通过子类访问这个属性,即:App.Current将得到与Application.Current相同的结果(均为App类的实例)。
Application中管理窗体的属性
-
Windows属性
应用程序中打开窗体的集合,遍历这个集合可以选择一个窗体并进行操作。虽然可以通过Windows[0]等这种方式访问窗体,但由于窗体的下标是不固定的(如关闭一个窗体会影响下标比其大的窗体的下标)不能使用这种方式访问窗体,否则可能会发生错误的引用。
-
MainWindow属性
包含一个应用程序主窗体的引用,默认值是运行程序所打开的第一个窗体。这个属性是一个可读可写的属性,所以也可以通过设置此值来指定一个主窗体。
-
Property属性
这个集合是一个用来存储数据的字典,这些数据可以在Window与其它对象之间共享。这样就可以直接把数据存储于这个属性中,而不用再在Application的子类中自定义公共属性来存储需共享的数据。这个属性写入与读取的方式如:
Application.Current.Properties["Key"] = "Value"; string Value = Application.Current.Properties["Key"] as string;
由于App与Application的继承关系,代码也可以写为:
App.Current.Properties["Key"] = "Value"; string Value = App.Current.Properties["Key"] as string;
注意,key与value的类型均为object,所以可以在其中存储任意类型的对象。
Application的事件
Application中定义了多种事件,这些事件在应用程序发生相应的动作是被触发。我们将以下面介绍的第一个事件来演示怎样给事件指定一个处理函数,并实现这个处理函数的方法。通常我们是在自定义的Application类的子类(一般为App)中重写父类这些同名的OnEventName方法。Application提供的可以通过重写来自定义处理方式的事件也是除了作为程序入口点与消息分发器外另一重要的功能。
-
Activated事件
Activated事件在用户点击应用程序在任务栏上的图标来激活时触发。订阅这个事件的方法是在XAML文件中添加Activated这个Attribute,如下:
<Application x:Class="WpfApplication1.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Window1.xaml" Activated="Application_Activated">
事件处理函数实现于后置代码文件:
public partial class App : Application { private void Application_Activated(object sender, EventArgs e) { //处理Activated事件 } }
-
Deactivated事件
当应用程序不再是"当前程序"时触发,往往是由于用户按Tab+Alt键切换到其他程序,或点击了任务栏上其他程序的图标。
-
SessionEnding事件
此事件处理应用程序被操作系统关闭时的情形。由传给这个事件处理函数的ReasonSessionEnding可以得知程序被关闭的具体原因,系统注销,关机等。如我们要在计算机注销或关闭但程序还未退出时保存文件,就可以在这个事件的处理函数中编写代码处理。
-
Startup事件
-
Exit事件
这个事件与应用程序的关闭有关。在应用程序调用Shutdown方法后,触发Application的Exit事件,Run方法返回,程序退出。我们可以重写父类的OnExit方法来处理这个事件进行清理工作。
也可以使用Shutdown属性控制怎样关闭应用程序,下列这些属性值会影响对Shutdown的触发。
-
OnLastWindowClose:当最后一个窗体关闭时应用程序退出。
-
OnMainWindowClose:当主窗体(即MainWindow属性指定的窗体)关闭时应用程序退出。
-
OnExplicitShutdown:仅当直接调用Shutdown函数时应用程序退出。如需要把程序放在通知区域就可以使用这个选项。
另一种应用程序关闭的方式是操作系统关闭并使Application触发SessionEnding。这个前文有介绍。
-
Application级别的Navigation事件
在Application对象上下文,Navigation事件被Application触发而不是被窗体触发。这些Application事件包括:
-
Navigating:在导航初始化/用户取消事件/组织导航时触发。
-
NavigationProgress:该事件跟踪导航的过程,周期性地被触发提供关于导航操作过程的信息。
-
Navigated:目标已找到,目标文件下载将被启动,部分用户页面被解析,至少根被加载到窗体。
-
LoadCompleted:目标页面被加载并且全部解析。
最后说一下,上面提到的这些事件中除SessionEnding与Navigation外在Window中都有同名事件,但Application中的事件主要处理应用程序级的问题。
入口函数
类似于WinForm或最基本的ConsoleApplication,为了使应用程序可以开始运行,需要提供一个入口函数。符合惯例,这个入口函数称为Main。但是这个函数只可以在调试时obj下的Debug文件夹中的App.g.cs这个文件中看到其踪迹。此函数大致形如:
[System.STAThreadAttribute()] public static void Main() { App app = new App(); app.InitializeComponent(); app.Run(); }
提示:由Main()方法可以看出,WPF主函数被标记为STAThread,即WPF 主线程将以STA方式运行,这样WPF中对象(派生自DispatcherObject类的对象)在STA线程上创建也就只能在同一线程中被调用。如果由 其它线程调用这些对象,WPF就会抛出一个异常。这样简化了许多问题,避免了未预期的结果的发生,同时使互操作变为可能。但同时WPF也提供了其它线程与 UI线程通信的机制。
在调用Run()方法后,Application将进入消息循环来处理请求。另外细看上面的Main()方法你会发现Run()方法没有参数。这是由于在App.xaml中定义了StartupUri="MainWindow.xaml",这样就告诉Application来启动MainWindow这个类的实例。如果使用代码实现等价于xaml的功能则应该写成这样(虽然默认这个Main()函数是自动生成而无法手动修改):
[System.STAThreadAttribute()] public static void Main() { App app = new App(); app.StartupUri = new Uri("MainWindow.xaml", UriKind.Relative); app.Run(); }
很明显的WPF中运行的应用程序也是Application类的子类的实例,默认模版生成的这个子类名为App。这个App类的构造函数中调用了InitializeComponent来进行应用程序的一些初始化工作。
提示:如果接收传给WPF程序的命令行参数。
一种可选的方法是通过特殊方法自定义一个可以接收参数的Main()方法,但此方法不推荐。更加简单可行的方法是调用System.Environment.GetCommandLineArgs方法来得到Main()方法得到的字符串数组的参数。
提示:创建单实例运行的WPF应用程序
传统.NET中创建单实例程序(主要针对WinForm)的方法仍然适用于WPF程序,即使用一个操作系统级的互斥量(mutex),下面是C#代码示例:
bool mutexIsNew; using (System.Threading.Mutex m = new Mutex(true, "uniqueName", out mutexIsNew)) { if(mutexIsNew) //The first instance. Run It! else //There is already an instance running. This should Exit! }注意为避免冲突,uniqueName建议使用GUID。
WPF中的窗体
WPF中提供三种可用的窗体,分别是Window类,NavigationWindow类和Page类。
-
Window
Window对象支持基本窗体功能,在不使用导航的情况下,这个比NavigationWindow窗体使用更少的资源,因为其顶部没有导航条,占用了更少的资源。
WPF中的Window是对Win32窗口的直接抽象,所以操作系统对待WPF的Window的方式与其它应用程序是一致的(操作系统无法区分这些窗体间不同 )。Window类提供了一些方法与属性,这些方法与属性内部调用Win32 API,实现对窗体的控制。
常用方法有:
-
Show:调用此方法显示一个窗体。
-
Hide:调用此方法隐藏一个窗体。
-
Close:调用来安全关闭一个窗体。
常用属性:
-
控制外观类:Icon,Title(用于修改标题),WindowStyle。
-
控制位置类:Left,Top,WindowStartupLocation(可选值有CenterScreen或CenterOwner)
-
控制行为类:Topmost(为true时,窗口永远位于最前面),ShowInTaskbar(为false时,程序不会出现在任务栏中)。
-
Owner:通过设置一个子窗体的该属性为一个父窗体的引用来建立子窗体关系。注意设置此属性时,父窗体需要已通过调用Show()方法显示在前台。
-
OwnedWindows:通过此属性可枚举一个窗体的子窗体。
常用事件:
-
Activated:当一个Window变为活动时(可能是用户操作亦或代码操作)被触发,也可是手工触发此事件使一个Window变为活动状态。
-
Deactivated:当一个Window变为不活动(如最小化到任务栏),即不是当前窗体时被触发。
-
Closed:窗体关闭时
-
Closing:窗体关闭过程中,发生在Closed之前
-
ContentRendered:窗体结束创建的上下文时
-
LocationChanged:窗体位置变化时
-
StateChanged:窗体状态在最小化,最大化与正常之间变化时
WPF应用程序中的一般窗体都是这个Window类的子类的对象,可以通过声明多个Window子类的对象来创建多个窗体。同WinForm,可以将一部分窗体设置为另一个窗体的子窗口,并且模式窗体与非模式窗体等的行为与WinForm也大致相同。设置与管理子窗体可以见上文对于Window类属性的介绍。
WPF应用程序模版建立的项目中默认的主窗体称为MainWindow,该类的构造函数中(不限于此,所有添加到项目的Window的构造函数中)调用InitializeComponent方法(切记,如果忘记调用,xaml文件就无法被处理)来初始化定义在Window中的一些元素,同时订阅Closing,Closed及Initialized事件。这些事件订阅了定义于Window中的OnEventName为名的函数,如OnClosing,OnClosed,OnInitialized等。这也是.NET框架推荐的对于事件的命名方式。这样我们只需要重载这些OnEventName方法,就可以在Closing等类似事件发生时调用我们自定义的处理函数。代码如:
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { base.OnClosing(e); //our processing } protected override void OnClosed(EventArgs e) { base.OnClosed(e); //our processing } }
提示:通过将传入OnClosing函数CancelEventArgs类型的参数的Cancel属性设置为true可以取消关闭事件的进一步发生。OnClsoed中不可以取消关闭。
-
NavigationWindow
NavigationWindow对象是Window对象的扩展,它增加了XAML页面间导航的支持,包括Navigation方法允许以编程方式从一个页面导航到另一个页面。
NavigationWindow窗体的默认样式如下,其中包含了默认支持向前与向后的按钮(当然这个按钮的功能可以自定义)。另外VS2008及以后版本的Visual Studio的WPF模版都不支持直接创建一个NavigationWindow。可以创建一个普通的Window,并在其基础上将其改成一个NavigationWindow。改后代码如下,改动部分以粗体展示:
<NavigationWindow x:Class="WpfApplication1.Window2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window2" Height="300" Width="300"> </NavigationWindow>
后置代码文件同样需要改动:
public partial class Window2 : NavigationWindow { public Window2() { InitializeComponent(); } }
另外注意要添加对命名空间System.Windows.Navigation的引用。
运行状态的NavigationWindow默认导航形如:
绿色圈起来的即是导航按钮。
另外由于主窗体包含了窗体对象的引用,所以要想访问导航用到的方法事件或属性,必须把主窗体设置为NavagationWindow对象。
-
Page
Page对象提供一种替代方式访问NavigationWindow对象。使用Page可以任意在Window或NavigationWindow作为根元素的窗体间切换。(而NavigationWindow无法做到)。即Page用于组织要使用导航特性的内容。Page是Window的简化版本,可以寄宿于NavigationWindow或Frame这两种内建的导航容器中,这两种导航容器提供从一页导航到另一页的方式,可以跟踪并记录所有导航记录,同时提供了一系列导航相关的事件供用户选择处理。
提示:两个Page可寄宿容器NavigationWindow与Frame之间的差别
这 两个类功能大致相同,使用Web类比,则NavigationWindow像一种顶层浏览器,而Frame像Html中的Frame和IFrame。 Frame也可以像顶级窗口NavigationWindow那样填充父元素的任意区域(除矩形区域),同时Frame也可以被嵌套在 NavigationWindow或其它Frame中。
默认设置下,NavigationWindow顶部会显示导航条(见NavigationWindow部分的插图),而Frame中不会显示。有两种方法可以控制在这两个导航容器中添加与移除这个导航控件:
通过设置宿主于容器的Page的ShowsNavigationUI来控制容器中导航控件的显示与否。
NavigationWindow提供与Page中同名属性控制它自身的导航控件的显示,Frame提供名为NavigationUIVisibility的属性来控制自身导航控件。这种设置方式优先级高于在Page中设置。
下面具体介绍怎么实现这个可导航的页面,首先我们需要建立一个前文介绍的NavigationWindow(方法上文有介绍),将其名为MainNavWindow.xaml。然后修改App.xaml将StartupUri指向这个新建的导航容器MainNavWindow.xaml。之后我们创建一个Page(可以通过模版快速建立),名为MainPage.xaml。多说两句,作为精简版的Window,其xaml文件与Window的很类似,包括一些命名空间的引用,区别之处在于在Page的后置代码文件中已经找不到OnClosing与Closed的处理逻辑,因为总是应该在Window中处理这两个事件。最后我们把导航容器MainNavWindow.xaml的Source属性指定为MainPage.xaml,这个一个基础的导航应用程序的架子就搭成了,当然这是第一步,此时导航条也是处于禁用状态。
Page可以与其宿主的导航容器进行交互,这个特性是NavigationService来支持的,NavigationService同时支持NavigationWindow与Frame这个两个宿主。要使用NavigationService首先要得到其实例,有两种方法:
-
调用NavigationService.GetNavigationService()这个静态方法,并传入Page实例作为参数。
-
直接使用Page的NavigationService属性
相比而言后者更简单。
下面的代码演示了使用NavigationService来为菜单按钮添加导航的能力(这些代码放置于按钮点击事件的处理函数中)。
转到指定页面:
this.NavigationService.Title = "the second page";
刷新当前页面:
this.NavigationService.Refresh();
最后要说一下,Page的属性中有一些是用来控制父容器行为的,如WindowHeight、WindowWidth与WindowTitle等,可以直接在xaml的Page元素中设置。
WPF多线程编程
WPF中的UI运行于一个单独的线程,这些运行于UI线程的对象都是继承自DispatcherObject的。除了那些被冻结(Freezable)的对象,在其它线程中都不可以访问UI线程中的对象。要访问UI线程中的对象,需要通过这些DispatcherObject类型对象的Dispatcher类型的Dispatcher属性中的Invoke与BeginInvoke方法及它们的重载。这些方法允许你传入一个委托,从而使相应的方法可以在相应DispatcherObject所在的线程上被调用。另外所有Invoke与BeginInvoke及它们的重载都需要接受一个DispatcherPriority枚举类型的参数,这个枚举定义了10中优先级,范围由最高的Send(直接执行)到最低的SystemIdle(Dispatcher队列空闲时执行)。
注意:可以直接调用Dispatcher.Run来产生单独的UI线程来运行应用程序,比如可以将一些需要运行在顶层的窗体使用该方法放到单独的线程中以提高响应速度。但是这样做的副作用是破坏Application的封装,即Application不再与整个程序相关联,而是与创建其所在线程的相应的Dispatcher相关联,这种情况下,像Application.Window属性也就只包含与其处在同一个线程中的Window对象。
本文完
参考:
《WPF揭秘》