WPF 中的 Expander 控件在 Windows 10 SDK 中并不提供,本文主要说明,如何在 UWP 中创建这样一个控件。其效果如下图:

首先,分析该控件需要的一些特性,它应该至少包括如下三个属性:

  • Content: 最重要的属性,设置该属性,可以使 Expander 控件显示其内容;
  • Header: 控件的 Header;
  • IsExpand: 当前是否展开。

接下来是定义其 UI,在这里使用 Grid,添加两行,一行显示 Header,一行显示 Content,当 IsExpand 属性为 false 时,只要将 Content 那一行隐藏即可;此外,还需要一个 ToggleButton 用于控制该控件的展开与关闭。

OK。思路弄清楚后,开始实践,

1. 创建控件,并添加属性

在项目中添加一个 Templated Control(模板化控件),名称为 Expander。为其添加三个依赖属性,代码如下:

   public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register("Content", typeof(object), typeof(Expander), new PropertyMetadata(null)); public static readonly DependencyProperty HeaderProperty =
DependencyProperty.Register("Header", typeof(object), typeof(Expander), new PropertyMetadata(null)); public static readonly DependencyProperty IsExpandProperty =
DependencyProperty.Register("IsExpand", typeof(bool), typeof(Expander), new PropertyMetadata(true)); /// <summary>
/// 控件的内容
/// </summary>
public object Content
{
get { return (object)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
} /// <summary>
/// 控件的标题
/// </summary>
public object Header
{
get { return (object)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
} /// <summary>
/// 返回或设置控件是否展开
/// </summary>
public bool IsExpand
{
get { return (bool)GetValue(IsExpandProperty); }
set { SetValue(IsExpandProperty, value); }
}

2. 定义UI

    在 Generic.xaml 中,找到 <Style TargetType="controls:Expander"> 节点,添加如下代码:

    <Style TargetType="controls:Expander">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:Expander">
<Grid x:Name="grid"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<ToggleButton x:Name="toggleButton"
Width="32"
Height="32"
Margin="0,0,4,0"
BorderThickness="0"
IsChecked="{Binding IsExpand,
RelativeSource={RelativeSource Mode=TemplatedParent},
Mode=TwoWay}">
<Path x:Name="arrow"
Width="16"
Height="16"
Data="M15.289001,0L20.484007,0 31.650999,15.953003 29.055021,19.658005 20.415007,32 15.35501,32 15.289001,31.906998 24.621,18.572998 0,18.572998 0,13.326004 24.621,13.326004z"
Fill="#DDFFFFFF"
RenderTransformOrigin="0.5,0.5"
Stretch="Uniform">
</Path>
</ToggleButton>
<ContentPresenter VerticalAlignment="Center" Content="{TemplateBinding Header}" />
</StackPanel>
<ContentPresenter Grid.Row="1"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Content="{TemplateBinding Content}"
Visibility="{Binding IsExpand,
RelativeSource={RelativeSource Mode=TemplatedParent},
Converter={StaticResource BooleanToVisibilityConverter},
Mode=TwoWay}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

可以看出:
    a) ToggleButton 的 IsChecked 属性绑定了控件的 IsExpand 属性, 绑定表达式 {Binding IsExpand,RelativeSource={RelativeSource Mode=TemplatedParent}} 是 {TemplateBinding IsExpand} 的另一种写法,在这种写法中,我们可以添加 Binding 对象的其它属性,如这里的 Mode=TwoWay,这样可以实现 ToggleButton 与 控件的 IsExpand 属性彼此互相的控制;
    b) ContentControl 的 Visibility 与 a) 同理,略微复杂的是,这里用了一个 Converter,用于在 Bool 和 Visibility 枚举之间转换;
    c) 我们为 ToggleButton 控件的 Content 属性设置了一个 Path 用来形象地表达 Expander 当前的状态。

3. 定义 VisualState

我们为该控件定义两个 VisualState,分别代表正常状态和展开状态,即 Normal 与 Expanded,通过切换这两种状态可以完成该控件的UI变化,这里主要是对 ToggleButton 的 Content 进行动画设置。

在 Path 中为其添加 RotateTransform,代码如下:

                      <Path x:Name="arrow"
