内容控件(content control)是更特殊的控件类型,它们可包含并显示一块内容。从技术角度看,内容控件时可以包含单个嵌套元素的控件。与布局容器不同的是,内容控件只能包含一个子元素,而布局容器主要愿意可以包含任意多个牵头元素。
正如前面所介绍,所有WPF布局容器都继承自抽象类Panel,该类提供了对包含多个元素的支持。类似地,所有内容控件都继承自抽象类ContentControl。下图显示了ContentControl类的层次结构。
图 ContentControl类的层次结构
如上图所示,几个常见的控件实际上都是内容控件,包括Label控件以及ToolTip控件。此外,所有类型的按钮都是内容控件,包括众所周知的Button控件、RadioButton控件以及CheckBox控件。还有几个更特殊的内容控件,如ScrollViewer控件(可使用该控件创建能够滚动的面板)和UserControl类(该类允许重用一组自定义控件)。用于在应用程序中表示每个窗口的Window类本身也是内容控件。
最后,还有继承自HeaderedContentControl类的内容控件子集。这些控件同时具有内容区域和标题区域两部分,标题区域用于显示一些标题。这些控件包括GroupBox控件、TabItem控件(位于TabControl控件中的一页)以及Expander控件。
一、Content属性
与Panel类提供Children集合来保存嵌套的元素不同,Control类添加了Content属性,该属性只接受单一对象。Content属性支持任何类型的对象,但可将该属性支持的对象分为两大类,针对每一类进行不同的处理:
- 未继承自UIElement类的对象:内容控件调用这些控件的ToString()方法获取文本,然后显示该文本
- 继承自UIElement类的对象:这些对象(包括所有可视化元素,它们是WPF的组成部分)使用UIElement.OnRender()方法在内容控件的内部进行显示。
为理解Content属性的工作原理,考虑简单的按钮。到目前位置,所看到的所有包含按钮的示例都简单地提供了一个字符串:
<Button Margin="3">Text Button</Button>
该字符串被设置为按钮的内容,并在按钮上显示该内容。然而,可通过在按钮上放置任何其他元素来获取更有趣的内容。例如,可使用Image类在按钮上放置一幅图像。
<Button Margin="3"> <Image Source="happyface.jpg" Stretch="None"</Image> </Button>
还可在布局容器(如StackPanel面板)中组合文本和图像:
<Button Margin="3"> <StackPanel> <TextBlock Margin="3">Image and Text Button</TextBlock> <Image Source="happyface.jpg" Stretch="None"></Image> <TextBlock Margin="3">Courtesy of the StackPanel</TextBlock> </StackPanel> </Button>
如果希望创建一个真正意义上的极具特色的按钮,甚至可在该按钮中放置其他内容控件,如文本框和按钮(还可以在这些元素内部继续嵌套元素)。
<Button Margin="3" Padding="3" HorizontalContentAlignment="Stretch"> <StackPanel> <TextBlock Margin="3">Type something here:</TextBlock> <TextBox Margin="3" HorizontalAlignment="Stretch">Text box in a button</TextBox> </StackPanel> </Button>
上面的XAML显示如下所示:
这与窗口使用的内容模型相同。与Button类相似,Window类也只能包含单一嵌套元素,可以是一块文本、一个任意对象或一个元素。
除Content属性外,ContentControl类没有添加多少其他属性。它包含HasContent属性,如果在控件中有内容,该属性返回true。还有ContentTemplate属性,通过该属性可以创建一个模板,用于高速控件如何显示他无法识别的对象。使用ContentTemplate模板,可更加只能地显示非继承自UIElement的对象。不是仅调用ToString()方法获取字符串,而是可以使用各种属性值,将它们布置到更复杂的标记中。
二、对齐方式
在前面介绍布局中,在FrameworkElement基类中定义的HorizontalAlignment和VerticalAlignment属性,在容器中对齐不同的控件。然而,一旦控件包含了内容,就需要考虑另一个组织级别。需要决定内容控件中额内容如何和边框对齐,这是通过使用HorizontalContentAlignment和VerticalContentAlignment属性实现的。
HorizontalContentAlignment和VerticalContentAlignment属性与HorizontalAlignment和VerticalAlignment属性支持相同的值。这意味着可将内容对象到控件的任意边缘(使用Top、Bottom、Left或Right值),可以居中(使用Center值),也可以拉伸内容使其充满可用空间(使用Stretch值)。这些设置直接应用于嵌套的内容元素,但你可以使用多层嵌套创建复杂布局。例如,如果在Label元素中嵌套StackPanel面板,Label.HorizontalContentAlignment属性决定了StackPanel面板被放置在Label控件中的何处,但StackPanel面板及其子艺元素的对齐方式和尺寸选项则会决定其余的布局。
在布局章节中,学习了Margin属性,通过该属性可在相邻元素之间添加空间。内容控件使用和Margin属性互补的Padding属性,该属性在控件边缘和内容边缘之间插入空间。比较下面两个按钮,观察他们之间的区别:
<Button>Absolutely No Padding</Button> <Button Padding="3">Well Padded</Button>
对于没有内边距(padding)的按钮(默认),其文本和按钮边缘拥挤到一起。每条边都具有3个单位内边距的按钮则具有更合理的空白空间。如下图所示:
三、WPF内容原则
现在,可能会怀疑WPF内容模型设计得这么复杂是否值得。毕竟,可选择在按钮上放置一幅图像,但是未必需要将图像嵌入到其他控件或整个布局面板中。然而,有几个非常重要的原因促进了观念转变。
在前面示例中,包含一个简单的图像按钮,在Button控件中放置了一个Image元素。这种方法不是非常理想,因为位图不是分辨率无关的。在高DPI显示器上,位图显示可能会变模糊,因为WPF必须通过插值添加更多的像素,以确保图像保持正确的大小。更完善的WPF节目应避免使用位图,而应当使用矢量图像的组合来创建自定义绘图按钮以及其他图像修饰。
这种方式可与内容控件模型很好地集成在一起。因为Button类是内容控件,所以可以*地使用一幅固定的位图对其进行填充——相反,可以包含其他内容。例如,可使用System.Windows.Shapes名称空间中的类,在按钮中绘制一幅矢量图像。下面的示例创建了一个具有两个菱形的按钮。
<Button Margin="3"> <Grid> <Polygon Points="100,25 125,0 200,25 125,50" Fill="LightSteelBlue"></Polygon> <Polygon Points="100,25 75,0 0,25 75,50" Fill="White"></Polygon> </Grid> </Button>
效果图如下所示:
显然,在这个示例中使用嵌套的内容模型比为Button类添加额外的属性以支持不同类型的内容更简单。嵌套内容典型不仅更灵活,还允许Button类提供更简单的接口。因为所有内容控件都支持以相同的方式嵌套内容,所以不必为多个类添加不同的内容属性。
实际上,使用嵌套内容模型需要进行折中。它简化了元素的类模型,因为不需要使用额外的继承层次,以便为支持不同类型的内容添加属性。然而,需要使用稍复杂的对象模型——元素可以由其他嵌套的元素构成。
四、标签
在所有内容控件中,最简单的是Label控件。与其他任意内容控件类似,Label控件接受希望放入其中的单一内容。但不同的是Label控件支持记忆符(mnemonics)——本质上,记忆符是能够为链接的控件设置焦点的快捷键。
为支持此功能,Label控件添加了Target属性。为了设置Target属性,需要使用指向另一个控件的绑定表达式。下面是必须遵循的语法:
<StackPanel Margin="5"> <Label Target="{Binding ElementName=txtA}">Choose _A</Label> <TextBox Name="txtA"></TextBox> <Label Target="{Binding ElementName=txtB}">Choose _B</Label> <TextBox Name="txtB"></TextBox> </StackPanel>
标签文本中的下划线指示快捷键(如果确实需要在标签中显示下划线,必须添加两个下划线)。所有记忆符都使用Alt键和已经确定的快捷键工作。例如在该例中,如果用户按下了Alt+A组合键,第一个标签会将焦点传递给链接的控件,即txtA。同样,如果按下了Alt+B组合键,会将焦点传递给txtB文本框。
快捷键字符通常是隐藏的,直到用户按下了Alt键,这时它们才显示为具有下划线的字母。但是这一行为取决于系统设置。
五、按钮
WPF提供了三种类型的按钮控件:熟悉的Button控件、CheckBox控件和RadioButton控件。所有这些控件都是继承自ButtonBase类的内容控件。
ButtonBase类增加了几个成员。定义了Click事件并添加了对命令的支持,从而允许为更高层的应用程序任务触发按钮。最后,ButtonBase类添加了ClickMode属性,该属性决定何时引发Click事件以响应鼠标动作。默认值时ClickMode.Release,这意味着当单击和释放鼠标键时引发Click事件。然而,也可选择当鼠标第一次按下时引发Click事件(ClickMode.Press)。更奇特的是,只要将鼠标移动到按钮上并在按钮上悬停一会儿旧会引发Click事件(ClickMode.Hover)。
1、Button控件
Button类表示一直使用的Windows下压按钮。它添加了两个可些属性:IsCancel和IsDefault。
如果将IsCancel属性设置为true,按钮就成为窗口的取消按钮。在当前窗口的任何位置如果按下Esc键,就会触发该按钮。
如果将IsDefault属性设置为true,按钮就成为默认按钮(也就是接受按钮)。其行为取决于焦点在窗口中的当前位置。如果焦点位于某个非按钮控件上(如TextBox控件、RadioButton控件和CheckBox控件等),默认按钮具有蓝色阴影,几乎像是具有焦点。如果按下Enter键,就会触发默认按钮。但如果焦点位于另一个按钮控件上,当前有焦点的按钮即具有蓝色阴影,而且按下Enter键会触发当前按钮而不是默认按钮。
许多用户依赖与这些快捷方式(特别是使用Esc键关闭不需要的对话框),所以花一些时间在创建的每个窗口中定义这些细节是有意义的。仍需为取消按钮和默认按钮编写事件处理代码,因为WPF没有提供这一行为。
某些情况下,将窗口中的同一个按钮既设置为取消按钮,又设置为默认按钮也是有意义的。一个例子是About对话框中的OK按钮。不过,窗口中只能有一个取消按钮和一个默认按钮。如果指定多个取消按钮,按下Esc键将把焦点移到下一个默认按钮,而不是触发它。如果设置多个默认按钮,按下Enter键后的行为更混乱。如果焦点在某个非按钮控件上,按下Enter键会把焦点移到下一个默认按钮。如果焦点位于一个Button控件上,按下Enter键就会触发该Button控件。
2、ToggleButton控件和RepeatButton控件
除Button类之外,还有三个类继承自ButtonBase类。这些类包括:
- GridViewColumnHeader类,当使用基于网络的ListView控件是,该类表示一例可以单击的标题。
- RepeatButton类,只要按钮保持按下状态,该类就不断地触发Click事件。对于普通按钮,用户每次单击只触发一个Click事件。
- ToggleButton类,该类表示具有两个状态(按下状态和未按下状态)的按钮。当单击ToggleButton按钮时,它会保持按下状态,直到再次单击该按钮以释放它为止。这有时称为“粘贴单击”(sticky click)行为。
RepeatButton和ToggleButton类都是在System.Windows.Controls.Primitives名称空间中定义的,这表明他们通常不单独使用。相反,它们通常通过组合来构建更复杂的控件,或通过继承扩展其功能。例如,RepeatButton类常用语构建高级的ScrollBar控件(最终,甚至ScrollBar控件都是高级的ScrollViewer控件的一部分)。RepeatButton类使滚动条两端的箭头按钮具有它们所特有的行为——只要按住箭头按钮不释放就会一直滚动。类似地,ToggleButton控件通常用于派生出更有用的CheckBox类和RadioButton类,后面将介绍这两个类。
然而,RepeatButton类和ToggleButton类都不是抽象类,所以可在用户界面中直接使用他们。ToggleButton控件在工具栏中非常有用,后面介绍工具栏的时候进行详细介绍。
3、CheckBox控件
CheckBox控件和RadioButton控件是不同类型的按钮。它们继承自ToggleButton类,这意味着用户可切换它们的开关状态,即他们的“开关”行为。对于CheckBox控件,切换到控件的“开”状态,意味着在其中放置复选标记。
CheckBox类没有添加任何成员,所以CheckBox类的基本接口是在ToggleButton类中定义的。最重要的是,ToggleButton类添加了IsChecked属性。IsChecked属性是可空的Boolean类型,这意味着该属性可以设置为true、false或null。显然,true表示选中的复选框,而false表示空的复选框。null值使用起来较为棘手——表示不确定状态,显示为具有阴影的复选框。不确定状态通常用于表示尚未设置的值。或存在一些差异的区域。例如,在文本应用程序中通常有用于加粗文本字体的复选框,并且如果当前选择的文本既包含粗体文本又包含正常文本,这时可将复选框设置为null,表示一种不确定状态。
为在WPF标记中指定null值,需要使用null标记扩展,如下所示:
<CheckBox IsChecked="{x:null}">A check box in indeterminate state</CheckBox>
除了IsChecked属性外,ToggleButton类还添加了IsThreeState属性,该属性决定了用户是否能将复选框设置为不确定状态。如果IsThreeState属性被设置为false(默认值),单击复选框时,其状态会在选中和未选中两种状态之间切换,并且这时只能通过代码将复选框设置为不确定状态。如果IsThreeState属性被设置为true,单击复选框时,就会在所有可能的三种状态之间循环切换。
ToggleButton类还定义了当复选框进入特定状态时会触发的三个事件:Checked、UnChecked和Indeterminate。大多数情况下,可以很容易地通过处理继承自ButtonBase类的Click事件,将这一逻辑合并为单个事件处理程序。无论何时改变按钮的状态都会触发Click事件。
4、RadioButton控件
RadioButton类也继承自ToggleButton类,并使用相同的IsChecked属性和相同的Checked、Unchecked以及Indeterminate事件。此外,RadioButton类还增加了GroupName属性,该属性用于控制如何对单选按钮进行分组。
单选按钮通常由他们的容器进行分组。这意味着,如果在StackPanel面板中放置三个单选按钮,那么这三个单选按钮就形成了一个组,而且只要选择这三个单选按钮中的一个。另一方面,如果在两个独立的StackPanel控件中放置一组单选按钮,就有了两组相互独立的单选按钮。
可以使用GroupName属性覆盖这一默认行为。可使用该属性在同一个容器中创建多个组,或将包含在多个容器中的单选按钮创建为一组。对于这两种情况,技巧很简单——只需为所有属于同一组的单选按钮提供相同的组名即可。
分析下面的这个示例:
<StackPanel> <GroupBox Margin="5"> <StackPanel> <RadioButton>Group 1</RadioButton> <RadioButton>Group 1</RadioButton> <RadioButton>Group 1</RadioButton> <RadioButton Margin="0,10,0,0" GroupName="Group2">Group 1</RadioButton> </StackPanel> </GroupBox> <GroupBox Margin="5"> <StackPanel> <RadioButton>Group 3</RadioButton> <RadioButton>Group 3</RadioButton> <RadioButton>Group 3</RadioButton> <RadioButton Margin="0,10,0,0" GroupName="Group2">Group 3</RadioButton> </StackPanel> </GroupBox> </StackPanel>
这个示例中有两个包含单选按钮的容器,但有三组单选按钮。每个分组框底部的最后一个单选按钮属于第三组。这个示例中的设计有些令人困惑,但有些情况下,可能希望以微妙的方式从包中分离出一个特定的单选按钮,而又不会导致该按钮离开原来的分组。
六、工具提示
WPF为工具提示(当在一些感兴趣的内容上悬停鼠标时,就会弹出的那些臭名昭著的黄色方框)提供了一个灵活模型。因为在WPF中工具栏提示的是内容控件,所以可在工具提示中放置任何可视化元素。还可改变各种时间设置来控制工具提示的显示和隐藏速度。
直接使用ToolTip类不是显示工具提示的最简单方式。相反,可为元素简单地设置ToolTip属性。ToolTip属性是在FrameworkElement类中定义的,所以所有能放到WPF窗口上的元素都可以使用该元素。
例如,下面的按钮具有基本的工具提示:
<Button ToolTip="This is my tooltip" >I have a tooltip</Button>
当在该按钮上悬停鼠标时,就会在熟悉的黄色方框中显示“This is my tooltip "文本。
如果希望提供更复杂的工具提示内容,如组合的嵌套元素,就需要将ToolTip属性分为单独的元素。下面的示例使用更复杂的嵌套内容设置按钮的ToolTip属性:
<Button > <Button.ToolTip> <ToolTip > <StackPanel> <TextBlock Margin="3" >Image and text</TextBlock> <Image Source="happyface.jpg" Stretch="None" /> <TextBlock Margin="3" >Image and text</TextBlock> </StackPanel> </ToolTip> </Button.ToolTip> <Button.Content>I have a fancy tooltip</Button.Content> </Button>
1、设置ToolTip对象的属性
ToolTip是内容控件,因此可调整它的标准属性,如Background属性(从而不再是黄色的方框)、Padding属性以及Font属性。还可以修改在ToolTip类中蒂尼工艺的成员(如下表所示)。这些属性中的大部分都用于帮助将工具提示放到所期望的位置。
下面的标记使用ToolTip属性创建了一个工具提示,该工具提示窗口没有阴影效果,但使用了透明的红色背景,从而可透过该工具提示看到底层的窗口和控件:
<Button > <Button.ToolTip> <ToolTip Background="#60AA4030" Foreground="White" HasDropShadow="False" > <StackPanel> <TextBlock Margin="3" >Image and text</TextBlock> <Image Source="happyface.jpg" Stretch="None" /> <TextBlock Margin="3" >Image and text</TextBlock> </StackPanel> </ToolTip> </Button.ToolTip> <Button.Content>I have a fancy tooltip</Button.Content> </Button>
大多数情况下,使用标准的工具提示位置便足以满足要求了,这是工具提示窗口位于当前鼠标位置。然而,ToolTip的各种属性提供了更多的选择。下面列出一些可用于放置工具提示的策略:
- 根据鼠标的当前位置。这是标准的行为,该行为依赖于将Placement属性设置为Mouse。工具提示框的左上角和包围鼠标指针的不可见”边界框“的左下角对齐。
- 根据悬停鼠标的元素的位置。根据希望使用的元素边缘,将Placement属性设置为Left、Right、Top、Bottom或Center。工具提示框的左上角与边缘对齐。
- 根据另一个元素(或窗口)的位置。使用将工具提示和当前元素对齐的相同方式设置Placement属性(使用Left、Right、Top、Bottom或Center值)。然后通过设置PlacementTarget属性选择元素。请记住,需要使用”{绑定元素名=名称}“语法来确定想要使用的元素。
- 使用偏移。使用上述任意一种策略,并何止HorizontalOffset和VerticalOffset属性来添加一定的额外空间。
- 使用绝对坐标。将Placement属性设置为Absolute,并使用HorizontalOffset和VerticalOffset属性(或使用PlacementRectangle属性)在工具提示和窗口左上角之间设置一些空间。
- 使用运行时的计算结果。将Placement属性设置为Custom。设置CustomPopupPlacementCallback属性指向一个已经创建的方法。
2、设置ToolTipService属性
有有几个工具提示属性不能通过ToolTip类的属性进行配置。在这种情况下,需要使用另一个类,即ToolTipService类。使用ToolTipService类可以配置显示工具提示的相关延迟时间。ToolTipService类的所有属性都是附加属性,所以可在控件标签中直接设置它们。
ToolTipService类定义了许多与ToolTip相同的属性,从而当处理只有文本的工具提示时可使用更简单的语法。不是天津嵌套的ToolTip元素,可使用特性设置所有内容。
<Button ToolTip="This is my tooltip" ToolTipService.Placement="Bottom">I have a tooltip</Button>
下表列出了ToolTipService类的属性。ToolTipService类还提供了两个路由事件:ToolTipOpening和ToolTipClosing。可响应这些事件,使用即时内容填充工具提示,或重写工具提示的工作方式。例如,如果在这个两个事件中设置已经处理过的标志,将不再自动显示或隐藏工具提示。反而,需要通过设置IsOpen属性来手动显示和隐藏工具提示。
3、Popup控件
Popup控件在许多方面与ToolTip控件相同,尽管他们之间没有相互继承的关系。与ToolTip类似,Popup也只能包含单一内容,该单一内容包含任何WPF元素(该内容存储在Popup.Child属性中,而不想ToolTip内容那样存储在ToolTip.Content属性中)。另外,与ToolTip控件一样,Popup控件也延伸出窗口的边界。最后,可使用相同的布局属性放置Popup控件,并且可使用相同的IsOpen属性显示或隐藏Popup控件。
- Popup控件和ToolTip控件之间的区别更重要。这些区别包括:
- Popup控件永远不会自动显示。为显示Popup控件,必须设置IsO喷属性。
- 默认情况下,Popup.StaysOpen属性被设置为true,并且Popup控件会一直显示,知道明确地将IsOpen属性设置为false为止。如果将Popup.StaysOpen属性设置为false,那么当用户在其他地方单击鼠标时,Popup控件将消失。
- Popup控件提供了PopupAnimation属性,当把IsOpen属性设置为true时,通过该属性可控制Popup控制进入视野的方式。可以选择None(默认值)、Fade(弹出窗口的透明度逐渐增加)、Scroll(如果空间允许,弹出窗口将从窗口的左上角滑入)以及Slide(如果空间允许,弹出窗口将从上向下滑进其位置)。为使用这些动画中的任意一个,还必须将AllowsTransparency属性设置为true。
- Popup控件可接受焦点。因此,可在其内部放置于用户交互的控件,如按钮,该功能是使用Popup控件的主要原因之一。
- Popup控件在System.Windows.Controls.Primitive名称空间中定义,因为它的最常见用法是用作更复杂控件的构件。在外观修饰方面可发现Popup控件和其他控件的区别很大。特别是,如果希望看到内容,就必须设置Background属性,因为Popup控件不会从包含它的窗口继承背景设置,而且你需要自行添加边框(对于这个目的,Border元素的效果堪称完美)。
因为必须手动显示Popup控件,所以可能完全通过代码创建。但也可以可以使用XAML标记定义Popup控件——但务必包含Name属性,从而可以使用代码操作该控件。
简单示例如下所示:
<Window x:Class="Controls.PopupTest" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="PopupTest" Height="300" Width="300"> <Grid Margin="10"> <TextBlock TextWrapping="Wrap">You can use a Popup to provide a link for a specific <Run TextDecorations="Underline" MouseEnter="run_MouseEnter" >term</Run> of interest.</TextBlock> <Popup Name="popLink" StaysOpen="False" Placement="Mouse" MaxWidth="200" PopupAnimation="Slide" AllowsTransparency = "True"> <Border BorderBrush="Beige" BorderThickness="2" Background="White"> <TextBlock Margin="10" TextWrapping="Wrap" > For more information, see <Hyperlink NavigateUri="http://en.wikipedia.org/wiki/Term" Click="lnk_Click">Wikipedia</Hyperlink> </TextBlock> </Border> </Popup> </Grid> </Window>
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; 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.Shapes; namespace Controls { /// <summary> /// PopupTest.xaml 的交互逻辑 /// </summary> public partial class PopupTest : Window { public PopupTest() { InitializeComponent(); } private void run_MouseEnter(object sender, MouseEventArgs e) { popLink.IsOpen = true; } private void lnk_Click(object sender, RoutedEventArgs e) { Process.Start(((Hyperlink)sender).NavigateUri.ToString()); } } }