上一章介绍的ColorPicker控件,是控件设计的最好示例。由于其行为和可视化外观是精心分离的,因此其余设计人员可开发动态改变其外观的新模板。编程
ColorPicker控件如此简单的一个缘由是不涉及状态。换句话说,不根据是否具备焦点、鼠标是否在它上面悬停、是否禁用等状态区分其可视化外观。接下来本章介绍的FlipPanel自定义控件有些不一样。app
FlipPanel控件背后的基本思想是,为驻留内容提供两个表面,但每次只有一个表面是可见的。为看到其余内容,须要在两个表面之间进行“翻转”。可经过控件模板定制翻转效果,但默认效果使用在前面和后面之间进行过渡的淡化效果。根据应用程序,可使用FlipPanel控件把数据条目表单与一些由帮助的文档组合起来,以便为相同的数据提供一个简单或较复杂的试图,或在一个简单游戏中将问题和答案融合在一块儿。框架
可经过代码执行翻转(经过设置名为IsFlipped的属性),也可以使用一个便捷的按钮来翻转面板(除非控件使用这从模板中移除了该按钮)。ide
显然,控件模板须要制定两个独立部分:FlipPanel控件的先后内容区域。然而,还有一个细节——FlipPanel控件须要一种方法在两个状态之间进行切换:翻转过的状态与未翻转过的状态。可经过为模板添加触发器完成该工做。当单击按钮是,可以使用一个触发器隐藏前面的面板并显示第二个面板,而使用另外一个触发器翻转这些更改。这两个触发器均可以使用喜欢的任何动画。但经过使用可视化状态,可向控件使用这清晰地指明这两个状态是模板的必须部分,不是为适当的属性或事件编写触发器,控件使用能管着只须要填充适当的状态动画。若是使用Expression Blend,该任务甚至变得更简单。函数
1、开始编写FlipPanel类布局
FlipPanel的基本骨架很是简单。包含用户可用单一元素(最有多是包含各类元素的布局容器)填充的两个内容区域。从技术角度看,这意味着FlipPanel控件不是真正的面板,由于不能使用布局逻辑组织一组子元素。然而,这不会形成问题。由于FlipPanel控件的结构是清晰直观的。FlipPanel控件还包含一个翻转按钮,用户可以使用该按钮在两个不一样的内容区域之间进行切换。动画
尽管可经过继承自ContentControl或Panel等控件类来建立自定义控件,可是FlipPanel直接继承自Control基类。若是不须要特定控件类的功能,这是最好的起点。不该该当继承自更简单的FrameworkElement类,除非但愿建立不使用标准控件和模板基础框架的元素:this
public class FlipPanel:Control { }
首先为FlipPanel类建立属性。与WPF元素中的几乎全部属性同样,应使用依赖项属性。如下代码演示了FlipPanel如何定义FrontContent属性,该属性保持在前表面上显示的元素。编码
public static readonly DependencyProperty FrontContentProperty = DependencyProperty.Register("FrontContent", typeof(object), typeof(FlipPanel), null);
接着须要调用基类的GetValue()和SetValue()方法的常规.NET属性过程,以便修改依赖性属性。下面是FrontContent属性的实现过程:spa
/// <summary> /// 前面内容 /// </summary> public object FrontContent { get { return GetValue(FrontContentProperty); } set { SetValue(FrontContentProperty, value); } }
同理,还须要一个存储背面的依赖项属性。以下所示:
public static readonly DependencyProperty BackContentProperty = DependencyProperty.Register("BackContent", typeof(object), typeof(FlipPanel), null); /// <summary> /// 背面内容 /// </summary> public object BackContent { get { return GetValue(BackContentProperty); } set { SetValue(BackContentProperty, value); } }
还须要添加一个重要属性:IsFlipped。这个Boolean类型的属性持续跟踪FlipPanel控件的当前状态(面向前面仍是面向后面),使控件使用者可以经过编程翻转状态:
public static readonly DependencyProperty IsFlippedProperty = DependencyProperty.Register("IsFlipped", typeof(bool), typeof(FlipPanel), null); /// <summary> /// 是否翻转 /// </summary> public bool IsFlipped { get { return (bool)GetValue(IsFlippedProperty); } set { SetValue(IsFlippedProperty, value); ChangeVisualState(true); } }
IsFlipped属性设置器调用自定义方法ChangeVisualState()。该方法确保更新显示以匹配当前的翻转状态。稍后介绍ChangeVisualState方法。
FlipPanel类不须要更多属性,由于它实际上从Control类继承了它所须要的几乎全部内容。一个例外是CornerRadius属性。尽管Control类包含了BorderBrush和BorderThickness属性,可使用这些属性在FlipPanel控件上绘制边框,但缺乏将方形边缘变成光滑曲线的CornerRadius属性,如Border元素所作的那样。在FlipPanel控件中实现相似的效果很容易,前提是添加CornerRadius依赖性属性并使用该属性配置FlipPanel控件的默认控件模板中的Border元素:
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(FlipPanel), null); /// <summary> /// 控件边框圆角 /// </summary> public CornerRadius CornerRadius { get { return (CornerRadius)GetValue(CornerRadiusProperty); } set { SetValue(CornerRadiusProperty, value); } }
还须要为FlipPanel控件添加一个应用默认模板的样式。将该样式放在generic.xaml资源字典中,正如在开发ColorPicker控件时所作的那样。下面是须要的基本骨架:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CustomControls"> <Style TargetType="{x:Type local:FlipPanel}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:FlipPanel"> ... </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
还有最后一个细节。为通知控件从generic.xaml文件获取默认样式,须要在FlipPanel类的静态构造函数中调用DefaultStyleKeyProperty.OverrideMetadata()方法:
DefaultStyleKeyProperty.OverrideMetadata(typeof(FlipPanel), new FrameworkPropertyMetadata(typeof(FlipPanel)));
2、选择部件和状态
如今已经具有了基本结构,而且已经准备好肯定将在控件模板中使用的部件和状态了。
显然,FlipPanel须要两个状态:
此外,须要两个部件:
还应当为先后内容区域添加部件。然而,FlipPanel克难攻坚不须要直接操做这些区域,只要模板包含在适当的时间隐藏和显示它们的动画便可(另外一种选择是定义这些部件,从而能够明确地使用代码改变它们的可见性。这样一来,即便没有定义动画,经过隐藏一部分并显示另外一部分,面板仍能在先后内容区域之间变化。为简单起见,FlipPanel没有采起这种选择)。
为表面FlipPanel使用这些部件和状态的事实,应为自定义控件类应用TemplatePart特性,以下所示:
[TemplatePart(Name = "FlipButton", Type = typeof(ToggleButton))] [TemplatePart(Name = "FlipButtonAlternate", Type = typeof(ToggleButton))] [TemplateVisualState(Name = "Normal", GroupName = "ViewStates")] [TemplateVisualState(Name = "Flipped", GroupName = "ViewStates")] public class FlipPanel : Control { }
3、默认控件模板
如今,可将这些内容投入到默认控件模板中。根元素是具备两行的Grid面板,该面板包含内容区域(在顶行)和翻转按钮(在底行)。用两个相互重叠的Border元素填充内容区域,表明前面和后面的内容,但一次只显示前面和后面的内容。
为了填充前面和后面的内容区域,FlipPanel控件使用ContentControl元素。该技术几乎和自定义按钮示例相同,只是须要两个ContentPresenter元素,分别用于FlipPanel控件的前面和后面。FlipPanel控件还包含独立的Border元素来封装每一个ContentPresenter元素。从而让控件使用者能经过设置FlipPanel的几个直接属性勾勒出可翻转内容区域(BorderBrush、BorderThickness、Background以及CornerRadius),而不是强制性地手动添加边框。
下面是默认控件模板的基本骨架:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:CustomControls"> <Style TargetType="{x:Type local:FlipPanel}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:FlipPanel}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <!-- This is the front content. --> <Border x:Name="FrontContent" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" > <ContentPresenter Content="{TemplateBinding FrontContent}"> </ContentPresenter> </Border> <!-- This is the back content. --> <Border x:Name="BackContent" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" > <ContentPresenter Content="{TemplateBinding BackContent}"> </ContentPresenter> </Border> <!-- This the flip button. --> <ToggleButton Grid.Row="1" x:Name="FlipButton" Margin="0,10,0,0" > </ToggleButton> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
当建立默认控件模板时,最好避免硬编码控件使用者可能但愿定制的细节。相反,须要使用模板绑定表达式。在这个示例中,使用模板绑定表达式设置了几个属性:BorderBrush、BorderThickness、CornerRadius、Background、FrontContent以及BackContent。为设置这些属性的默认值(这样即便控件使用者没有设置它们,也仍然确保能获得正确的可视化外观),必须为控件的默认样式添加额外的设置器。
一、翻转按钮
在上面的示例中,显示的控件模板包含一个ToggleButton按钮。然而,该按钮使用ToggleButton的默认外观,这使得ToggleButton按钮看似广泛的按钮,彻底具备传统的阴影背景。这对于FlipPanel控件是不合适的。
尽管可替换ToggleButton中的任何内容,但FlipPanel须要进一步。它须要去掉标准的背景并根据ToggleButton按钮的状态改变其内部元素的外观。
为建立这种效果,须要为ToggleButton设置自定义控件模板。该控件模板可以包含绘制所需箭头的形状元素。在该例中,ToggleButton是使用用于绘制圆的Ellipse元素和用于绘制箭头的Path元素绘制的,这两个元素都放在具备单个单元格的Grid面板中,以及须要一个改变箭头指向的RotateTransform对象:
<ToggleButton Grid.Row="1" x:Name="FlipButton" RenderTransformOrigin="0.5,0.5" Margin="0,10,0,0" Width="19" Height="19"> <ToggleButton.Template> <ControlTemplate> <Grid> <Ellipse Stroke="#FFA9A9A9" Fill="AliceBlue" /> <Path Data="M1,1.5L4.5,5 8,1.5" Stroke="#FF666666" StrokeThickness="2" HorizontalAlignment="Center" VerticalAlignment="Center"> </Path> </Grid> </ControlTemplate> </ToggleButton.Template> <ToggleButton.RenderTransform> <RotateTransform x:Name="FlipButtonTransform" Angle="-90"></RotateTransform> </ToggleButton.RenderTransform> </ToggleButton>
二、定义状态动画
状态动画是控件模板中最有趣的部分。它们是提供翻转行为的要素,它们仍是为FlipPanel建立自定义模板的开发人员最有可能修改的细节。
为定义状态组,必须在控制模板的根元素中添加VisualStateManager.VisualStateGroups元素,以下所示:
<ControlTemplate TargetType="{x:Type local:FlipPanel}"> <Grid> <VisualStateManager.VisualStateGroups> ... </VisualStateManager.VisualStateGroups> </Grid> </ControlTemplate>
可在VisualStateGroups元素内部使用具备合适名称的VisualStateGroup元素建立状态组。在每一个VisualStateGroup元素内部,为每一个状态添加一个VisualState元素。对于FlipPanel面板,有一个包含两个可视化状态的组:
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="ViewStates"> <VisualState x:Name="Normal"> <Storyboard> ... </Storyboard> </VisualState> </VisualStateGroup> <VisualStateGroup x:Name="FocusStates"> <VisualState x:Name="Flipped"> <Storyboard> ... </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
每一个状态对应一个具备一个或多个动画的故事板。若是存在这些故事板,就会在适当的时机触发它们(若是不存在,控件将按正常方式降级,而不会引起错误)。
在默认控件模板中,动画使用简单的淡化效果从一个内容区域改变到另外一个内容区域,并使用旋转变换翻转ToggleButton箭头使其指向另外一个方向。下面是完成这两个任务的标记:
<VisualState x:Name="Normal"> <Storyboard> <DoubleAnimation Storyboard.TargetName="BackContent" Storyboard.TargetProperty="Opacity" To="0" Duration="0" ></DoubleAnimation> </Storyboard> </VisualState> <VisualState x:Name="Flipped"> <Storyboard> <DoubleAnimation Storyboard.TargetName="FlipButtonTransform" Storyboard.TargetProperty="Angle" To="90" Duration="0"> </DoubleAnimation> <DoubleAnimation Storyboard.TargetName="FrontContent" Storyboard.TargetProperty="Opacity" To="0" Duration="0"></DoubleAnimation> </Storyboard> </VisualState>
经过上面标记,发现可视化状态持续时间设置为0,这意味着动画当即应用其效果。这看起来可能有些怪——毕竟,不是须要更平缓的改变从而可以注意到动画效果吗?
时机上,该设计完成正确,由于可视化状态用于表示控件在适当状态时的外观。例如,当翻转面板处于翻转过的状态是,简单地显示其背面内容。翻转过程是在FlipPanel控件进入翻转状态前得过渡,而不是翻转状态自己的一部分。
三、定义状态过渡
过渡是从当前状态到新状态的动画。变换模型的优势之一是不须要为动画建立故事板。例如,若是添加以下标记,WPF会建立持续时间为0.7秒得动画以改变FlipPanel控件的透明度,从而建立所但愿的悦目的褪色效果:
<VisualStateGroup x:Name="ViewStates"> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="0:0:0.7"> </VisualTransition> </VisualStateGroup.Transitions> <VisualState x:Name="Normal"> ... </VisualState> </VisualStateGroup>
过渡会应用到状态组,当定义过渡时,必须将其添加到VisualStateGroup.Transitions集合。这个示例使用最简单的过渡类型:默认过渡。默认过渡应用于该组中的全部状态变化。
默认过渡是很方便的,但用于全部状况的解决方案不可能老是适合的。例如,可能但愿FlipPanel控件根据其进入的状态以不一样的速度过渡。为实现该效果,须要定义多个过渡,而且须要设置To属性以指示什么时候应用过渡效果。
例如,若是有如下过渡:
<VisualStateGroup.Transitions> <VisualTransition To="Flipped" GeneratedDuration="0:0:0.5"></VisualTransition> <VisualTransition To="Normal" GeneratedDuration="0:0:0.1"></VisualTransition> </VisualStateGroup.Transitions>
FlipPanel将在0.5秒得时间内切换到Flipped状态,并在0.1秒得时间内进入Normal状态。
这个示例显示了当进入特定状态时应用的过渡,但还可以使用From属性建立当离开某个状态时应用的过渡,而且可结合使用To和From属性来建立更特殊的只有当在特定的两个状态之间移动时才会应用的过渡。当应用过渡时WPF遍历过渡集合,在全部应用的过渡中查找最特殊的过渡,并只使用最特殊的那个过渡。
为进一步加以控制,可建立自定义过渡动画来替换WPF一般使用的自动生成的过渡。可能会犹因为几个缘由而建立自定义过渡。下面是一些例子:使用更复杂的动画控制动画的步长,使用动画缓动、连续运行几个动画或在运行动画时播放声音。
为定义自定义过渡,在VisualTransition元素中放置具备一个或多个动画的故事板。在FlipPanel示例中,可以使用自定义过渡确保ToggleButton箭头更快递旋转自身,而淡化过程更缓慢:
<VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="0:0:0.7" To="Flipped"> <Storyboard> <DoubleAnimation Storyboard.TargetName="FlipButtonTransform" Storyboard.TargetProperty="Angle" To="90" Duration="0:0:0.2"></DoubleAnimation> </Storyboard> </VisualTransition> <VisualTransition GeneratedDuration="0:0:0.7" To="Normal"> <Storyboard> <DoubleAnimation Storyboard.TargetName="FlipButtonTransform" Storyboard.TargetProperty="Angle" To="-90" Duration="0:0:0.2"></DoubleAnimation> </Storyboard> </VisualTransition> </VisualStateGroup.Transitions>
但许多控件须要自定义过渡,并且编写自定义过渡是很是乏味的工做。仍需保持零长度的状态动画,这还会不可避免地再可视化状态和过渡之间复制一些细节。
四、关联元素
经过上面的操做,已经建立了一个至关好的控件模板,须要在FlipPanel控件中添加一些内容以使该模板工做。
诀窍是使用OnApplyTemplate()方法,该方法还款用于在ColorPicker控件中设置绑定。对于FlipPanel控件,OnApplyTemplate()方法用于为FlipButton和FlipButtonAlternate部件检索ToggleButton,并为每一个部件关联事件处理程序,从而当用户单击以翻转控件时可以进行响应。最后,OnApplyTemplate()方法调用名为ChangeVisualState()的自定义方法,该方法确保控件的可视化外观和其当前状态的相匹配:
public override void OnApplyTemplate() { base.OnApplyTemplate(); ToggleButton flipButton = base.GetTemplateChild("FlipButton") as ToggleButton; if (flipButton != null) flipButton.Click += flipButton_Click; // Allow for two flip buttons if needed (one for each side of the panel). // This is an optional design, as the control consumer may use template // that places the flip button outside of the panel sides, like the // default template does. ToggleButton flipButtonAlternate = base.GetTemplateChild("FlipButtonAlternate") as ToggleButton; if (flipButtonAlternate != null) flipButtonAlternate.Click += flipButton_Click; this.ChangeVisualState(false); }
下面是很是简单的容许用户单击ToggleButton按钮并翻转面板的事件处理程序:
private void flipButton_Click(object sender, RoutedEventArgs e) { this.IsFlipped = !this.IsFlipped; }
幸运的是,不须要手动触发状态动画。即不须要建立也不须要触发过渡动画。相反,为从一个状态改变到另外一个状态,只须要调用静态方法VisualStateManager.GoToState()。当调用该方法时,传递正在改变状态的控件对象的引用、新状态的名称以及肯定是否显示过渡的Boolean值。若是是由用户引起的改变(例如,当用户单击ToggleButton按钮时),该值应当为true;若是是由属性设置引起的改变(例如,若是使用页面的标记设置IsFlipped属性的初始值),该值为false。
处理控件支持的全部不一样状态可能会变得凌乱。为避免在整个控件代码中分散调用GoToState()方法,大多数控件添加了与在FlipPanel控件中添加的ChangeVisualState()相似地方法。该方法负责应用每一个状态组中的正确状态。该方法中的代码使用If语句块(或switch语句)应用每一个状态组的当前状态。该方法之因此可行,是由于它彻底可使用当前状态的名称调用GoToState()方法。在这种状况下,若是当前状态和请求的状态相同,那么什么也不会发生。
下面是用于FlipPanel控件的ChangeVisualState()方法:
private void ChangeVisualState(bool useTransitions) { if (!this.IsFlipped) { VisualStateManager.GoToState(this, "Normal", useTransitions); } else { VisualStateManager.GoToState(this, "Flipped", useTransitions); } }
一般在如下位置调用ChangeVisualState()方法或其等效的方法:
正如上面介绍的,FlipPanel控件很是灵活。例如,可以使用该控件而且不使用ToggleButton按钮,经过代码进行翻转(多是当用户单击不一样的控件时)。也可在控件模板中包含一两个翻转按钮,而且容许用户进行控制。
4、使用FlipPanel控件
使用FlipPanel控件相对简单。标记以下所示:
<Window x:Class="CustomControlsClient.FlipPanelTest" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="FlipPanelTest" Height="300" Width="300" xmlns:lib="clr-namespace:CustomControls;assembly=CustomControls" > <Grid x:Name="LayoutRoot" Background="White"> <lib:FlipPanel x:Name="panel" BorderBrush="DarkOrange" BorderThickness="3" IsFlipped="True" CornerRadius="4" Margin="10"> <lib:FlipPanel.FrontContent> <StackPanel Margin="6"> <TextBlock TextWrapping="Wrap" Margin="3" FontSize="16" Foreground="DarkOrange">This is the front side of the FlipPanel.</TextBlock> <Button Margin="3" Padding="3" Content="Button One"></Button> <Button Margin="3" Padding="3" Content="Button Two"></Button> <Button Margin="3" Padding="3" Content="Button Three"></Button> <Button Margin="3" Padding="3" Content="Button Four"></Button> </StackPanel> </lib:FlipPanel.FrontContent> <lib:FlipPanel.BackContent> <Grid Margin="6"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <TextBlock TextWrapping="Wrap" Margin="3" FontSize="16" Foreground="DarkMagenta">This is the back side of the FlipPanel.</TextBlock> <Button Grid.Row="2" Margin="3" Padding="10" Content="Flip Back to Front" HorizontalAlignment="Center" VerticalAlignment="Center" Click="cmdFlip_Click"></Button> </Grid> </lib:FlipPanel.BackContent> </lib:FlipPanel> </Grid> </Window>
当单击FlipPanel背面的按钮时,经过编程翻转面板:
private void cmdFlip_Click(object sender, RoutedEventArgs e) { panel.IsFlipped = !panel.IsFlipped; }
本实例源码:FlipPanel.zip