Width="16"
Height="16"
Data="M15.289001,0L20.484007,0 31.650999,15.953003 29.055021,19.658005 20.415007,32 15.35501,32 15.289001,31.906998 24.621,18.572998 0,18.572998 0,13.326004 24.621,13.326004z"
Fill="#DDFFFFFF"
RenderTransformOrigin="0.5,0.5"
Stretch="Uniform">
<Path.RenderTransform>
<RotateTransform x:Name="pathRotate" />
</Path.RenderTransform>
</Path>

在 Grid 中添加 VisualState,代码如下:

                    <Grid x:Name="grid"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualStateGroup.Transitions>
<VisualTransition From="Normal"
GeneratedDuration="0:0:0.2"
To="Expanded" />
<VisualTransition From="Expanded"
GeneratedDuration="0:0:0.2"
To="Normal" />
</VisualStateGroup.Transitions>
<VisualState x:Name="Normal">
<Storyboard>
<DoubleAnimation Duration="0:0:0"
Storyboard.TargetName="pathRotate"
Storyboard.TargetProperty="Angle"
To="0">
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</VisualState>
<VisualState x:Name="Expanded">
<Storyboard>
<DoubleAnimation Duration="0:0:0"
Storyboard.TargetName="pathRotate"
Storyboard.TargetProperty="Angle"
To="90">
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode="EaseIn" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
...

这里,我们可以看到,除了两个 VisualState 外,我们还定义了两个 VisualTransition,用来设置切换此两种状态时的过度时间。

提示:关于 Content 区域的隐藏与显示,也可以通过在 VisualState 添加动画来控制,不过在上面的代码中,我们利用了 ToggleButton 以及它的 IsCheced 属性来控制其显示与隐藏,较为简洁地了实现这一功能。

接下来,我们需要在代码中来控制何时在这两种状态间切换,在 Expander.cs 中添加如下代码:

       private ToggleButton button;

        protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
button = GetTemplateChild("toggleButton") as ToggleButton;
button.Loaded += (s, e) => { ChangeControlState(false); };
button.Checked += (s, e) => { ChangeControlState(); };
button.Unchecked += (s, e) => { ChangeControlState(); };
} /// <summary>
/// 改变控件的 VisualState
/// </summary>
/// <param name="useTransition">是否使用 VisualTransition,默认使用</param>
private void ChangeControlState(bool useTransition = true)
{
if (button.IsChecked.Value)
{
VisualStateManager.GoToState(this, "Expanded", useTransition);
}
else
{
VisualStateManager.GoToState(this, "Normal", useTransition);
}
}

可以看出,我们为 ToggleButton 添加事件响应来切换状态。之所以在 Load 时也来改检查并更改状态,是因为,如果在使 Expander 控件时,如果为它设置 IsExpand 为 true 时,那么加载时,会及时更新控件状态为 Expanded ,否则将默认为 Normal。

最后,我们为控件添加一个 ContentPropertyAttribute,并设置其 Name 为 Content,这样,该控件的 Content 属性就作为此控件的内容属性(ContentPropery)。简言之,可以省去 <xxx:Expander.Content> 这个节点,类似在 Button 中直接添加其 Content 一样。代码如下:

    [ContentProperty(Name = "Content")]
public sealed class Expander : Control

至此,一个 Expander 控件就完成了,至于你还有额外、其它的需求(如样式的修改等),则可在此基础上进行修改。

如果你有什么更好的建议或其它观点,请留言,互相交流。

源代码下载

参考资料:

What's the difference between ContentControl and ContentPresenter?

