【WPF学习】第六十章 建立控件模板

  通过数十天的忙碌,今天终于有时间写博客。框架

  前面一章经过介绍有关模板工做方式相关的内容,同时介绍了FrameWorkElement下全部控件的模板。接下来将介绍如何构建一个简单的自定义按钮,并在该过程当中学习有关控件模板的一些细节。工具

  经过上一章内容,基本Button控件使用ButtonChrome类绘制其特殊的背景和边框。Button类使用ButtonChrome类而不使用WPF绘图图元的一个缘由是,标准按钮的外观依赖于几个明显的特征(是否被禁用、是否具备焦点以及是否正在被单击)和其余一些更微妙的因素(如当前Windows主题)。只使用触发器实现这类逻辑是笨拙的。学习

  然而,当构建自定义控件时,能够不用担忧标准化和主题集成(实际上,WPF不像之前的用户界面技术那样强调用户界面标准化)。反而能更须要关注如何建立富有吸引力的新颖控件,并将他们混合到用户界面的其余部分。所以,可能不须要建立诸如ButtonChrome的类,而可以使用以及学过的元素,设计自给自足的不使用代码的控件模板。测试

1、简单按钮动画

  为应用自定义控件模板,只须要设置控件的Template属性。尽管可定义内联模板(经过在控件标签内部嵌入控件模板标签),但这种方法基本没有意义。这是由于几乎老是但愿为同一控件的多个皮肤实例重用模板。为适应这种设计,须要将控件模板定义为资源,并使用StaticResource引用该资源,以下所示:spa

<Button  Margin="10" Padding="5" Template="{StaticResource ButtonTemplate}"  >
            A Simple Button with a Custom Template
</Button>

  经过这种方法,不只能够较容易地建立许多自定义按钮,在之后还能够很灵活地修改控件模板,而不会扰乱应用程序用户界面的其他部分。设计

  在这个特定示例中,ButtonTemplate资源放在包含窗口的Resource集合中。然而,在实际应用程序中,可能更喜欢使用应用程序资源,具体缘由在下一章介绍的“组织模板资源”中进行讨论。code

  下面是控件模板的基本框架:orm

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            ...
        </ControlTemplate>
</Window.Resources>

  在上面的控件模板中设置了TargetType属性,以明确指示该模板是为按钮设计的。与样式相似,这老是一个能够遵循的好约定。在内容控件(如按钮)中也须要使用该约定,不然ContentPresenter元素就不能工做。对象

  要为基本按钮建立模板,须要本身绘制边框和背景,而后在按钮中放置内容。绘制边框的两种可能的候选方法是使用Rectangle类和Border类。下面的示例使用Border类,将具备圆角的桔色轮廓与引入注目的红色背景和白色文本结合在一块儿:

 <Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
            ...
            </Border>
        </ControlTemplate>
</Window.Resources>

  在此主要关注背景,但仍须要一种方法显示按钮内容。在之前的学习中,可能还记得Button类在其余控件模板中包含了一个ContentPresenter元素。全部内容控件都须要ContentPresenter元素——它是标示“在此插入内容”的标记器,告诉WPF在何处保存内容:

<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                  <ContentPresenter RecognizesAccessKey="True"></ContentPresenter>
            </Border>
</ControlTemplate>

  在ContentPresenter元素将RecognizesAccessKey属性设置为true。尽管这不是必需的,但可确保按钮支持访问键——具备下划线的字母,可使用该字母快速触发按钮。对于这种状况,若是按钮具备文本Click_Me,那么当用户按下Alt+M组合键时会触发按钮(在标准的Windows设置中,下划线是隐藏的,而且只要按下Alt键,访问键(在此是M键)就会具备下划线)。若是为将RecongnizesAccessKey属性设置为true,就会忽略该细节,而且任何下划线都将被视为普通的下划线,并做为按钮内容的一部分进行显示。

2、模板绑定

  该例还存在一个小问题。如今为按钮添加的标签将Margin属性的值指定为10,并将Padding属性的值指定为5。StackPanel控件关注的是按钮的Margin属性,但忽略了Padding属性,使按钮的内容和侧边挤压在一块儿。此处的问题是Padding属性不起做用,除非在模板中特别注意它。换句话说,模板负责检索内边距值并使用该值在内容周围插入额外的空白。

  幸运的是,WPF专门针对该目的设计了一个工具:模板绑定。头能改过使用绑定模板,模板可从应用模板的控件中提取一个值。在本例中,可以使用模板绑定检索Padding属性的值,并使用该属性值在ContentPresenter元素周围建立外边距:

<ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                  <ContentPresenter RecognizesAccessKey="True"  Margin="{TemplateBinding Padding}"></ContentPresenter>
            </Border>
