第一章 Application类和Window类 [App = Code + Markup]
在Windows下WPF应用程序的生命周期通常都起始于创建Application和Window类的实例。来看一个简单的WPF程序:
SayHello.cs
//-----------------------------------------
// SayHello.cs (c) 2006 by Charles Petzold
//-----------------------------------------
using System;
using System.Windows;
namespace Petzold.SayHello
{
class SayHello
{
[STAThread]
public static void Main()
{
Window win = new Window();
win.Title = "Say Hello";
win.Show();
Application app = new Application();
app.Run();
}
}
}
我假设你熟悉上面引用的System命名空间,如果不熟悉的话,你可以读一下《.NET Book Zero》,这本在线书籍可以在我的网站www.charlespetzold.com上找到。上面的程序还引用了System.Windows命名空间,WPF中所有的基础类、结构、接口、委托和枚举都在该命名空间之内,包括Application和Window类。WPF中其他命名空间的都以System.Windows作为名字的开头,比如System.Windows.Controls、 System.Windows.Input 和 System.Windows.Media。对于这个命名规则来说, System.Windows.Forms命名空间显然是个例外,它属于WinForm。以System.Windows.Forms开头的命名空间一般也都属于WinForm,除了用于集成WinForm和WPF的System.Windows.Forms.Integration。
本书中的示例程序遵守统一的命名习惯,每个程序都关联有一个VS项目,项目中的所有代码都被包含在一个命名空间内,这些命名空间通常以我的姓Petzold加上项目的名称来命名。比如说,上面例子的项目名称是SayHello,因而命名空间是Petzold.SayHello。项目中的每个类都将拥有一个单独的源文件,源文件的名称一般与类名相同。如果项目象第一个例子那样仅包含一个类,则一般用项目名称来命名这个类。
在任何WPF程序中,Main函数都必须加上attribute[STAThread],否则编译器就会抱怨^_^。这个attribute指定程序的初始线程模型为STA--单线程套间,这是实现WPF与COM间的互操作性所必须的。STA是一个属于旧的COM时代,先于.Net时代的术语,对我们而言,可以这样简单地理解它:我们的WPF程序不会使用多个由运行时创建的线程。[注1]
在SayHello程序中,Main函数从创建一个Window类的实例开始执行,该类是标准的应用程序窗体类。窗体类的Title属性表示要在标题栏显示的文本,而其Show方法则是负责把窗体显示在屏幕上。
最后的重要步骤是调用Application实例的Run方法。用Windows编程的术语来说,该方法创建了用于接收用户键盘鼠标输入的消息环(message loop)。如果是运行在平板PC上,程序也接收来自手写笔(stylus)的用户输入。
如果你使用VS 2005来创建、编译和运行WPF程序,可以按如下步骤来重建SayHello程序:
1. 从[文件]菜单选择 [新建]-〉[项目…]
2. 在"新建项目"对话框中选择"Visual C#","WPF应用程序","空项目"。给项目找个"并命名为 SayHello。反选"创建解决方案的目录"。点击[确定]。
3. 在右边的"解决方案资源管理器"中,"引用"节点内必须包含对PresentationCore, PresentationFramework, System和 WindowsBase的引用。 如果这些装配件不在"引用"之 列,那就把它们添加进来。右击"引用",选择[添加引用…]。在弹出的对话框中,点击 [.NET]栏位,添加相应的装配件。点击[确定]。
4. 在右边的"解决方案资源管理器"中,右击项目名称SayHello,选择[添加]-〉[添加新项](也可以从[项目]菜单中选择[添加新项])。在"添加新项"对话框里面选择"代码文件",键入文件名"SayHello.cs"。
5. 键入上文中的代码。
6. 从[调试]菜单选择[开始执行(不调试)](或者按Ctrl+F5)来编译运行。
在本书的第一部分(包括本章)中,除了那些拥有多个源元件的程序,创建程序项目的步骤基本上都与上面一致。
关掉SayHello窗口,你会发现有一个命令行窗口也一直在运行。这个命令行窗口是由一个编译参数控制的,可以在项目的属性里面设置该参数。右击项目名称,选择"属性",然后你就可以查看/修改项目的各种设置。需要注意的是:项目当前的输出类型设置是"控制台应用程序" 。很明显,这个设置并没有影响到程序逾越控制台应用程序的范围去创建窗体对象。把输出类型更改为"Windows应用程序",程序将仍会正常运行,但是不会再有命令行窗口出现。我个人在做开发的时候喜欢让命令行窗口同步运行,这样我可以在运行和调试的时候用它来显示一些文本信息。如果bug太多,程序甚至根本不显示窗体出来,抑或窗体显示出来了但是却陷入了无限循环,那么可以很容易地在命令行窗口中按 Ctrl+C 来结束程序。
在SayHello中用到的Window 和Application类都继承于DispatcherObject类,但是从下图中可以看到Window类的继承层次要比Application类深得多:
当然了,现在你还并不需要对上面的类层次非常熟悉,但是随着对WPF的深入学习,你会不断地碰到它们。
一个程序仅能创建一个Application对象,该对象充当了程序其余部分一个固定的锚。Application对象不可见,但Window对象可见。Window对象作为标准窗体显现在桌面上,它有一个显示Title属性值的标题栏。这个标题栏的左端有一个系统菜单图标,右端有"最小化"、"最小化"和"关闭"按钮。Window窗体对象还有可调边框和占有窗体内部大块区域的客户区(client area)。
在SayHello程序的Main函数里面,你可以在某种程度上自由地调整语句的顺序,而程序仍将可以运行。比如说,你可以在调用Show方法之后才设置Title属性。这个改动从理论上来说将让窗体的初始标题栏不显示任何标题内容,但是由于程序运行的太快,你可能根本观察不到这个状态。
Application对象可以先于Window对象之前被创建,但是对其Run方法的调用必须放在Main函数的最后。Run方法在窗体被关闭之前不会返回,当它返回的时候Main函数也就执行完毕,Windows随即开始清理程序资源。如果把对Run方法的调用去掉,窗体仍会被创建和显示出来,但是它会在Main函数结束时立即被销毁。
除了调用Window窗体对象的Show方法,你还可以把它作为参数传递给Application实例的Run方法:
app.Run(win);
在这种情况下,Run方法就负有调用窗体对象Show方法的责任。
程序在调用Run方法之前其实并未真正地启动完毕,而窗体对象在调用Run方法之后才能够相应用户的输入。用户关闭窗体的时候Run方法将返回,程序此时亦行将结束。那么如果程序把所有的时间都花在了Run方法之内,它是怎么处理其他事情的呢?
事实上,程序在初始化完毕之后所作的全部事情就是等待/响应事件。这些事件通常意味着用户的键盘、鼠标或者手写笔输入。UIElement类(当然了,UI指的是用户界面)定义了许多与键盘、鼠标和手写笔相关的事件,Window类继承了所有的这些事件。其中的一个事件是MouseDown事件,由用户用鼠标点击客户区的动作触发。
下面的程序调整了Main函数中一些语句的顺序并为窗体的MouseDown事件添加了一个处理程序:
//----------------------------------------------
// HandleAnEvent.cs (c) 2006 by Charles Petzold
//----------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
namespace Petzold.HandleAnEvent
{
class HandleAnEvent
{
[STAThread]
public static void Main()
{
Application app = new Application();
Window win = new Window();
win.Title = "Handle An Event";
win.MouseDown += WindowOnMouseDown;
app.Run(win);
}
static void WindowOnMouseDown(object
sender, MouseButtonEventArgs args)
{
Window win = sender as Window;
string strMessage =
string.Format("Window clicked with
{0} button at point ({1})",
args.ChangedButton,
args.GetPosition(win));
MessageBox.Show(strMessage, win.Title);
}
}
}
我对事件处理程序惯用的命名习惯是:相关类的类名 + 事件名称,比如上面的WindowOnMouseDown。但是你个人可以用任何你喜欢的方式来命名。
MouseDown的文档中指明了它需要一个与MouseButtonEventHandler委托相匹配的事件处理程序,处理程序的第一个参数的类型要求是object类,第二个参数类型是MouseButtonEventArgs类。MouseButtonEventArgs类定义在System.Windows.Input命名空间下,所以代码中包含了对该命名空间的引用指令。由于是在静态(static)的Main函数中被引用,上面程序中的事件处理程序必须也被定义为静态的。
本书中后面的大多数程序中都会包含一个对System.Windows.Input命名空间的引用指令,即便他们并不真的需要它。
无论何时,只要用户用鼠标的任意按键点击了窗体的客户区,MouseDown事件就会被触发。传给处理程序的第一个参数是触发该事件的对象,这里就是那个Window窗体,因此处理程序可以安全地把参数类型转换成Window窗体类型。
窗体对象对上面的处理程序而言有两个作用:首先,被作为参数提供给MouseButtonEventArgs类的GetPosition方法,该方法返回一个Point类型(System.Windows中的一个结构体)的对象,包含鼠标相对于传入参数的左上角的坐标;其次,访问其Title属性并把它作为弹出消息框的标题。
MessageBox类同样被定义在System.Windows命名空间内,它包含有静态方法Show的12个重载,这些重载为在消息框中显示不同的按钮和图标组合提供了丰富的选择。缺省情况下,只有[确定]按钮显示出来。
上面程序中的消息框显示了鼠标相对于客户区左上角的的坐标,你可能想当然地以为那些坐标的单位是像素,然而事实上并非如此。他们是具有设备独立性的单位--1/96英寸。在本章的稍后部分我会更详细地介绍这种"古怪的"单位。
上面程序中的事件处理程序通过对sender参数的类型转换获取窗体实例,这并不是唯一的途径。比如说,你可以把该窗体实例声明为静态的,这样处理程序就可以直接使用它。同样地,你也可以通过Application类的属性来获取该窗体:静态属性Current用于返回当前程序创建的Application实例(正如我前面提到的那样,一个程序仅能创建一个Application实例),而其实例属性MainWindow就是用来返回窗体对象。因此,上面的事件处理程序可以像这样来设置局部变量win:
Window win = Application.Current.MainWindow;
如果事件处理程序获取窗体实例的唯一目的就是得到其标题内容以提供给消息框,那么你可以选择把Application.Current.MainWindow.Title作为一个参数传给MessageBox.Show方法。
Application类定义了一些有用的事件,大部分事件都对应着一个protected方法来负责触发这些事件,这一点符合.NET的一贯作风。其protected OnStartup方法负责触发Startup事件,在Run方法被调用之后很快也被调用;OnExit方法则会在Run方法返回前被调用。你可以利用这两个时机来进行整个应用范围内的初始化或者清理工作。
OnSessionEnding方法和SessionEnding事件关联于用户的注销或者关机动作。该事件有一个SessionEndingCancelEventArgs类型的参数,SessionEndingCancelEventArgs类继承于CancelEventArgs类,继承的内容包括了Cancel属性。如果你的程序希望阻止Windows的关机进程,可以设置Cancel属性为true。需要注意的是:仅当项目的输出类型为Windows应用程序的时候,程序才能接收到该事件。
如果你需要处理Application类的事件,你可以选择为事件添加处理程序,但是最方便的做法是象下面例子InheritTheApp一样定义一个类来继承Application类。在Application的继承类中可以简单地重写那些负责触发事件的幕后方法。
//----------------------------------------------
// InheritTheApp.cs (c) 2006 by Charles Petzold
//----------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
namespace Petzold.InheritTheApp
{
class InheritTheApp : Application
{
[STAThread]
public static void Main()
{
InheritTheApp app = new InheritTheApp();
app.Run();
}
protected override void OnStartup
(StartupEventArgs args)
{
base.OnStartup(args);
Window win = new Window();
win.Title = "Inherit the App";
win.Show();
}
protected override void OnSessionEnding
(SessionEndingCancelEventArgs args)
{
base.OnSessionEnding(args);
MessageBoxResult result =
MessageBox.Show("Do you want to
save your data?",
MainWindow.Title,
MessageBoxButton.YesNoCancel,
MessageBoxImage
.Question, MessageBoxResult.Yes);
args.Cancel = (result ==
MessageBoxResult.Cancel);
}
}
}
InheritTheApp类继承自Application类并重写了其OnStartup 和OnSessionEnding方法。在这个程序中,Main方法并未创建Application类的实例,而是创建了一个InheritTheApp类的实例。由于Main函数是InheritTheApp类的一个成员函数,在它的函数体内部创建一个其所属类的实例看起来可能有些古怪,但是这绝对是合法的,因为Main函数被定义成静态的,它甚至在任何InheritTheApp对象被创建之前就已存在。
上面的程序在重写的OnStartup中创建了窗体对象并显示出来,但是同样也可以在InheritTheApp的构造函数中完成这项任务。
在重写的OnSessionEnding方法中,程序弹出了一个含有[是]、[否]和[取消]按钮的消息框。注意消息框的标题被设置成了MainWindow.Title,这是由于OnSessionEnding方法是继承自Application类的一个实例方法,对MainWindow.Title的引用将直接从当前实例取值。你也可以显式的为MainWindow.Title加上this关键字来更清楚地表示这是一个来自Application类的属性。
当然了,本程序没有什么需要保存的数据,所以忽略了对[是] 和 [否]的相应,从而允许程序被关闭,Windows继续进行终止当前会话的工作。然而,如果用户选择了[取消],那么SessionEndingCancelEventArgs参数的Cancel将被设置成true,从而会阻止Windows的本次关机或者注销操作。你可以通过SessionEndingCancelEventArgs的ReasonSessionEnding属性来查看当前正在进行的是关机还是注销操作,这个属性是枚举类型ReasonSessionEnding的实例,可能的枚举值为ReasonSessionEnding.Logoff和ReasonSessionEnding.Shutdown。
程序中的OnStartup和OnSessionEnding方法都在其开始部分调用了基类的相应实现。严格来说,这些调用在本程序中并不是必需的,但是它们也不会带来什么副作用。通常来说,除非你有特殊的理由,一般都应该进行这些调用。
如你所熟知的那样,可以从命令行窗口来运行程序,并且可以同时给程序传递参数。所有的Windows程序都一个样子。为了处理命令行参数,你就必须稍微修改一下Main函数:
public static void Main(string[] args)
所有的命令行参数一起作为一个字符串数组被传递给Main函数。这个字符串数组也可以在OnStartup方法中通过其StartupEventArgs类型的参数的Args属性来访问。
Application类MainWindow属性的名字(主窗体)的言外之意似乎是说一个程序可以拥有多个窗体,事实的确如此。这些额外的窗体大都以短暂存在的消息框的形式出现,仅在显示方式与交互方式上与标准的主窗体有所不同。
下面的程序将举行一个宴会,把更多的窗体请到桌面上来:
//-------------------------------------------------
// ThrowWindowParty.cs (c) 2006 by Charles Petzold
//-------------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
namespace Petzold.ThrowWindowParty
{
class ThrowWindowParty: Application
{
[STAThread]
public static void Main()
{
ThrowWindowParty app = new
ThrowWindowParty();
app.Run();
}
protected override void OnStartup
(StartupEventArgs args)
{
Window winMain = new Window();
winMain.Title = "Main Window";
winMain.MouseDown += WindowOnMouseDown;
winMain.Show();
for (int i = 0; i < 2; i++)
{
Window win = new Window();
win.Title = "Extra Window No. " +
(i + 1);
win.Show();
}
}
void WindowOnMouseDown(object sender,
MouseButtonEventArgs args)
{
Window win = new Window();
win.Title = "Modal Dialog Box";
win.ShowDialog();
}
}
}
与InheritTheApp类一样,ThrowWindowParty类也继承自Application类并在其重写的OnStartup方法中创建了一个Window窗体对象。随后,它又另外创建了两个Window对象并把它们显示出来(我后面很快就将解释在MouseDown处理程序中发生的事情)。
你首先会注意到:在OnStartup方法中创建的3个窗体在本应用中都是平等的。你点击任何一个窗体,它都会变成显示在最前面。你可以以任意顺序来关闭这些窗体,仅当你关闭了最后一个窗体的时候程序才会终止。如果不是他们中的一个标题栏显示着"Main Window",你将很难把它区分出来。 如果你的程序访问了Application类的MainWindow属性,你会发现被当作应用的主窗体的其实是第一个调用Show方法的窗体(至少是在初始状态下)。
Application类还包含了一个WindowCollection类型的属性Windows(注意其复数形式)。WindowCollection类是一个典型的.NET集合类,实现了ICollection和IEnumerable接口,并象其名称暗示的那样用来保存多个Window窗体对象。该类有一个Count属性和一个索引器,你可以通过它们获取程序中所有调用过Show方法并且仍然存活的窗体。在重写的OnStartup方法的最后,Windows.Count属性将返回3,并且Windows[0]就是那个带有"Main Window"标题的窗体。
本程序的一个古怪之处是:所有的3个窗体都显示在Windows底部的(如果你的系统设置跟大家一样的话)任务栏中。程序在系统任务栏中占据多个栏位通常被认为是非常不酷的。如果希望禁止那些额外的窗体显示在任务栏中,你需要在for循环中加上这么一句:
win.ShowInTaskbar = false;
但是现在另外的怪事又出现了:如果你关掉了标题为"Main Window"的窗体,你会发现程序在系统任务栏中的显示项不见了,但是程序却分明仍然在运行,并且有着两个窗体显示在桌面上。
程序通常是在Run方法返回的时候终止,而Run方法在缺省情况下是在用户关掉最后一个窗体的时候返回。这种行为由Application类的ShutdownMode属性所控制,这是属性是枚举ShutdownMode类型,除了其缺省值ShutdownMode.OnLastWindowClose之外,你可以把它指定为ShutdownMode.OnMainWindowClose。
把下面这一句加到对Run的调用之前:
app.ShutdownMode = ShutdownMode.OnMainWindowClose;
或者把下面的这一句加到重写的OnStartup方法中的任意地方(在Main函数中,你必须在ShutdownMode之前加上对Application对象的引用;而在OnStartup中,你可以直接使用该属性或者在其之前加上this关键字):
ShutdownMode = ShutdownMode.OnMainWindowClose;
现在只要主窗体被关闭,Run方法就会返回并且程序随即终止。
保留上面对ShutdownMode属性的改变,在for循环中加进这么一句:
MainWindow = win;
你会回想起来MainWindow是Application类的一个属性。你可以通过它为程序指定一个主窗体。当for循环结束的时候,标题为"Extra Window No. 2"的窗体将被设置为程序的主窗体,因此要终止程序就必须关闭该窗体。
对ShutdownMode属性来说,还有另外一个可能的枚举值: ShutdownMode.OnExplicitShutdown。这个值将会使得Run方法仅在程序显式调用Application类的Shutdown方法才会返回。
现在把前面插入的所有与ShutdownMode和MainWindow属性的代码都删掉,你还有一种途径来指定窗体间的层次关系:通过Window类的Owner属性。缺省情况下,Owner属性是null空值,也就是说窗体不被任何其他窗体所拥有。你可以把窗体的Ower属性设置为程序中的其他窗体对象(注意不可以有环状的拥有关系)。举例来说,把下面这行代码插到for循环中:
win.Owner = winMain;
现在两个额外的窗体都为主窗体所拥有。你仍然可以前后切换这3个窗体,但是当你前后切换的时候你会发现窗体总是会显示在其拥有者的前面。当你最小化拥有者的时候,它所拥有的窗体也都从屏幕上消失;当你关闭拥有者的时候,它所拥有的窗体也都自动被关闭。换言之,这两个额外的窗口现在变成了非模态对话框(modeless dialog boxes)。
在对话框的两种主要类别中,非模态是较少使用的一种。更多使用的是模态的对话框。你可以通过点击ThrowWindowParty主窗体的客户区来得到一个模态的对话框。 WindowOnMouseDown方法创建一个Window窗体,设置它的Title属性,但是并未调用它的Show方法,取而代之的是ShowDialog方法。与Show方法不同的是,ShowDialog方法并不立即返回,它弹出的模态对话框不允许你切换到程序的其他窗体(但是它允许你切换到Windows下运行的其他程序)。ShowDialog方法仅在你关掉模态对话框的时才会返回。与此相反,非模态的对话框允许你与主程序的其他窗体进行交互。VS里面的"快速查找"就是非模态对话框一个不错的例子。它允许你查找源代码中的字符串,并且在未被关闭的状态下同时允许你编辑源代码。
模态对话框会截获用户的输入,并在其被关闭前阻止你与程序中其他窗体的交互,而非模态的对话框则不这么做。
现在来做个试验,跳到第一个例子SayHello,把Show方法改成ShowDialog并注释掉所有对Application实例对象的引用。可以看到程序仍能正常运行,其奥秘在于ShowDialog实现了自己的消息环(message loop)来处理输入事件。模态对话框之所以成为其"模态"就是因为它不参与应用程序的消息环,并因此而阻止应用程序获得用户的输入事件。
前面的两个程序都定义了继承自Application的类,但是程序同样也可以(并且经常就是这样)选择定义继承自Window的类。下面的程序包含了3个类和他们的源文件。在VS 2005,要添加的新的空源文件,可以右击"解决方案资源管理器"中的项目名称,然后在弹出的菜单中选择[添加新项...]。你也可以从[项目]菜单中选择 [添加新项...]。甭管你怎么做,要添加的都是缺省为空的"代码文件"。
本项目的名称是InheritAppAndWindow,这同时也是包含有Main函数的类的名称:
//----------------------------------------------------
// InheritAppAndWindow.cs (c) 2006 by Charles Petzold
//----------------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
namespace Petzold.InheritAppAndWindow
{
class InheritAppAndWindow
{
[STAThread]
public static void Main()
{
MyApplication app = new MyApplication();
app.Run();
}
}
}
Main函数创建了一个MyApplication类的实例,并调用了其Run方法。MyApplication类继承自Application类,定义如下:
//----------------------------------------------
// MyApplication.cs (c) 2006 by Charles Petzold
//----------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
namespace Petzold.InheritAppAndWindow
{
class MyApplication : Application
{
protected override void OnStartup
(StartupEventArgs args)
{
base.OnStartup(args);
MyWindow win = new MyWindow();
win.Show();
}
}
}
在重写的OnStartup方法中,类创建了一个MyWindow类型的对象。MyWindow是项目中的第三个类,继承自Window类:
//-----------------------------------------
// MyWindow.cs (c) 2006 by Charles Petzold
//-----------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
namespace Petzold.InheritAppAndWindow
{
public class MyWindow : Window
{
public MyWindow()
{
Title = "Inherit App & Window";
}
protected override void OnMouseDown
(MouseButtonEventArgs args)
{
base.OnMouseDown(args);
string strMessage =
string.Format("Window clicked with
{0} button at point ({1})",
args.ChangedButton,
args.GetPosition(this));
MessageBox.Show(strMessage, Title);
}
}
}
Window类的继承类通常都是在构造函数中完成初始化的工作。MyWindow类唯一的自定义初始化操作是设置它的Title属性。请注意,你不需要在Title属性前面加上任何对象名称来对它进行引用,因为MyWindow从Window类继承了该属性。你也可以选择在Title属性前面加上this关键字以进行引用:
this.Title = "Inherit App & Window";
除了为MouseDown添加事件处理程序,MyClass类还可以重写OnMouseDown方法以达到同样的目的。因为OnMouseDown是一个实例方法,它可以把关键字this作为参数传递给GetPosition方法来为其提供窗体对象。此外,它还可以直接访问Title属性。
虽然上面的程序并没有任何问题,但是一个仅包含代码的WPF程序更常见(也更容易)的做法是定义一个继承自Window而非Application的类。这里是一个典型的单文件程序:
//----------------------------------------------
// InheritTheWin.cs (c) 2006 by Charles Petzold
//----------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
namespace Petzold.InheritTheWin
{
class InheritTheWin : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new InheritTheWin());
}
public InheritTheWin()
{
Title = "Inherit the Win";
}
}
}
我将在本书第一部分中的示例程序中广为使用这种结构。如你所见,它很短,并且如果你真的想给Main函数瘦身,你可以把它所有的功能在一个语句中实现:
new Application().Run(new InheritTheWin());
现在,我们来玩玩这个程序。我将会提些更改这个程序的建议,你可以跟着照做,也可以(甚至更好)自己折腾些花样出来。
窗体在屏幕上的位置与大小缺省由操作系统决定,但是你也可以自己来设置。Window类从FrameworkElement类继承了Width与Height属性,你可以在构造函数中修改它们:
Width = 288;
Height = 192;
你并不是只能用整数来对它们进行赋值,它们都被定义为双精度浮点数,因此你可以这样来赋值:
Width = 100 * Math.PI;
Height = 100 * Math.E;
Width和Height属性在初始情况下都是未定义的,如果你的程序从未对其进行设置,它们将保持未定义的状态,这就意味着它们的值一直是NaN――"not a number"(不是一个数字)的简写,这种写法由IEEE作为浮点数标准之一推出并广为使用。
因此当需要获取窗体的实际大小的时候,不要使用Width和Height属性,你应该使用只读属性ActualWidth和ActualHeight。然而,只读属性ActualWidth和ActualHeight的值在窗体的构造函数中为零,仅在窗体被显示在屏幕上之后,这两个属性才是可用的。
在上面用到的两个语句中,我看起来好像是随意选择了两个数:
Width = 288;
Height = 192;
注意这些数不是以像素为单位的,如果Width和Height属性是以像素为单位的话,那么他们就没有必要定义为双精度的浮点数了。WPF中用于表示所有坐标和位置的单位有时被称为“独立于设备的像素”或者“逻辑像素”,但是也许最好的叫法就是根本不要提及像素这两个字。我将称他们为"独立于设备的单位"。每单位是1/96英寸,所以上面用到的数字288和192实际上意味着窗体被设置成3英寸宽2英寸高。
如果(这些数是以像素为单位的话,那么当[飞扬跋扈添加])你拿个尺子来量你的显示器,你可能并不会精确测得这些值。像素与英寸的比例关系由Windows系统确定,并且用户可以对其加以修改。右击你的屏幕,选择"属性"。点击"设置"栏位,然后点击[高级]按钮,在弹出的对话框里点击"常规"栏位。
缺省情况下,Windows把屏幕分辨率设置为每英寸96个点,如果你的电脑是这样设置的话,窗体的Width和Height属性的值就与以像素为单位的值相同。
然而,如果你把屏幕分辨率设置成每英寸120个点(出生在《星球大战》之前的人常用的一个分辨率),那么当WPF把窗体的Width和Height属性分别设置为288和192的时候,窗体的Width和Height就分别为360和240像素,仍然是3英寸宽2英寸高。
针对未来将会出现的支持更高分辨率的显示器,我们的WPF程序应该能够不加修改地在其上运行。举例而言,假设有一个几乎达到每英寸200像素的显示器,为避免所有的东西在屏幕上都变得很小,用户需要使用"显示"选项来设置一个相称的分辨率,比如说每英寸192个点。当WPF把窗体的Width和Height属性分别设置为288和192的时候,这些尺寸现在变成了576和384像素,仍然将会是3英寸宽2英寸高^_^。
这种独立于设备的单位在WPF中得到了普遍的应用。比如说,本章中前面一些程序使用消息框来显示鼠标点击客户区时相对于其左上角的位置,使用的单位就不是像素,而是这种具有设备独立性的单位――1/96英寸。
如果你把Width和Height属性设置成很小的数,你将会发现窗体总是会至少把它的部分标题栏显示出来。这些最小尺寸可以从静态只读的SystemParameters.MinimumWindowWidth和SystemParameters.MinimumWindowHeight属性中获取,单位同样是这种具有设备独立性的单位――1/96英寸。SystemParameters类有着很多像这样的静态属性。
如果你想让窗体出现在屏幕上的特定位置,可以设置Window类的Left 和Top 属性:
Left = 500;
Top = 250;
这两个属性指定了窗体的左上角相对于屏幕左上角的位置,同样使用这种具有设备独立性的单位――1/96英寸。如果你从未设置过它们,它们也将保持NaN的值。此外,Window类并未定义Right和Bottom属性,它们可以经由Left 和Top 属性与窗体的大小来获得。
假设你的显示适配器和显示器都能水平显示1600像素竖直显示1200像素,并且你的屏幕设置为1600 * 1200,那么当你访问SystemParameters类的MinimumWindowWidth和MinimumWindowHeight属性时,他们的值会是1600 和 1200么?答案是:仅当你的屏幕设置是每英寸96个点的时候,他们才是1600 和 1200,同时意味着你的显示区域为16-2/3 英寸* 12-1/2 英寸。
假设你的屏幕设置是每英寸120个点,而SystemParameters类的MinimumWindowWidth和MinimumWindowHeight属性的返回值分别是1280 和 960个独立于设备的单位,那么对应的显示区域就是13-1/3 英寸 * 10 英寸。
SystemParameters类中几乎所有的尺寸都是以独立于设备的单位来表示,仅有的两个例外是SmallIconWidth 和SmallIconHeight属性,这两个属性使用的单位是像素。因此,你在大部分情况下都可以不加任何单位变换地使用SystemParameters类提供的值来进行计算。比如说,你可以使用下面的代码来设置窗体的位置为屏幕的右下角:
Left = SystemParameters.PrimaryScreenWidth - Width;
Top = SystemParameters.PrimaryScreenHeight - Height;
这两行代码传达的信息是:你已经设置过Width 和Height属性,但是现在并不喜欢曾经的设置。如果你的屏幕底部有一个系统任务栏,它将会遮住窗体的底部,因此,你可能会更希望把窗体的位置设置为系统工作区的右下角。所谓“系统工作区”指的是屏幕上未被任何工具栏(其中最常见的就是Windows任务栏)覆盖的区域。
SystemParameters.WorkArea属性表示系统工作区,返回Rect类型的实例,Rect是一个用左上角坐标和大小来表示长方形的结构体。由于用户可能会把任务栏放在屏幕的左端(或上端右端),WorkArea属性不能够简单地用宽和高来表示,而是必须定义为Rect类型。当用户把任务栏放在屏幕的左端时,其Left属性就是一个非零值,而Width属性就将是屏幕宽度减去Left属性值。
这里是把窗体显示在系统工作区的右下角的代码:
Left = SystemParameters.WorkArea.Width - Width;
Top = SystemParameters.WorkArea.Height - Height;
下面的代码则是让窗体显示在系统工作区中央的代码:
Left = (SystemParameters.WorkArea.Width - Width) / 2 + SystemParameters.WorkArea.Left;
Top = (SystemParameters.WorkArea.Height - Height) / 2 + SystemParameters.WorkArea.Top;
你也可以使用Window类的WindowStartupLocation属性来达到与上面一样的效果。它是枚举WindowStartupLocation类型的实例,缺省为WindowStartupLocation.Manual,意味着由程序或者操作系统来决定窗体的位置。你可以把它设置成WindowStartupLocation.CenterScreen,虽然CenterScreen(屏幕中央)从字面上看起来像是屏幕的中央,但是事实上窗体会显示在系统工作区的中央。(第三个值是WindowStartupLocation.CenterOwner,你可以通过它来让模态对话框显示在其拥有者的中央。)
下面的这个程序把窗体显示在系统工作区的中央,你可以通过键盘上的上下方向键来按10%的比例改变其大小:
//----------------------------------------------
// GrowAndShrink.cs (c) 2006 by Charles Petzold
//----------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
namespace Petzold.GrowAndShrink
{
public class GrowAndShrink : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new GrowAndShrink());
}
public GrowAndShrink()
{
Title = "Grow & Shrink";
WindowStartupLocation =
WindowStartupLocation.CenterScreen;
Width = 192;
Height = 192;
}
protected override void OnKeyDown
(KeyEventArgs args)
{
base.OnKeyDown(args);
if (args.Key == Key.Up)
{
Left -= 0.05 * Width;
Top -= 0.05 * Height;
Width *= 1.1;
Height *= 1.1;
}
else if (args.Key == Key.Down)
{
Left += 0.05 * (Width /= 1.1);
Top += 0.05 * (Height /= 1.1);
}
}
}
}
上面的OnKeyDown方法(以及相关的KeyDown事件)会响应用户的击键动作。在按下然后放开键盘上按键的过程中,OnKeyDown和OnKeyUp方法会依次被调用,可以重写这两个方法来处理相应的按键事件。它们使用的KeyEventArgs类型的参数有一个Key属性,对应于大枚举类Key的一个成员,代表着要处理的键。由于Left、 Top、 Width和 Height属性都是浮点数类型,在增大和缩小窗体大小的过程中不会损失任何信息。也许你会逾越系统为窗体设定的最大值或者最小值,从而使得窗体看起来没有继续响应,但是相关属性仍会继续忠实地记录它们的运算结果。
OnKeyDown和OnKeyUp方法适用于取得方向键和功能键的键击,但是如果希望获取键盘实际输入Unicode字符,你应该重写的是OnTextInput方法。该方法所使用的TextCompositionEventArgs类型的参数有一个Text属性,是一个Unicode编码的字符串。通常情况下,该字符串仅会包含一个字符,但是语音和手写输入也可能会调用OnTextInput方法,这时候对应的字符串就可能会比较长了。
下面的这个程序并没有在代码中设置Title属性,相反地,你可以输入你自己期望的标题内容:
//----------------------------------------------
// TypeYourTitle.cs (c) 2006 by Charles Petzold
//----------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
namespace Petzold.TypeYourTitle
{
public class TypeYourTitle : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new TypeYourTitle());
}
protected override void OnTextInput
(TextCompositionEventArgs args)
{
base.OnTextInput(args);
if (args.Text == "\b" && Title.Length > 0)
Title = Title.Substring(0, Title
.Length - 1);
else if (args.Text.Length > 0 && !Char
.IsControl(args.Text[0]))
Title += args.Text;
}
}
}
本程序中唯一允许的控制字符是回格键(backspace)'\b',并且仅当Title属性包含有至少一个字符的时候才做出响应。对应于其他输入,程序只是简单地把键入的内容追加到窗体的标题中。
Window类还定义了一些其他影响窗体外观和行为的属性。
窗体风格属性WindowStyle为枚举WindowStyle类型的实例,缺省值为WindowStyle.SingleBorderWindow。可取的枚举值WindowStyle.ThreeDBorderWindow会使得窗体看起来更加吸引人,但是事实上同时使得客户区缩小了一点点。枚举值WindowStyle.ToolWindow通常是用来设置对话框的,它使得窗体的标题栏稍微短些,没有[最大化]和[最小化]按钮,只有[关闭]按钮,但是你仍然可以通过按Alt+空格键来调出系统菜单以对窗体进行最大化和最小化操作,你也可以手工调整窗口大小。枚举值WindowStyle.None对应的窗体风格也有一个可调边框,但是没有标题栏,你同样可以通过按Alt+空格键来调出系统菜单,此时虽然对应窗体的Title属性不会显示在窗体上,但是会显示在系统任务栏中。
窗体可调边框的显现与否取决于ResizeMode属性,这个属性是枚举ResizeMode类型的实例。其缺省值为ResizeMode.CanResize,允许用户调整窗体的大小,进行最大化或者最小化操作。枚举值ResizeMode.CanResizeWithGrip会使得窗体客户区的右下角显示一个小把手(grip)。枚举值ResizeMode.CanMinimize会隐藏可调边框并使得[最大化]按钮不可用,但是仍允许窗体最小化,一般在应用于大小固定的窗体。最后,枚举值ResizeMode.NoResize会隐藏[最大化]、[最小化]按钮以及可调边框。
窗体状态属性WindowState为枚举WindowState类型的实例,用于控制窗口的初始显示方式。可供选择的值包括WindowState.Normal、 WindowState.Minimized和WindowState.Maximized。
把窗体的Topmost属性设置为true会使得窗体显示在最前端。(你应审慎使用该设置,仅应在有特殊用意时使用该设置。当然了,同时要给用户提供关掉它的选项。)
Window类还有一个重要的属性:Background属性。这是个从Control类继承来的属性,它控制着客户区的颜色。但是,简单地将可对Background属性进行的操作描述为对颜色的控制未免有点过于草率。Background属性其实是Brush类的实例,可用的刷子包括梯度刷子(gradient brush)和基于位图及其他图像的刷子。Brush类在WPF中扮演的角色是如此的重要,以至于本书的两章都是为它而写。这两章中的第一章就是接下来的一章,咱们现在就来探个究竟吧。