Difference between ContentControl, ContentPresenter, ContentTemplate and ControlTemplate?  (Bob_Bao's answer)

What is ContentPropertyAttribute?

在 UWP 中实现 Expander 控件的更多相关文章

  1. WPF中Expander控件样式,ListBox的样式(带checkbox)恢复

    Expander控件样式: <ControlTemplate x:Key="ExpanderToggleButton" TargetType="ToggleButt ...

  2. Win10 UWP开发系列——开源控件库:UWPCommunityToolkit

    在开发应用的过程中,不可避免的会使用第三方类库.之前用过一个WinRTXamlToolkit.UWP,现在微软官方发布了一个新的开源控件库—— UWPCommunityToolkit 项目代码托管在G ...

  3. WPF Expander控件(扩展面板)

    这算是我比较喜欢的一个控件,以前在Winform中也常用类似的.它包装了一块内容,通过单击一个小箭头按钮可以显示或隐藏所包含的内容.在线帮助以及Web页面经常使用这种技术,因为既可以包含大量内容,而又 ...

  4. 《WPF》Expander控件简单美化

    示例图: Expander控件功能很常见, 一般用于系统左侧的菜单收缩面板. 1.主要的组成 一个头部(header) 和 一个 内容(content) 组成. <Expander Expand ...

  5. 在DevExpress程序中使用SplashScreenManager控件实现启动闪屏和等待信息窗口

    在我很早的WInform随笔<WinForm界面开发之"SplashScreen控件">有介绍如何使用闪屏的处理操作,不过那种是普通WInform和DevExpress ...

  6. 在WPF中使用WinForm控件方法

    1.      首先添加对如下两个dll文件的引用:WindowsFormsIntegration.dll,System.Windows.Forms.dll. 2.      在要使用WinForm控 ...

  7. wpf telerik中的book控件

    下载 telerik中的书本控件,仅供学习使用.

  8. [原创]在Framelayout中放置button控件出现的覆盖问题

    android Framelayout(帧布局)是很常用的布局,主要用来处理需要多个view叠加显示的情况. 然而在使用中,我发现Framelayout中的Button控件,会挡住所有其他控件,而不论 ...

  9. (转)客户端触发Asp.net中服务端控件事件

    第一章. Asp.net中服务端控件事件是如何触发的 Asp.net 中在客户端触发服务端事件分为两种情况: 一. WebControls中的Button 和HtmlControls中的Type为su ...

随机推荐

  1. MyBatis的好处及常见问题

    好处 MyBatis持久层框架,它对jdbc的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动.创建connection.创建statement.手动设置 ...

  2. 1-C++的并发世界

    1.1 何谓并发 并发的两种方式 多核机器上的真正并行 单核机器的任务切换 并发的两种途径 多进程并发 1.1 多进程并发需要通过操作系统进行进程间通信 多线程并发 2.1 多线程并发需要共享内存 1 ...

  3. EBS API及接口清单

    https://www.cnblogs.com/lizicheng/p/9521742.html 模块 应用场景 类型 API/接口 AP 付款核销 API ap_pay_invoice_pkg.ap ...

  4. Delphi ClientDataSet复制记录

    数据源记录集:ClientDataSetSource:目标记录集:ClientDataSetCopy 1)复制一条记录. ClientDataSetCopy.Close;  ClientDataSet ...

  5. 大叔学ML第三:多项式回归

    目录 基本形式 小试牛刀 再试牛刀 调用类库 基本形式 上文中,大叔说道了线性回归,线性回归是个非常直观又简单的模型,但是很多时候,数据的分布并不是线性的,如: 如果我们想用高次多项式拟合上面的数据应 ...

  6. 谈一款MOBA类游戏《码神联盟》的服务端架构设计与实现(更新优化思路)

    注:本文仅用于在博客园学习分享,还在随着项目不断更新和完善中,多有不足,暂谢绝各平台或个人的转载和推广,感谢支持. 一.前言 <码神联盟>是一款为技术人做的开源情怀游戏,每一种编程语言都是 ...

  7. C#中四步轻松使用log4net记录本地日志(WPF有点小区别)

    在这里,记录我在项目中使用log4net记录本地日志的步骤.在不会之前感觉很难,很神秘,一旦会了之后其实没那么难.其实所有的事情都是一样的,下面我就分享一下我使用log4Net的经验. 第一步:首先从 ...

  8. JavaScript笔记整理

    整理一篇工作中的JavaScript脚本笔记,不定时更新,笔记来自网上资料或者自己经验归纳. (1) 获取Url绝对路径 function getUrlRelativePath() { var url ...

  9. c++多继承多态

    C++多继承多态的实现 如果一个类中存在虚函数,在声明类的对象时,编译器就会给该对象生成一个虚函数指针,该虚函数指针指向该类对应的虚函数表. 多态的实现是因为使用了一种动态绑定的机制,在编译期间不确定 ...

  10. 剑指offer【07】- 斐波那契数列(java)

    题目:斐波那契数列 考点:递归和循环 题目描述:大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0),n<=39. 法一:递归法,不过递归比较慢, ...