</ControlTemplate>

  这样就会获得所指望的效果,在边框和内容之间添加了一些空白。以下图显示了新的简单按钮。

   模板绑定和普通的数据绑定相似,但它们的量级更轻,由于它们是专门针对在控件模板中使用而设计的。它们只支持单向数据banding(换句话说,它们是从控件向模板传递信息,但不能从模板向控件传递信息),而且不能用于从Freezable类的派生类的属性中提取信息。若是遇到模板绑定不生效的情形,可改用具备完整功能的数据绑定。

  预计须要哪些模板绑定的惟一方法是检查默认控件模板。若是查看Button类的控件模板,就会发如今模板绑定的使用方法上与自定义模板是彻底相同的——获取为按钮指定的内边距,并将它转换成ContentPresenter元素周围的外边距。还会发现标准按钮模板包含另外几个模板绑定,如HorizontalAlignment、VerticalAlignment以及Background,这个简单的自定义模板中没有使用这些模板绑定。这意味着若是为控件设置了这些属性,对于这个简单的自定义模板来讲,这些设置是没有效果。

  在许多状况下,可不考虑模板绑定。实际上,若是不许备使用属性或者不但愿修改模板,就没必要绑定属性。例如,当前得简单按钮将用于文本的Foreground属性设置为白色并忽略为Background属性设置的任何值是合理的,由于前景色和背景色是该该按钮可视化外观的固有部分。

  可能选择避免模板绑定的另外一个缘由是——控件不能横好地支持它们。例如,若是为按钮设置了Background属性,可能注意到当按钮被按下时不会连贯地处理该背景色(实际上,这时该背景色消失了,而且被按下的默认外观替换了)。该例中的自定义模板与此相似,尽管尚未任何鼠标悬停和鼠标单击行为,但一旦添加这些细节,就会但愿彻底控制按钮的颜色以及在不一样状态下它们的变化。

3、改变属性的触发器

  若是测试上一节建立的按钮,就会发现它使人十分失望。本质上,它不过是一个红色的圆角矩形——当在它上面移动鼠标或单击鼠标时,其外观没有任何反应。按钮只是无动于衷,呆在那儿不动。

  可经过为控件模板添加触发器来方便地解决这个问题。当一个属性发生变化时,可以使用触发器改变另外一个或多个属性。在按钮中至少但愿响应IsMouseOver和IsPressed属性。下面的标记是控件模板的修改版本。当这些属性发生变化时,会改变控件的颜色:

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                <ContentPresenter RecognizesAccessKey="True" 
                                      Margin="{TemplateBinding Padding}"></ContentPresenter>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="DarkRed"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="IndianRed"/>
                    <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>

  为使该模板可以工做,还要进行另外一项修改。已为Border元素指定一个名称,而且该名称被用于设置每一个设置其的TargetName属性。经过这种方法,设置器能更新在模板中指定的Border元素的Background和BorderBrush属性。使用名称是确保更新模板特定部分的最容易方法。可建立一条元素类型规则来影响全部Border元素(缘由是已经知道在按钮模板中只有一个边框),但若是在之后改变模板,这种方法更清晰,也跟更灵活。

  在全部按钮(以及其余大部分控件)中还须要另外一个元素——焦点指示器。虽然没法改变现有的边框以天津焦点效果,可是能够很容易地天津另外一个元素以显示是否具备焦点,而且能够简单地使用触发器根据Button.IsKeyboardFocused属性显示或隐藏该元素。尽管可以使用许多方法建立焦点效果,但下面的示例值只天津了一个具备虚线边框的透明的Rectangle元素。Rectangle元素不能包含子内容,从而须要确保Rectangle元素和其他内容相互重叠。完成该操做最容易得方法是,使用只有一个单元格的Grid空哦关键来封装Rectangle元素和ContentPresenter元素,这两个元素位于同一个单元格中。

  下面是修改后的支持焦点的的模板:

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                <Grid>
                    <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
                               StrokeThickness="1" StrokeDashArray="1 2"
                               SnapsToDevicePixels="True"></Rectangle>
                    <ContentPresenter RecognizesAccessKey="True" 
                                      Margin="{TemplateBinding Padding}"></ContentPresenter>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="DarkRed"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="IndianRed"/>
                    <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki"/>
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>

  设置器在此使用TargetName属性茶盅须要改变的元素。

  下图显示了使用修改版模板的三个按钮。第二个按钮当前具备焦点(经过虚线矩形表示),而鼠标正好悬停在第三个按钮上。

   为了润色该按钮,还须要另外一个触发器。当按钮的IsEnable属性变为false是,该触发器改变按钮的背景色(也可改变文本的前景色):

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                <Grid>
                    <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
                               StrokeThickness="1" StrokeDashArray="1 2"
                               SnapsToDevicePixels="True"></Rectangle>
                    <ContentPresenter RecognizesAccessKey="True" 
                                      Margin="{TemplateBinding Padding}"></ContentPresenter>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="DarkRed"/>
                </Trigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="IndianRed"/>
                    <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki"/>
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible"/>
                </Trigger>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter TargetName="Border" Property="TextBlock.Foreground" Value="Gray" />
                    <Setter TargetName="Border" Property="Background" Value="MistyRose" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>

  为确保该规则优先与其余相冲突的触发器设置,应当在触发器列表的末尾定义它。这样,无论IsMouseOver属性是否为true,IsEnabled属性触发器都具备优先权,而且按钮保持未激活状态的外观。

  下图设置按钮不可用时所示的图片:

