Windows应用程序为用户交互提供了一个图形化的界面。大多数的用户都熟悉普遍的GUI样式。例如,他们认为按钮可以点击,知道在文本框输入文本。这些样式通常由很多可视化的独立“模块”组成。例如,一个文本框包括:外面的矩形边框,一列代表文本的字符,以及一个闪烁的竖线,用来表示下一个字符出现的位置。用户认识到,这些“模块”构成了一个个提供特定功能的单一实体。在WPF中,这些复合的实体被称为控件。
有很多办法修改控件的外观。对控件的简单属性进行调整,例如设置背景色和前景色。随着控件支持*content model,我们可以在控件中混合放置图形和文本。我们甚至可以使用模板来替换控件的外观。尽管如此,即使我们替换了它的外观,比如说一个scrollbar,我们并未改变其具有滚动效果的这样一个基本角色。在WPF中,这是这种行为构成了空间的本质。
在这一章,我们要考察如何使用控件处理输入,同时,我们将探究嵌入型(built-in)控件提供的一系列行为。
注:content model内容模型,XML术语,定义了如何使底层可分享(sharable)、可重复使用(reusable)的学习资源(learning resources)聚集为高阶的学习组件。参见http://www.chinaret.com/user/team_rss.aspx?u=xianglong&TagId=0665e0e9-11e2-4a72-886d-948d251a0fc7
3.1 什么是控件
对于一个应用程序而言,控件是搭建用户界面的积木。它们具备交互式的特征,例如文本框、按钮以及列表框。尽管如此,WPF还有点不寻常,在于其并不直接地依赖其外观。在自定义一个控件外观时,很多GUI框架要求设计人员编写一个自定义控件。在WPF中,这是不必要的:嵌入式内容和模板提供了简单而强有力的解决方案。仅当你所需要的控件,其行为在已有嵌套式控件中不存在时,才需要重新去开发。
#很多WPF用户界面元素并不是控件。例如一些形状:Rectangle、Ellipse,它们不具有内在的行为,仅仅是一个外观。
图3-1示了如何将一个控件适合于一个程序。正如你所看到的,控件的可见部分,是由其模板提供的,而不是控件本身。这个控件并不完全分离于其外观,当然,控件使用这些外观向用户提供信息。此外,由于这些外观是用户所能看到的全部,所以它们将是任何用户输入的即时目标。这意味着虽然这些外观可以被替换,这些替换具备特定的职责。在控件与其外观之间存在一种契约方式。模板,用来替换控件的外观,其使用将在第5章讨论。
图3-1
#你可能熟悉MVC(Model View Controller)概念。这是设计交互式系统的一种方式。以往,MVC已经有若干种阐释方式。但是广泛地说,它经常将设计拆分为:表示底层数据的对象(Model),展示数据的对象(View),管理用户输入以及管理del与View交互的对象(Controller)。
尽管控件通过外观使其自身对用户可见,通过API(见图3-1左)使其服务对开发者有效。控件提供了命令这个概念,来提供其支持的操作。例如,一个文本框提供了剪切、复制和粘贴命令。控件提供了属性这个概念,从而提供了一种修改行为或外观的方式。当有重要情况发生时,例如接收到外界的输入,控件会激活Event。最后,一些功能通过Method对外暴露。要优先选择命令、属性和事件,因为它们易于在标记语言中使用而且被一些设计工具支持。尽管如此,仍然存在一些情况,这些控件要在后台代码中使用,方法这时候就是最合适的API表现形式。
当然,开发人员和设计人员并不是仅有的使用控件的人。控件同时要响应用户,所以,让我们看一看,在WPF中,输入是如何被处理的。
3.2 处理输入
在Windows应用程序中,又3种基本的用户输入形式:鼠标、键盘和手写板。同时,还有一种更高级输入方式,其可能来自快捷键、工具栏的按钮、菜单项。
尽管控件担当着主要的输入对象,用户界面的所有元素都可以接受输入。不必吃惊,这是因为,为了提供外观,控件完全依赖于底层元素的服务,如Rectangle和TextBlock。因此,在用户界面内的元素类型中,所有的输入机制都是有用的,我们将要在接下来的章节介绍这些机制。
3.2.1 Routed事件
.Net框架定义了一个标准的机制来暴露事件。一个类可能暴露了一些事件,每个事件可能有任意数量的订阅者。虽然WPF也使用了这一标准机制,声称其克服了一个局限:如果一个正常.NET事件没有注册句柄,该事件将被视为无效并忽略。
考虑一下这对于一个典型的WPF控件意味着什么。大多数控件是由多个可视化组件组成的。例如,即使你为一个按钮添加了一个非常简单的可视化树,这棵树包括一个单独的矩形框,以及一条简单的文本,目前有两个元素:文本和矩形框。不管光标是否在文本或矩形框上,这个按钮都要响应鼠标点击事件。在标准.NET事件处理模型中,这意味着要为所有元素注册MouseLeftButtonUp事件。
更严重的是使用WPF内容模型。一个按钮并不局限于只有简单文本作为标题,它可以包含任意标签。示例3-2是一个相当普通的情况,但即使如此,其中仍然有6个元素:黄色的边框,代表眼睛的两个点,代表嘴的曲线,文本,以及作为背景的按钮本身。为每一个单独元素关联事件句柄关联,是烦冗而且效率低下的。幸运的是,这些并不是必需的。
图3-2
WPF使用routed事件,该事件比其他普通事件更为直接。原先的机制是,将委托句柄关联到激发该事件的元素,调用该句柄。如今,一个rounted事件会调用所有的关联到已知代码的句柄,从初始元素向上直到用户界面书的根元素。
示例3-1显示了图3-2中按钮的标记。如果Canvas中的一个Elliipse元素接收到输入,事件路由可以支持Button、Grid、Canvas和Ellispse接收事件,如图3-3所示。
示例3-1
图3-3
一个路由事件可以是bubbling,tunneling或Direct的。Bubbling事件以寻找附属到激发事件的事件句柄开始,接着寻找它的父级别,再接着是它的父级别的父级别,依次类推,直到达到这棵树的根,这个顺序是由图3-3的数字表明的。Tunneling事件以相反的方式工作。它先在树根寻找句柄,接着向下开始工作,以原始的元素作为结束。
Direct事件的路由方式与传统的.NET事件处理相同,只有直接附属到原始元素的句柄会被通知到。这典型地用于仅在它们的原始元素的上下文中有意义的那些事件。例如,如果鼠标的进入和移开是bubbled或tunneled的,这将是无用的。父级元素未必会关心何时鼠标从一个元素移动到另一个元素。在父一级元素,你可能希望“鼠标移开”意味着“鼠标已经离开了父一级元素”,因为使用了Direct事件路由,这才是它正确地意味着什么。一旦使用bubbiling,事件将有效的意味着“鼠标已经离开了这个元素,可能在或不在其父一级内的另一个元素中”。
除direct事件之外,WPF还定义了很多成对(bubbling和tunneling)的路由事件。Tunneling事件的名称通常以Preview开始,而且会首先被激发。这将给原始元素的父级一个看一下事件的机会,在到达其子级别之前(因此以“Preview”为前缀。)tunneling的Preview事件直接遵循bubbling事件。在大多数情形中,你将要处理bubbling事件,preview事件只用于你想要阻塞一个事件时,或者你想要父一级在正常处理事件时预先做一些事情。
在示例3-1中,大多数元素拥有事件句柄,由MouseLeftButtonDown和PreviewMouseLeftButtonDown事件指定相应的bubbling和tunneling事件。示例3-2显示了相应的后台代码文件。
示例3-2
每一个句柄输出了一条debug信息。这里时我们获得的debug输出,当点击Canvas中的TextBlock时。
PreviewButtonDownButton
PreviewButtonDownGrid
PreviewButtonDownCanvas
PreviewButtonDownEllipse
ButtonDownEllipse
ButtonDownCanvas
ButtonDownGrid
ButtonDownButton
输出结果证实了Preview事件是最先被激发的。还显示了它是从Button元素开始向下工作,正如我们对tunneling事件希望的那样。bubbling事件则从Ellispse开始向上工作。
Bubbling路由事件提供了很多事件,意味着你可以注册一个单独的事件处理在一个控件上,而且它将为内嵌在控件中的任何元素接收事件。你不需要任何特殊的处理以解决内嵌内容或自定义可视化内容,事件简单的向上冒泡,并且在那里可以全部被处理。
3.2.1.1中止事件处理
有很多情形你可能不想让事件冒泡。例如,你可能希望转换事件为别的什么东西,Button元素有效的转换了MouseLeftButtonDown和MouseLeftButtonUp事件为Click事件。它抑止了底层事件,从而只有Click事件冒泡到控件之外。
任何句柄都能防止进一步的处理路由事件——通过设置RoutedEvebtArgs的Handled属性,如示例3-4所示。
示例3-3
另一个设置Handled标志的原因是,如果你想要防止正常的事件处理。一旦你在Preview句柄中这么做,不仅tunneling的Preview事件会停止,本应正常执行的bubbling事件也不会被激活,因此看起来似乎事件没有发生。
3.2.1.2确定目标
虽然在一个单独的地方,能够处理来自一组元素的事件,这是非常便利的,你的句柄可能需要知道是哪个元素引起激活一个事件,你可能想这正是句柄中sender参数的意图。事实上,sender一直将对象归诸于你附加到的事件句柄上。在使用bubbling和tunneling事件的情形中,这并不总是引起事件被激活的元素。在示例3-1中,ButtonDownWindow句柄的sneder是Window本身。
幸运的是,找到潜在的导致事件发生的元素,这是容易的。RouteEventArgs对象作为第二个参数传递,提供了一个OriginalSource属性。
3.2.1.3路由事件和正常的事件
正常的.NET事件(或者说,他们曾经称为CLR事件),提供了一个优势——相对于路由事件语法:很多.NET语言对处理CLR事件提供内嵌的支持。这就提供了最好的两种世界:你可以使用你喜欢的语言的事件处理语法,而不是利用额外的由路由事件提供的功能。
#多亏了CLR事件机制的弹性设计。虽然这里有一种标准的联合了CLR事件的简单行为,CLR的设计者有远见的意识到,一些应用程序需要更多的高级行为。这些类因此可以自由的实现它们喜欢的事件。WPF获益于这种有CLR事件定义的设计——这些事件内在的作为路由事件来实现。
示例3-1和示例3-2安排了事件句柄的连接,通过使用标记中的属性。但是我们可能已经替代地使用了正常的C#事件句柄语法来关联构造函数中的句柄。例如,我们要在示例3-1中移除MouseLeftButtonDown和PreviewMouseLeftButtonDown属性,接着修改示例3-2的构造函数,如下面的示例3-4。
示例3-4
我们还能对来自内嵌元素的事件进行同样的处理。我们不得不应用x:Name属性为了能够访问C#的元素。
后台代码经常是最好的地方来附属事件句柄。一旦你的用户界面有不寻常和有创意的可视化外观,这是一个好的时机让xaml文件有效地被图形设计器拥有。一个设计者不应该知道开发者需要处理哪些事件,或者调用那些句柄函数。因此,你将通常要设计者在xaml中给元素命名,同时开发者将要在后台代码附属句柄。
3.2.2鼠标输入
鼠标输入关注于哪个元素直接位于鼠标下。所有的用户界面元素派生于UIElement基类,这个基类定义了大量的鼠标输入事件。这些事件列于表3-1中。
表3-1
Event |
Routing |
Meaning |
---|---|---|
GotMouseCapture |
Bubble |
Element captured the mouse. |
LostMouseCapture |
Bubble |
Element lost mouse capture. |
MouseEnter |
Direct |
Mouse pointer moved into element. |
MouseLeave |
Direct |
Mouse pointer moved out of element. |
PreviewMouseLeftButtonDown, MouseLeftButtonDown |
Tunnel, Bubble |
Left mouse button pressed while cursor inside element. |
PreviewMouseLeftButtonUp, MouseLeftButtonUp |
Tunnel, Bubble |
Left mouse button released while cursor inside element. |
PreviewMouseRightButtonDown, MouseRightButtonDown |
Tunnel, Bubble |
Right mouse button pressed while cursor inside element. |
PreviewMouseRightButtonUp, MouseRightButtonUp |
Tunnel, Bubble |
Right mouse button released while cursor inside element. |
PreviewMouseMove, MouseMove |
Tunnel, Bubble |
Mouse cursor moved while cursor inside element. |
PreviewMouseWheel, MouseWheel |
Tunnel, Bubble |
Mouse wheel moved while cursor inside element. |
QueryCursor |
Bubble |
Mouse cursor shape to be determined while cursor inside element. |
注意到,上表中基本的鼠标事件设置不包括Click事件。这是因为Click一个高级别的概念——相对于基本的输入。一个按钮可以被点击——通过鼠标或键盘。此外,Click并不是必要的直接符合一个单独的鼠标事件。通常的,用户不得不点击或按下或释放鼠标,当鼠标在鼠标之上以注册一个Click事件时。相应地,这些高级别的事件由更明确的元素类型提供。Control类添加了一对事件:MouseDoubleClick和PreviewMouseDoubleClick。ButtonBase——Button的基类,CheckBox,RadioButton,都有添加这个Click事件。
#如果你使用了一个Fill属性为透明的Shape,这个Shape将担当输入的目标,一旦鼠标在Shape之上。这回有一点令人惊讶,如果你使用了一个完全透明的笔刷。这个Shape将是不可见的,但是仍然作为输入的目标,不管鼠标在其上看来可能是什么样的。如果你想要一个填充为透明的Shape,而且不捕获鼠标输入,简单的根本不提供Fill属性,如果Fill属性为null值(而不是一个完全的透明笔刷),,这个Shape将不会担当输入的模板。
记住,如果你考虑处理一个鼠标事件的原因是,简单的为用户提供某些可见的反馈,写一个事件句柄可能过度了。这通常是可能的,通过声明性的属性触发器和事件触发器,可以在样式的标签中,完全达到你需要的可视化效果。
3.2.3键盘输入
键盘输入引入了focus的概念。不同于鼠标,没法为用户移动键盘在一个元素上,从而指出输入的目标。在Windows中,一个特定的元素被指定为拥有focus,意味着它会担当键盘输入的目标。用户通过点击鼠标或Alt+Tap 在正在讨论的控件上设置focus,或者通过使用导航键如Tab和指针。
#原则上,任何用户元素可以获得焦点。IsFocused属性定义在UIElement——FrameworkElement的基类。尽管如此,Focusable属性决定了是否支持这个特征在任意特定的元素上。默认的,这个值对于控件是true;对其他元素是false。
表3-2显示了有用户界面元素提供的盘输入事件。所有的这些项使用tunnel和bubble路由,分别为Preview和主要事件。
表3-2
Event |
Routing |
Meaning |
---|---|---|
PreviewGotFocus, GotFocus |
Tunnel, Bubble |
Element received the focus. |
PreviewLostFocus, LostFocus |
Tunnel, Bubble |
Element lost the focus. |
PreviewKeyDown, KeyDown |
Tunnel, Bubble |
Key pressed. |
PreviewKeyUp, KeyUp |
Tunnel, Bubble |
Key released. |
PreviewTextInput, TextInput |
Tunnel, Bubble |
Element received text input. |
3.2.4手动输入
手写板上的铁笔以及其他支持手动输入的系统,有一套自己的事件。表3-3显示了手动输入事件——由用户界面元素提供。
表3-3
Event |
Routing |
Meaning |
---|---|---|
GotStylusCapture |
Bubble |
Element captured stylus. |
LostStylusCapture |
Bubble |
Element lost stylus capture. |
PreviewStylusDown, StylusDown |
Tunnel, Bubble |
Stylus touched screen over element. |
PreviewStylusUp, StylusUp |
Tunnel, Bubble |
Stylus left screen while over element. |
PreviewStylusEnter, StylusEnter |
Tunnel, Bubble |
Stylus moved into element. |
PreviewStylusLeave, StylusLeave |
Tunnel, Bubble |
Stylus left element. |
PreviewStylusInRange, StylusInRange |
Tunnel, Bubble |
Stylus moved close enough to screen to be detected. |
PreviewStylusOutOfRange, StylusOutOfRange |
Tunnel, Bubble |
Stylus moved out of detection range. |
PreviewStylusMove, StylusMove |
Tunnel, Bubble |
Stylus moved while over element. |
PreviewStylusInAirMove, StylusInAirMove |
Tunnel, Bubble |
Stylus moved while over element but not in contact with screen. |
PreviewStylusSystemGesture, StylusSystemGesture |
Tunnel, Bubble |
Stylus performed a gesture. |
PreviewTextInput, TextInput |
Tunnel, Bubble |
Element received text input. |
很多应用程序提供了多于一种的方式来执行确定动作。例如,考虑创建一个新文件的动作。你可以选择Fiel——New menu item,或者你可以点击相应的工具栏按钮。可选择的,你可以使用快捷键如Ctrl+N。如果应用程序提供了一个脚本系统,这个脚本还可以提供另一种执行这个动作的方式。结果是,无论你使用什么机制,都是一样的,因为这里有不同的方式调用同样的底层命令。
WPF对这个想法提供了内嵌的支持。RoutedCommand类代表了一个可以在多种方式调用的逻辑动作。在典型的WPF应用程序中,每个菜单项和工具栏按钮都联合到一个底层的RoutedCommand对象。
RoutedCommand以一种与底层输入表单非常相似的方式工作。当调用一个命令的时候,它激活了两个事件:PreviewExecuteEvent和ExecuteEvent。这些事件在这棵元素树中使用tunnel和bubble机制,和输入事件的方式相同。命令的目标是由命令的调用方式来决定。典型地,这个目标将会是当前有焦点的任何一个元素,但是RoutedCommand还提供了一个Execute的重载方法,这会传递一个明确的目标元素。
你可以从很多地方获取一个RoutedCommand。一些控件提供了命令。例如,ScrollBar控件为它的每个动作定义了命令,使之在静态字段有效,如LineUpCommand和PageDownCommand。然而,大多数命令并不是唯一对应到特定的控件。一些符合应用程序级别的动作如”新文件”或“打开”。其他动作会在控件上被调用,但是可以被一些不同的控件实现。例如,TextBox和RichTextBox都能处理剪切操作。
这里有一组提供了标准命令的类。这些类显示在表3-4中。这意味着你不需要创建自己的RoutedCommand对象来代表最普遍的操作。此外,很多命令被内嵌控件了解。例如TextBox和RichTextBox都支持很多标准的操作,包括clipboard,undo和redo命令。
表3-4
Class |
Command types |
---|---|
ApplicationCommands |
Commands common to almost all applications. Includes clipboard commands, undo and redo, and document-level operations (open, close, print, etc.). |
ComponentCommands |
Operations for moving through information such as scroll up and down, move to end, and text selection. |
EditCommands |
Text-editing commands such as bold, italic, and alignment. |
MediaCommands |
Media-playing operations such as transport (play, pause, etc.), volume control, and track selection. |
作为一个有用的命令,必须有事物对其进行响应。这个工作些微不同于处理正常的输入事件,因为大多数不是由控件定义的命令将会处理它们。表3-4中的类定义了95个命令,因此如果Control为每个截然不同的命令定义了CLR事件,那将需要190个事件——一旦还要包括preview的话。这不仅会极度不广泛,甚至还不是一个完全的解决方案。大多数应用程序在使用标准命令的同时,还定义了他们自身的自定义命令。明显的可选择性是为了RoutedCommand自身激活事件。然而,每个命令都是一个单件。例如,只有一个ApplicationCommand.New对象。如果你能直接添加一个句柄到命令对象,这个句柄会在任何时间运行。这个命令在你的应用程序任何地方被调用。如果你正想处理一个命令,当此命令在一个特定的窗口中执行的时候,会怎么样呢?
CommandBinding类解决了这些问题。一个CommandBinding对象映射了一个明确的RoutedCommand到一个句柄函数上——在一个特定的用户界面元素级别。正是这个CommandBinding会激活PreviewExecute和Execute事件,而不是UI元素。这些绑定保存在UI元素定义的CommandBinding属性。示例3-5显示了如何为一个窗体在后台代码文件中,处理ApplicationCommand.New命令。
示例3-5
这段代码依赖于命令路由的冒泡本质。顶级Window元素不同于成为命令目标的元素,当焦点通常属于某个窗体中的子元素时。然而,命令会向上冒泡到顶级。这个路由对命令的处理只放在一个地方,从而变得容易。
示例3-5处理的命令是ApplicationCommand.New。如果这组标准命令并没有满足你的应用程序的需要,你可以为明确的操作定义自定义命令。
3.2.5.2定义命令
示例3-6显示了如何定义一个命令。WPF使用对象实例来确定命令的唯一性。如果你要创建同名的第二个命令,这不会被当作同样的命令。由于这个原因,命令通常放置在静态字段或属性。
示例3-6
在示例3-6中创建的Foo命令,通过一个CommandBinding被处理,正如任何其它命令一样。当然,用户某种调用这个命令的方式。
3.2.5.3调用命令
不仅定义了一个自定义命令,示例3-6还显示了一个将命令联合到用户输入的方法。配置这个特别的命令用来被一个特殊的输入表示所调用。当前支持两种输入表示类型:MouseGesture,是一个特别的由鼠标和触笔选中的形状;KeyGesture,正如在示例3-6中使用的,是一个特别的键盘快捷键。很多内嵌控件联合了标准的表示。例如,ApplicationCommand.Copy联合了标准的键盘快捷键,用来复制(大多数地方为Ctrl+C)。
虽然一个命令在创建的时候可以联合一组表示, 在一个特别的窗体的上下文中,你可能希望为这个命令分配另外的快捷键。为了允许这样做,用户界面元素有一个InputBindings属性。这个集合包含了InputBinding对象——联合了输入表示和命令。这些增加了联合了命令的默认表示。
输入表示如快捷键,不是唯一调用命令的方式。你可以在命令上调用Execute方法从而在代码上调用它。正如示例3-7所示,Execute被重载了。如果你没有传递参数,这个命令目标将会是任何得到焦点的元素,正如通过一个输入表示调用一个命令。但是你可以传递任何你想要的目标元素。
示例3-7
你可能想,要在菜单项和工具栏按钮的Click句柄中,编写这样的代码。尽管如此,由于命令经常联合于菜单项和工具栏按钮,Button和MenuItem都支持Command属性。这就唯一标志了要调用的命令,当元素被点击的时候。这里,为命令本身,提供了一种声明式的方式,而不是为每一个绑定到命令的UI元素提供一个句柄。示例3-8显示了一个联合了标准Copy命令的Button。
示例3-8
因为这个示例使用了来自ApplicationCommands类的标准命令,我们可以使用这个语法的简写形式,只需要指出命令名称。因为命令不是定义表3-4中的类定义的,这就需要一些更详细的信息。完整的命令属性xaml语法是:
[[xmlNamePrefix:]ClassName.]EventName
如果当前只有事件名,这个事件假定为标准命令中的一个。例如,Undo是ApplicationCommands. Undo的简写。否则,你必须提供一个类的名称,以及可能一个命名空间前缀。如果你正在使用自定义命令或者某个第三方组件定义的命令,这个命名空间前缀就是需要的。与Mapping这个XML处理指令(使外部类型在xaml文件中有效)协力工作。(参见附录A获取更多Mapping处理指令的信息。)
示例3-9显示了命令名称语法的使用——所有部分都在。M:MyCommand.Foo的值意味着当前正在讨论的命令是在mylib组件的MyLib.Commands.MyCommands类中定义的,并且存储在名为Foo的字段或属性中。
示例3-9
<?Mapping ClrNamespace="MyLib.Commands" Assembly="mylib"
XmlNamespace="urn:mylib" ?>
<Window xmlns:m="urn:mylib" ...>
...
<Button Command="m:MyCommands.Foo">Custom Command</Button>
...
3.2.5.4支持命令
不仅可以被执行,命令还提供了一个QueryEnabled方法,返回了一个Boolean值表明命令是否能被立刻调用;某些命令仅在特定的上下文中有效。这个特征可以用来决定菜单或工具栏中的项是否应该变为灰色。调用QueryEnabled方法,会被以Execute同样的方式处理;CommandBinding对象用于处理这次查询。这个绑定激活一对PreviewQueryEnabled和QueryEnabled事件,这将以与PreviewExecute和Execute同样的方式进行tunnel和bubble。示例3-10显示了如何处理这个事件,为了系统定义的Redo命令。
示例3-10
#不幸的是,截止到写作时间,当前WPF的版本并不会使菜单或工具栏中的项变灰。它会激活QueryEnabled事件当一个菜单项被调用时,以及防止命令的执行,如果被disabled了,但是当前不提供任何可视化的指示,来表明一个项被disabled。我们希望这个问题会被解决在将来的版本中。
我们已经看到在WPF中控件是如何处理输入的所有可能方式。现在让我们开一下一组内嵌在WPF中的控件。
3.3内嵌控件
WPF提供了一系列内嵌控件。其中大多数符合标准的你已经熟悉的Windows控件类型。注意到没有一个是包装在旧的Win32控件外面的控件。虽然它们看上去就像是它们的副本,它们都是与生俱来的WPF控件。这意味着它们为WPF在本书中描述的功能提供了完全的支持,包括样式、独立的分辨率、数据绑定、合成、以及充分的集成支持WPF的图形化能力。
3.3.1按钮
按钮是用户可以点击的控件。点击的结果由应有程序的开发者胜任,但是共同的期望依赖于按钮的类型。例如,点击一个用来表示选择的CheckBox或RadioButton,并未正常拥有任何即时的效果来真实反映那个选择。与之对比,点击一个正常的按钮,通常会有即时的效果。
使用按钮是直接的。示例3-11显示了按钮元素的标记。
示例3-11
元素的内容(这种情形下是“Button”文字)用于按钮的标题。点击事件的句柄通过一个属性明确地指定。这表明了xaml的后台代码必须包含一个在标签中明确指定名称的方法,正如示例3-12所示(当然我们还可以附属事件句柄,通过给按钮一个x:Name,以及使用正常的C#事件句柄语法。)
示例3-12
可选择性的,一个按钮常规的属性可以被设定,在这种情形中,当按钮被点击时,指定的命令将会被调用。示例3-13显示了一个按钮调用标准ApplicationCommands.Copy命令。
示例3-13
图3-4显示了3种由WPF提供的按钮类型。这些按钮都派生于一个共同的基类,ButtonBase——这个类派生于ContentControl,意味着它们全部支持内容模型:你不受限制于为一个按钮使用简单的文本作为一个标签。
图3-4
如图3-5所示,你可以使用无论任何你喜欢的内容,虽然你仍能获取默认的按钮外观在你选择的内容周围或者旁边(如果你希望取代整个按钮的外观,而不是仅定义它的标题,你可以使用一个控件模板。参考第5章获取更多关于模板的信息。)
图3-5
虽然这些按钮派生于共同的ButtonBase基类,RadioButton和CheckBox通过ToggleButton类间接派生于这个基类。这个结基类定义了一个IsChecked属性,指出了用户是否检查了按钮。
Radio按钮正常使用于组中,其中每次只能选择一个按钮。使用RadioButtonList元素来指出一组radio按钮作为一个组,正如示例3-14所示。
示例3-14
3.3.2 Slider和ScrollBar控件
WPF提供了允许从一定范围中选取一个值的控件。它们都提供了一个类似的外观和用法:显示了一个跟踪,指定了范围,以及一个可以拖动的“thumb”——用来调整值。有两个Slider控件,HorizontalSlider和VerticalSlider,如图3-6所示。有两个ScrollBar控件,HorizontalScrollBar和VerticalScrollBar,如图3-7所示。主要的不同是一个约定而不是功能,ScrollBar通常用于与某些滚动的可视化区域协力工作;而slider是用来调整值的。
图3-6
图3-7
Slider和ScrollBar在使用上是非常类似的。它们都派生于一个共同的基类RangeBase。这个类提供了Minimum和Maxmum属性——定义了一定范围的由控件表示的值;提供了Value属性保持当前选定的值;还定义了SmallChange和LargeiChange属性——由Value改变的多少来决定,当使用方向键调整的时候,或者是相应的PageUp和PageDown键。LargeChange值还用于当slider的一部分track在thumb被点击的任何一边。
Slider控件有一个固定大小的thumb,而ScrollBar上的thumb可以在大小上改变。如果slider用于联合一个可滚动的视图,thumb的大小——相当于track,与可视化区域的大小——相对于全部可滚动区域,是成比例的。例如,thumb是大约scrollbar的长度和宽度的1/3,这就指定了1/3可滚动区域在当前视图中。
你可以通过ViewPortSize属性控制scrollbar的thumb大小。无论何处这可以是从0到Maximum属性值。如果ViewPortSize与Maximum相同,thumb将会填充track,并且不能移动。ViewPortSize越小,thumb也会越小。
如果你想提供一个可滚动的视图——一个更大的用户区域,你可以非常规的直接使用scrollbar控件。这通常更容易使用ScrollViewer控件。
一个ScrollViewer元素有一个单独的子元素。示例3-15使用了Ellipse元素,但是它可能是任何事情。如果你想放置多个元素在一个scrollable视图中,你可以嵌入它们在一个面板中(在第2章讨论)
示例3-15
如果ScrollViewer的内容大于可利用的空间,ScrollViewer会提供滚动条允许用户滚动内容,如示例3-8所示。默认的,ScrollViewer提供了一个垂直滚动条,并不是一个水平滚动条。在示例3-15中,HorizontalScrollBarVisiablity属性设置为Auto,指定了水平滚动条在需要的时候添加上去。
图3-8
这个Auto可见性——我们为水平滚动条所选择的,不同于默认的垂直的行为。VerticalScrollBarVisiablity默认为Visible,意味着这个滚动条总是存在无论是否需要它。
有两种确保滚动条不显示的方式。你可以设置它的可见性为Disabled(默认的水平滚动条)或者Hidden。二者的区别是,Disabled约束了ScrollViewer内容的逻辑上的大小与可利用的空间一样。Hidden允许这个逻辑上的大小是不受约束的,即使用户无法在额外的空间滚动。这可以改变确定的外观样式的行为。
为了检查这些设置如是如何影响ScrollViewer的行为,我们看一下示例3-16发生了什么,当我们改变ScrollViewer属性时。
示例3-16
这个示例显示了一个Grid,包含了3个Button元素在一行中。如果Grid获得更多它需要的空间,它将伸展按钮变得比必需的更宽。如果没有充分的空间,将会裁剪按钮。如果它被取代在ScrollBarViewer中,这将可能为ScrollViewer提供更充分的实质上的、可滚动的空间,即使屏幕上的空间是不充分的。
图3-9显示了示例3-16中的Grid是如何显示在一个ScrollViewer中,当这里有更多充分的空间时。显示了HorizontalScrollBarVisiablity所有的四种选项,在这四种情形中,按钮被伸展了来填充空间。
图3-9
图3-10
图3-10显示了相同的四个排列,但是带有不充分的水平空间。顶部的2个ScrollViewer元素支持水平滚动,带有各自的Visible和Auto属性。正如你希望的,ScrollViewer提供了充分的空间来容纳所有的内容和允许用户在视图中的裁剪部分内滚动。在左下位置,水平的滚动条被设置为Hidden,外观行为是一样的。它排列了元素,好像有充分的空间来容纳所有的内容。唯一的不同是它并没有显示一个滚动条。在右下位置,我们可以看到,Disabled是不同于行为结果的。这里,不仅不显示滚动条,而且水平滚动条完全被Dsiabled了。Grid因此被强制裁剪按钮来适合可利用的空间。
3.3.3文本控件
WPF提供了编辑和显示文本的控件。最简单的文本编辑控件是TextBox。默认的,它允许编辑文本的单独一行;但是通过设置AcceptReturn为true,它可以编辑多行。它提供了标准的基本文本编辑机制:支持选择,系统剪切板集成(剪切、粘贴等),以及支持多重级别的Undo。
示例3-17显示了2中TextBox元素,一个带有默认的设置;另一个是多行的模式。它还显示了类似的PasswordBox,设计用来输入密码。图3-11显示了这些结果。正如你看到的,PasswordBox中的文本被显示为一行星号。这是惯例。为了防止密码被任何人可以在屏幕上看到。PasswordBox还拒绝复制到剪切板内容的能力。
示例3-17
图3-11
TextBox和PasswordBox只支持普通的文本。它们不支持任意种类的内嵌内容——试图嵌入任何而不是普通的文本——会引起一个运行期错误。这使得它们易于在输入和编辑简单数据时使用。TextBox提供了一个Text属性,代表了一个控件的内容,作为一个字符串。
#PasswordBox没有Text属性。取代的,它有一个Password属性。它返回一个SecureString类型,而不是返回一个String。它仍然只提供普通的文本值。然而,它还提供了两种机制来防止意外的泄漏密码数据。
首先,它以加密的形式储存字符串。这意味着包含字符串的内存应该被分发到系统分页文件中,它的内容是不可读取的。(.NET在运行时生成了一个随机的密钥,将它存储在一个内存位置中,这个位置是被锁定的,用来防止被修改到分页文件中)。如果使用一个正常的字符串,攻击者能够获取它的内容,通过制作一个你的系统交换文件的副本。各种各样的.NET安全API可以传递一个SecureString,意味着你的代码从不用处理解密的版本。
其次,你可以在Secure上调用Dispose,这个方法会复写密码数据。这意味着即使以其加密的形式,敏感性数据也会按需求被清除。带有一个正常的String,数据能长时间保存在内存中,直到你已经完成了使用,只会被销毁于垃圾收集器开始执行时。
意识到每当你读取Password数据的时候,都会返回一个新的SecureString,因此如果你打算利用按需清除的行为,你必须Dispose在你每次读取这个属性的时候。PasswordBox维护着它内部的字符串复制,当控件被销毁时,将字符串安排给你。
普通文本的简单的是很好的,如果你仅仅需要普通文本作为输入。然而,这样做有时是有用的——允许更加多样化的输入。WPF因此提供了RichTextBox。
RichTextBox非常有弹性的,可以包括几乎任意的内容。示例3-18显示了RichTextBox的标记,包含了文本、图形、控件的混合。结果在图3-12显示。
示例3-18
图3-12
对待非文本化的元素和对待文本的字符有同样的方式,你可以在它们的前后插入文本,或者使用剪切和粘贴来移动文本。Windows没有为用户定义一个标准的方法,来使用键盘“输入”一个椭圆,因此虽然我们能在标签中预载带有这些元素的RichTextBox,并没有为用户提供一个添加这类元素的方式。然而,写一个提供了如此便捷的程序,是相当容易的。
示例3-19显示了应用程序使用RichTextBox所用的标签。在屏幕的顶级,它提供了一个包括控件的面板,该控件允许用户添加椭圆和矩形。如图3-13所示。
图3-13
示例3-19
添加了这些元素的代码是相当直接的。示例3-20是它的后台代码文件。这些代码创建了一个Shape;设置了它的Width、Height、Fill和Stroke属性,以及将这个Shape添加到RichTextBox内容中。RichTextBox提供了一个TextSelection熟悉,指出了当前选择或没有选择的点。这段代码简单地在开始区域插入了这个Shape。
示例3-20
3.3.4标签
在前面的章节,示例3-19使用了Label控件。它典型地应用于为控件提供一个标题,该控件没有其自身的内嵌标题。最普通的示例是TextBox。Label看起来可能是多余的,由于达到了同样的可视化效果而不用一个完整的控件;你可以只使用底层的TextBlock元素。然而,Label有一个重要的处理焦点的职责。
设计良好的用户界面应该是易于键盘使用的。一种通常的达到目的的方式是提供访问键。访问键是一个关联到控件的字母,从而一旦你按住了Alt键,同时按住访问键,这个行为与你点击当前控件是一样的:当按下Alt键的时候,通过在控件的标题中给相应的字母加下划线,应用程序把它们的访问键告诉用户。(你也可以配置Windows来一直显示访问键的下划线,无论是否按下Alt键)。示例3-19中使用了内部的AccessKey元素,为我们管理这个下划线。
图3-14显示了示例3-9中用户界面一部分的特写,显示了访问键下划线,正如用户会看到的,如果当他按下Alt键的时候。只是一个Button,并不需要特殊的代码,就能使访问键工作。所有你需要做的是,使用AccessKey内嵌的修改器,在控件的标记之中。在这种情形中,使用Alt+R或Alt+E将自动的得到相同的效果,如同点击矩形的或椭圆形的按钮。
图3-14
TextBox面临着比较多的挑战。他们有一个标题,只有显示文本而且这些文本还是可以编辑的。这个标题由一个独立的元素提供在TextBox的左边——这里引进了一个Label。Label控件的意图是为了提供放置标题的地方,同时带有AccessKey元素。当访问键按下的时候,这个Label将会重定向输入到相关的控件,这里是一个TextBox。
Label是如何知道重定向访问键到哪一个控件的呢?Label有一个Target属性,指出了访问键的已有目标。我们使用绑定表达式来连接label到它的目标。绑定表达式细节将在第4章讨论。这里的表达式简单地设置了Target属性到相关的命名元素。
3.3.5 Selectors
一些类型的控件运行用户从一组项目中进行选择。在某些情形中,如RadioButtonList和TabControl,经常只有一个选择项。ComboBox扩展了这个功能——没有当前的选项。ListBox 则更加进一步的,具有同时选择多个选项的能力。所有的这些控件从Selector基类继承了共同的列表和选择功能。
最简单的使用这些控件的方式是将内容添加到它们的Item属性。示例3-21显示了ComboBox标签,各种元素被添加到它的Item中。
示例3-21
同样的技术还可以使用在ListBox、TabControl和RedioButtonList中,正如你在图3-15中看到的,每一个控件以它们自己的方式代表项。RadioButtonList为每个项生成一个RadioButton,使用这些项作为标题。TabControl把每个元素放进它自己的TabControl中,为了以其自己的tab页面来表示。(图3-15只显示了第一项,但是另外联合的三个贯穿3个tab头)。
图3-15
所有的Selector控件包装了我们所有的项,为了以一种合适的方式表示它们。虽然这有可能是便利的,在某种情形中,你可能想要更多的控件。例如,显示在上面的TabControl控件,并不是特别的有用。它包装了我们的项,带着没有标题的tab。为了修复这个问题,我们简单地提供了我们自己的TabItem控件,取代以让TabControl元素为我们生这些项。我们可以接着设置Header属性从而控制tab页的标题。这些技术在示例3-22中说明。
示例3-22
示例3-22显示了TabControl标签,带有和前面相同的三个选项,但是这次是显示的指定TabItem元素。在其中的第一项,Header属性被设置为文本“button”。另外两项解释了这些头部支持内嵌内容。第一项使用TextBlock控制文本外观;第二项把一个Ellipse放入头部,取代了文本。图3-16显示了这个结果。
图3-16
提供一组固定的元素贯穿Item属性,对tab页和RadioButton来说是有意义的,在你设计用户界面的时候,你想知道需要什么元素。但是这可能不适合ComboBox和列表。为了支持你在运行时决定要显示哪一个选项,所有Selector控件提供了一个折中的方法来导入列表:数据绑定。替代以使用Items,你可以提供一个带有ItemSource属性的数据源对象,以及使用数据样式来决定元素如何显示。这些技术将在第4章和第5章介绍。
不管你是否使用了一组项或一个绑带数据源,你总是可以查找出选中的元素什么时候有改动——通过处理SelectionChanged事件。你可以接着使用SelectedIndex或者SelectedItem属性来查找当前选中了哪一个元素。
3.3.6菜单
很多Windows应用程序通过层级菜单,提供了访问他们功能的方法。这些典型的表现为顶级窗口的主菜单和弹出式“上下文”菜单。WPF提供了两种菜单控件:Menu是永久性可见的菜单(如主菜单);ContextMenu是上下文菜单。
#如今Windows中的菜单被典型的视为不同于其他用户界面的元素。这是一个特殊的句柄类型。在Windows窗体中,大多数可见的元素派生于Control基类,但菜单不是。这意味着菜单趋向于有点非弹性的。为了避免它的缺点,一些用户界面工具选择不用这些Windows内嵌的简单菜单处理。在WPF中,菜单只是一个普通的控件,因此它们不需要特殊的样式和约束。
这两种类型的菜单都以同样的方式生成。它们的内容组成了层级的MenuItem元素。示例3-23创建了一个典型地菜单。结果如图3-17所示。
图3-17
示例3-23
上下文菜单使用于一种非常简单的方式。主要的不同是外观。Menu在顶部有一个水平条,事实上一个Menu可以在UI的任何地方使用,而ContextMenu只能用作一个元素ContextMenu属性值。示例3-24显示了一个带有ContextMenu的Grid元素。
示例3-24
带有这个在适当位置的上下文菜单,在Grid上的任何地方右击都会产生一个上下文菜单。图3-18显示了这个在活动中的上下文菜单。
图3-18
每个MenuItem都有一个Header属性。作为Menu的子一级,这个header决定了显示在菜单条上的label。为了一个MenuItem——内嵌在ContextMenu或另一个MenuItem中,Header包括了菜单线的内容。这个Header熟悉允许普通文本,如示例3-23,或者内嵌内容。示例3-25显示了一个菜单项目的修改后版本,暴露了添加结构的能力,从而标记了一个字母作为访问键。图3-19显示了结果。
示例3-25
图3-19
示例3-23的菜单并没有做任何有意义的事情,因为没有明确的事件句柄或命令。有两种方式使你可以钩住MenuItem到一些代码。你可以处理它的Click事件——就像处理一个按钮的Click事件。折中地,你可以在MenuItem上设置Command属性,使用相同的语法——如我们在示例3-8所见。
示例3-26显示了一个Edit子菜单修改后的版本,菜单项都联合到相关的标准命令上。只要焦点落在一个控件上,如TetBox或RichTextBox——理解这些标准命令;这些标准命令将会被处理而无需任何显示的代码。(如果焦点不在这样的控件上,这些命令会简单地向上冒泡。如果没有处理这些命令的事物,就会忽略它们。)
示例3-26
菜单经常还有一个快捷键以作为一个加速器。加速器只在菜单打开的时候工作。一个快捷键,如Ctrl+S用于保存,无论菜单是否打开,都会工作。这个菜单不负责绑定控件快捷键到键-表示,正如我们先前看到的。我们这么做使用了CommandBindings,这就联合了命令输入。然而,菜单按照惯例地展示了快捷键,从而有助于用户发现它们。
一旦一个菜单的Command有一个联合的键-表示的绑定,WPF会自动地在菜单中展示这个快捷键。由于示例33-26使用了标准的剪切板和undo/redo命令,其产生的菜单就有联合的快捷键,正如你在图3-20所看到的。
图3-20
一旦由于某些原因,你选择不使用WPF的基本命令系统,你仍然可以展示一个快捷键。MenuItem提供了一个InputGestureText属性,让你选择要出现的文本在正常的位置,作为这样一个快捷键。示例3-27显示了一个菜单项同时具有快捷键和访问键。
示例3-27
注意到,Menu和ContextMenu都间接派生于ItemControl,同一个基类作为所有的Selector控件。这意味着你可以使用ItemDataSource属性来导入使用了数据绑定的菜单,而不是使用固定的内容。这将是有用的——如果你想要制作自己的可配置菜单结构。参见第4章获取更多如何使用数据绑定的细节。
3.3.7 工具栏
大多数Windows应用程序在提供菜单的同时,还提供了工具栏。工具栏为频繁使用的操作提供了更快速的访问,因为用户不需要通过菜单系统进行导航,而工具栏在屏幕上一直是可见的,如示例3-21所示。
图3-21
WPF通过ToobarTray和ToolBar支持工具栏。ToobarTray提供了一个容器,你可以向其中添加多个TooBar元素。示例3-28显示了一个简单的例子,这为图3-21提供了标记。
示例3-28
这只创建了一个工具栏,带有一对按钮。当你添加一个按钮到工具栏的时候,它的大小默认为17X17合理的像素。如果内容太大,就会被裁减掉。这是因为工具栏按钮通常被希望为图片而不是文本。工具栏设置所有的按钮为同样的大小,以确保一致的间隔空间。如果你提供的内容比17X17小,这会被居中设置。(如果你想要一个更大的按钮,你可以显示地设置大小;17X17是简单的默认值。)
#在这个例子里,我们使用了一些简单的向量图,用来绘制通常的图标如New和Open等等。这里使用的图形化元素在第7章中更详细的描述。实际上,你可以很少地放置像这样的内嵌的图形。通常你可以希望图画是简单引用到的资源,通过工具栏上的按钮。看第6章获取更多细节。
由于工具栏上的按钮只是正常的Button元素,带有指定的可视化,关于它们的行为,没有什么尤其特殊之处。工具栏很少只提供一个特定的方式排列和呈现控件。你也可以添加其他元素到工具栏,如TextBox或ComboBox。这些将连同按钮一起排列在工具栏上。
3.4 我们进行到哪里了?
控件是由应用程序创建的块。它们描述了用户用来交互的界面特征。控件提供了行为,依赖样式和模板来表示一个外观。输入由事件和命令处理,这使用了一个路由系统来允许简单的、一致的事件处理,而不顾用户界面的详细结构可视化可能是多么复杂。WPF提供了一组内嵌控件,基于普遍使用于Windows应用程序中的控件。