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

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

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

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

一、简单按钮

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

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

  通过这种方法,不仅可以较容易地创建许多自定义按钮,在以后还可以很灵活地修改控件模板,而不会扰乱应用程序用户界面的其余部分。

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

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

<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,就会忽略该细节,并且任何下划线都将被视为普通的下划线,并作为按钮内容的一部分进行显示。

二、模板绑定

  该例还存在一个小问题。现在为按钮添加的标签将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属性,可能注意到当按钮被按下时不会连贯地处理该背景色(实际上,这时该背景色消失了,并且被按下的默认外观替换了)。该例中的自定义模板与此类似,尽管还没有任何鼠标悬停和鼠标单击行为,但一旦添加这些细节,就会希望完全控制按钮的颜色以及在不同状态下它们的变化。

三、改变属性的触发器

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

  可通过为控件模板添加触发器来方便地解决这个问题。当一个属性发生变化时,可使用触发器改变另一个或多个属性。在按钮中至少希望响应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属性触发器都具有优先权,并且按钮保持未激活状态的外观。

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

四、使用动画的触发器

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

  乍一看,这好像有些曲折,但除了最简单的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画刷的中心点。

【WPF学习】第六十章 创建控件模板的更多相关文章

  1. 【WPF学习】第二十章 内容控件

    内容控件(content control)是更特殊的控件类型,它们可包含并显示一块内容.从技术角度看,内容控件时可以包含单个嵌套元素的控件.与布局容器不同的是,内容控件只能包含一个子元素,而布局容器主 ...

  2. WPF笔记(1.9 样式和控件模板)——Hello,WPF!

    原文:WPF笔记(1.9 样式和控件模板)--Hello,WPF! 资源的另一个用途是样式设置: <Window >  <Window.Resources>    <St ...

  3. IOS开发学习笔记019-动态创建控件

    动态创建控件 一.按钮 二.文本输入框 三.lable标签 注意: 只是简单的拖拽控件会毁了你,所以最好还是手动通过代码创建控件. 如果要通过代码生成按钮的话,可以在系统自带的函数viewDidLoa ...

  4. WPF中Expander的用法和控件模板详解

    一.Expander的用法 在WPF中,Expander是一个很实用的复合控件,可以很方便的实现下拉菜单和导航栏等功能.先介绍简单的用法,而后分析他的控件模板. <Window.Resource ...

  5. WP8.1学习系列(第二十章)——添加控件和处理事件

    先决条件 添加控件 设置控件的名称 设置控件属性 创建事件处理程序 新控件 总结 相关主题 通过使用如按钮.文本框和组合框等控件,你可以创建应用的 UI. 下面将显示如何将控件添加到应用.处理控件时, ...

  6. WP8.1学习系列(第十章)——中心控件Hub设计指南

    Windows Phone 应用商店应用中的中心控件指南   在本文中 说明 示例 用法指南 设计指南 相关主题 重要的 API Hub (XAML) HubSection (XAML) 说明 中心控 ...

  7. WPF学习(一)--布局控件简介

    WPF的4种基本布局介绍 1.Grid的布局 这个就没啥特别好说的,其实,基本上复杂的布局,都需要用到Grid. 主要就是对行和列进行进行设置和定义. 1.行表格 列表格: 包含行和列的表格 2.St ...

  8. 【WPF学习】第五十九章 理解控件模板

    最近工作比较忙,未能及时更新内容,敬请了解!!! 对于可视化树的分析引出了几个有趣问题.例如,控件如何从逻辑树表示扩张成可视化树表示? 每个控件都有一个内置的方法,用于确定如何渲染控件(作为一组更基础 ...

  9. WP8.1学习系列(第二十六章)——控件模板

    在本文中 自定义控件模板示例 指定控件的可视结构. 指定控件的可视行为 使用工具轻松处理主题 控件和辅助功能 了解有关控件默认模板的详细信息 控件模板中的主题资源 相关主题 在 XAML 框架中,如果 ...

随机推荐

  1. 拿到webshell之后的事情

    之前搞站都是搞到后台管理员,或者搞到webshell就宣布结束了,,今天终于有机会学习一下后面的操作了. 公司网站为php的站,已经拿到webshell.可以进行菜刀连接. 菜刀虚拟终端 php -m ...

  2. ORACLE数据库实现主键自增

    ORACLE数据库是甲骨文公司的一款关系数据库管理系统. 实现主键自动增长需要四个步骤: 去看 创建表格 去看 创建自增序列 去看 创建触发器 去看 插入测试 1.创建表格(必须有主键) -- 创建学 ...

  3. js 打开新窗口方式

    之前的项目,有个功能是下载文件,这里只要在浏览器输入 url 就会下载那个文件了.当时我只是简单得使用 window.open ,但是却会被浏览器进行拦截,要手动开启才行,然后就搜索研究其他方法,就看 ...

  4. ubuntu下pip的安装,更新及卸载

    在Ubuntu下,不小心uninstall pip了,然后呢,作为小白的我,还是有些着急的,用了一些方法不好使,最后找到了这个方法: 1.安装pip3: sudo apt-get install py ...

  5. Spring Boot从入门到精通(七)集成Redis实现Session共享

    单点登录(SSO)是指在多个应用系统中,登录用户只需要登录验证一次就可以访问所有相互信任的应用系统,Redis Session共享是实现单点登录的一种方式.本文是通过Spring Boot框架集成Re ...

  6. 本地目录配置多个远程Git仓库

    目录 情景一:不同的库分别 pull/push 1. 使用git命令配置 2. 修改.git/config 文件 3. 操作 情景二:不同的库一次push 1. 使用git命令配置 2. 修改.git ...

  7. 如何在PHP7中扩展mysql,先安装php7.2。后安装mysql

    相对与PHP5,PHP7的最大变化之一是移除了mysql扩展,推荐使用mysqli或者pdo_mysql,实际上在PHP5.5开始,PHP就着手开始准备弃用mysql扩展,如果你使用mysql扩展,可 ...

  8. oracle单机数据库搭建巨详细文档

    规划 环境:redhat6.9 安装包:p13390677_112040_Linux-x86-64_1of7.zip p13390677_112040_Linux-x86-64_2of7.zip 数据 ...

  9. django 从零开始 4 404页面和500页面设置

    在视图函数中定义两个 函数 分别对应404 个500页面 (自定义html内容吧,这里只是展示) 在template页面指向自己定义的404.html和500.html页面 在项目的urls中设置 h ...

  10. RStudio终端操作

    转于:https://support.rstudio.com/hc/en-us/articles/115010737148-Using-the-RStudio-Terminal#send 原文是英文版 ...