4、使用动画的触发器

  触发器并不是局限于设置属性。当特定属性发生变化时,还可使用事件触发器运行动画。

  乍一看,这好像有些曲折,但除了最简单的WPF控件外,触发器实际上时其余全部WPF控件的关键要素。例如,考虑到目前位置研究过的按钮。目前,当鼠标移到按钮上时,该按钮当即从一种颜色切换到另外一种颜色。然而,更时髦的按钮可能使用一个很是短暂的动画从一种颜色昏倒到其余颜色,从而建立微妙但优雅的效果。相似地,按钮可以使用动画改变焦点提示矩形的透明度,当按钮获取焦点时将快速淡入到试图中,而不是骤然显示。换句话说,事件触发器容许控件更通畅地一点点从一个状态改变到另外一个状态,从而进一步润色其外观。

  下面是从新设计的按钮模板,当鼠标悬停在按钮上时,该模板使用触发器实现按钮颜色脉冲效果(在红色和蓝色之间不断切换)。当鼠标离开时,使用一个单独的持续1秒得动画,将按钮背景返回到其正常颜色:

<Window.Resources>
        <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}">
            <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2"
                    Background="Red" TextBlock.Foreground="White" Name="Border">
                <Grid>
                    <Rectangle Name="FocusCue" Visibility="Hidden" Stroke="Black"
                               StrokeThickness="1" StrokeDashArray="1 2"
                               SnapsToDevicePixels="True"></Rectangle>
                    <ContentPresenter RecognizesAccessKey="True" 
                                      Margin="{TemplateBinding Padding}"></ContentPresenter>
                </Grid>
            </Border>
            <ControlTemplate.Triggers>
                <EventTrigger RoutedEvent="MouseEnter">
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation Storyboard.TargetName="Border"
                                Storyboard.TargetProperty="Background.Color" 
                                  To="Blue" AutoReverse="True" RepeatBehavior="Forever" Duration="0:0:1"></ColorAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <EventTrigger RoutedEvent="MouseLeave">
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation Storyboard.TargetName="Border"
                                            Storyboard.TargetProperty="Background.Color"
                                            Duration="0:0:0.5"></ColorAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <Trigger Property="IsPressed" Value="True">
                    <Setter TargetName="Border" Property="Background" Value="IndianRed" />
                    <Setter TargetName="Border" Property="BorderBrush" Value="DarkKhaki" />
                </Trigger>
                <Trigger Property="IsKeyboardFocused" Value="True">
                    <Setter TargetName="FocusCue" Property="Visibility" Value="Visible" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </Window.Resources>

  可以使用两种等价的方法添加鼠标悬停动画——建立响应MouseEnter和MouseLeave事件的事件触发器,或建立当IsMouseOver属性发生变化时添加进入和退出动做的属性触发器。最终效果图以下所示:

  该例使用两个ColorAnimation对象来改变按钮。下面是可能但愿使用EventTrigger驱动的动画只需的其余一些任务:

  •   显示或隐藏元素。为此,须要改变控件模板中元素的Opacity属性。
  •   改变形状或位置。可以使用TranslateTransform对象调整元素的位置(例如,稍偏移元素使按钮具备已被按下的感受)。当用户将鼠标移到元素上时,可以使用ScaleTransform或RotateTransform对象稍微旋转元素的外观。
  •   改变光照或着色。为此,需使用改变绘制背景色画刷的动画。可以使用ColorAnimation动画改变SolidBrush画刷中的颜色,也可动态显示更复杂的画刷以获得更高级的效果。例如,使用LinearGradientBrush画刷中的一种颜色(这是默认按钮控件模板执行的操做),也可改变RadialGradientBrush画刷的中心点。
相关文章
相关标签/搜索