1. 前言

上一篇文章介绍了使用Resizer实现Expander简单的动画效果,运行效果也还好,不过只有展开/折叠而缺少了淡入/淡出的动画(毕竟Resizer模仿Expander只是附带的功能)。这篇继续Measure的话题,自定义了一个带有动画的ExtendedExpander。

2. ExtendedExpander的需求

使用Resizer实现的简易Expander没办法在折叠时做淡出动画,因为ControlTemplate中的ExpandSite在Collapsed状态下直接设置为隐藏。一个稍微好看些的Expander的状态改变动画要满足下面的需求:

  • 拉伸
  • 淡入淡出
  • 上面两个效果都可以用XAML定义

最终运行效果如下:

3. 实现思路

模仿SilverlightToolkit,我也用一个带有Percentage属性的ExpandableContentControl控件控制Expander内容的拉伸。(顺便一提,SilverlightToolkit的Expander没有拉伸动画,ExpandableContentControl用在AccordionItem里面)。ExpandableContentControl的Percentage属性控制这个控件的展开的百分比,1为完全展开,0为完全折叠。

在ControlTemplate中使用VisualState控制Expanded/Collapsed的动画。VusialState.Storyboard控制VisualState的最终值,过渡动画由VisualStateGroup.Transitions控制,这在以前的 这篇文章 中有介绍过:

<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="3" SnapsToDevicePixels="true">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ExpansionStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.3">
<VisualTransition.GeneratedEasingFunction>
<QuarticEase EasingMode="EaseOut"/>
</VisualTransition.GeneratedEasingFunction>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Expanded"/>
<VisualState x:Name="Collapsed">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="ExpandableContentControl">
<EasingDoubleKeyFrame KeyTime="0" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Percentage" Storyboard.TargetName="ExpandableContentControl">
<EasingDoubleKeyFrame KeyTime="0" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<DockPanel>
<ToggleButton x:Name="HeaderSite" ContentTemplate="{TemplateBinding HeaderTemplate}" ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}" Content="{TemplateBinding Header}" DockPanel.Dock="Top" Foreground="{TemplateBinding Foreground}" FontWeight="{TemplateBinding FontWeight}" FocusVisualStyle="{StaticResource ExpanderHeaderFocusVisual}" FontStyle="{TemplateBinding FontStyle}" FontStretch="{TemplateBinding FontStretch}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" IsChecked="{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" Margin="1" MinWidth="0" MinHeight="0" Padding="{TemplateBinding Padding}" Style="{StaticResource ExpanderDownHeaderStyle}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
<Primitives:ExpandableContentControl x:Name="ExpandableContentControl" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="{TemplateBinding Padding}" ClipToBounds="True">
<ContentPresenter x:Name="ExpandSite" DockPanel.Dock="Bottom" Focusable="false" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Primitives:ExpandableContentControl>
</DockPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="false">
<Setter Property="IsHitTestVisible" TargetName="ExpandableContentControl" Value="False"/>
</Trigger>
...
</ControlTemplate.Triggers>

这样Expander及它的ControlTemplate只做了最少的改动就实现了动画效果。主要的代码逻辑都交给ExpandableContentControl。

4. 实现ExpandableContentControl

ExpandableContentControl派生自ContentControl,它的Percentage属性的定义如下:

public static readonly DependencyProperty PercentageProperty =
DependencyProperty.Register(nameof(Percentage),
typeof(double),
typeof(ExpandableContentControl),
new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.AffectsMeasure));

FrameworkPropertyMetadataOptions用于定义依赖属性的行为,其中AffectsMeasure的意思是依赖属性的值改变时要求重新Measure,既然Measure了Arrange也会发生,所以这个AffectsMeasure其实就是要求重新执行两步布局。功能和上一篇文章介绍的InvalidateMeasure差不多。

在MeasureOverride里根据Percentage告诉父元素自己需要多大的空间,那么使用动画操作Percentage属性就可以实现拉伸效果:

protected override Size MeasureOverride(Size constraint)
{
int count = VisualChildrenCount;
Size childConstraint = new Size(Double.PositiveInfinity, Double.PositiveInfinity);
UIElement child = (count > 0) ? GetVisualChild(0) as UIElement : null;
var result = new Size();
if (child != null)
{
child.Measure(childConstraint);
result = child.DesiredSize;
} return new Size(result.Width * Percentage, result.Height * Percentage);
}

最后,因为没有使用Arrange限制子元素的大小,子元素的UI一定会超出范围,所以要overrid GetLayoutClip 函数控制当子元素超出自身大小时是否显示超出的部分,可以用ClipToBounds属性控制。

protected override Geometry GetLayoutClip(Size layoutSlotSize)
{
if (ClipToBounds)
return new RectangleGeometry(new Rect(RenderSize));
else
return null;
}

之后只要把ExpandableContentControl放到Expander的ControlTemplate中就大功告成了。

5. 模仿Accordion

因为实现起来太简单,内容太少,所以顺便提一下怎么模仿Accordion。

Accordion通常被翻译为手风琴?通常也就程序的左侧导航菜单会用到,用ExpandableContentControl也可以简单地模仿如下:

private void OnLoaded(object sender, RoutedEventArgs e)
{
var expanders = new List<KinoExpander>();
Expander firstExpander = null;
for (int i = 0; i < 10; i++)
{
var expander = new KinoExpander() { Header = "This is AccordionItem " + i };
if (i == 0)
firstExpander = expander; Grid.SetRow(expander, i);
var panel = new StackPanel();
panel.Children.Add(new CheckBox { Content = "Calendar" });
panel.Children.Add(new CheckBox { Content = "中国节假日" });
panel.Children.Add(new CheckBox { Content = "Birthdays" });
expander.Content = panel;
MenuRoot.Children.Add(expander);
MenuRoot.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
int index = i;
expander.Expanded += (s, args) =>
{ var lastExpander = expanders.Where(p => p.IsExpanded && p != s).FirstOrDefault();
if (lastExpander != null)
lastExpander.IsExpanded = false; MenuRoot.RowDefinitions[index].Height = new GridLength(1, GridUnitType.Star);
}; expander.Collapsed += (s, args) =>
{
if (expanders.Any(p => p.IsExpanded) == false)
{
expander.IsExpanded = true;
return;
} MenuRoot.RowDefinitions[index].Height = new GridLength(1, GridUnitType.Auto);
};
expanders.Add(expander);
} firstExpander.IsExpanded = true;
}

MenuRoot是一个空的Grid,上面这段代码用于控制MenuRoot的RowDefinitions根据当前选中的Expander变化。

最终效果如下:

6. 结语

虽然实现了Expander,但我想这种方式会影响到Expander中ScrollViewer的计算,所以最好还是不要把ScrollViewer放进Expander。

写完这篇文章才发觉可能把这篇和上一篇调换下比较好,因为这篇的Measure的用法更简单。

其实有不少方案可以实现,但为了介绍Measure搞到有点舍近求远了。例如直接用LayoutTransform就挺好的。

不过这种动画效果不怎么好看,所以很多控件库基本上都实现了自己的带动画的Expander控件,例如Telerik开源了UI for UWP控件库,里面的RadExpanderControl是个漂亮优雅的方案,应该可以轻易地移植到WPF(不过某些情况运行起来卡卡的)。

其它控件库的AccordionItem也可以实现类似的功能,可以当作Expander来用,例如Silverlight Toolkit,移植起来应该也不复杂。

另外有没有从上面ExtendedExpander的ControlTemplate感受到不换行的XAML有多烦?Blend产生的样式默认就是这样的。ExtendedExpander的XAML没有使用之前的每个属性一行的方式写,这样的好处是很容易看清楚结构,但在分辨率不高的显示器,或者在Github上根本看不到后面的属性,很容易因为看不到添加在最后的属性犯错(而且我的博客园主题,代码框里还没有滚动条)。使用哪种格式化见仁见智,这篇文章的样式因为是从别的地方复制的,既然保持了原格式就顺便用来讲解一下格式的这个问题,正好HeaderSite的ToggleButton几乎是PresentationFramework.Aero2主题里最长的一行,感受一下这有多欢乐。最终选择使用哪种方式视乎团队人员的显示器有多大,但为了博客里看起来方便我会尽量选择每个属性一行的格式。

7. 参考

Expander 概述 _ Microsoft Docs

Customizing WPF Expander with ControlTemplate - CodeProject

FrameworkPropertyMetadataOptions Enum (System.Windows) _ Microsoft Docs

FrameworkElement.MeasureOverride(Size) Method (System.Windows) Microsoft Docs.html

8. 源码

Kino.Toolkit.Wpf_Expander at master

[WPF自定义控件库]自定义Expander的更多相关文章

  1. [WPF自定义控件库]使用WindowChrome自定义RibbonWindow

    原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自 ...

  2. WPF 如何创建自己的WPF自定义控件库

    在我们平时的项目中,我们经常需要一套自己的自定义控件库,这个特别是在Prism这种框架下面进行开发的时候,每个人都使用一套统一的控件,这样才不会每个人由于界面不统一而造成的整个软件系统千差万别,所以我 ...

  3. [WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack)

    原文:[WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack) 1. 什么是滚动轮劫持# 这篇文章介绍一个很简单的继承自ScrollViewer的控件 ...

  4. [WPF自定义控件库] 让Form在加载后自动获得焦点

    原文:[WPF自定义控件库] 让Form在加载后自动获得焦点 1. 需求 加载后让第一个输入框或者焦点是个很基本的功能,典型的如"登录"对话框.一般来说"登录" ...

  5. [WPF自定义控件库] 给WPF一个HyperlinkButton

    1. 在WPF怎么在UI上添加超级链接 这篇文章的目的是介绍怎么在WPF里创建自定义的HyperlinkButton控件.很神奇的,WPF居然连HyperlinkButton都没有,不过它提供了另一种 ...

  6. [WPF自定义控件库]为Form和自定义Window添加FunctionBar

    1. 前言 我常常看到同一个应用程序中的表单的按钮----也就是"确定"."取消"那两个按钮----实现得千奇百怪,其实只要使用统一的Style起码就可以统一按 ...

  7. [WPF自定义控件库]了解如何自定义ItemsControl

    1. 前言 对WPF来说ContentControl和ItemsControl是最重要的两个控件. 顾名思义,ItemsControl表示可用于呈现一组Item的控件.大部分时候我们并不需要自定义It ...

  8. [WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画

    1. 前言 这篇文章介绍WPF UI元素的两步布局过程,并且通过Resizer控件介绍只使用Measure可以实现些什么内容. 我不建议初学者做太多动画的工作,但合适的动画可以引导用户视线,提升用户体 ...

  9. [WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题

    1. 为什么选择Aero2 除了以外观为卖点的控件库,WPF的控件库都默认使用"素颜"的外观,然后再提供一些主题包.这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分 ...

随机推荐

  1. 关于Windows更新窗口内容的问题(作为一个实验,效果很明显)

    Windows中的窗口在特定情况下会由系统进行重绘,如无效区域重新显现时,,会向窗口的处理过程发送VM_PAINT消息,但是,可能还有Windows自己的更新窗口处理,如在下面的代码中,将击键显式地转 ...

  2. 直播的本质(创业者应该要从商业模式的右边开始思考,你为用户创造了什么价值?找客户并不难,但要想办法让客户不离不弃;PC端功能的丰富很重要,因为手机版通常只是一个迷你版)

    我想稍微给直播这件事浇点冷水. 的确,直播现在越来越火,YouTube凭着良好的基础建设平台前段时间也做起了直播,Facebook Live最近也加入了变脸.预定直播时间和双人录制的功能,更不用说国内 ...

  3. 对新数据库使用 Code First

    如果使用的是 Visual Studio 2010,还需要安装 Nuget 1.创建应用程序 简单起见,我们将构建一个使用 Code First 执行数据访问的基本控制台应用程序. 打开 Visual ...

  4. 如何使用Vue.js来搭建一个后台管理系统

    目录 使用的技术 基础但不好版 1.初始化项目 2.实现初始页内容自定义 3.使用路由 原始代码 自建页面 修改路由 4.测试路由跳转 补充 子路由版 嵌套router-view 定义子路由 修改菜单 ...

  5. 整合SSM框架必备基础—SpringMVC(上)

    01 MVC概述 在Web系统开发中一般按照视图(View).模型(Model).控制(Controller)三层设计模式进行构建,视图层负责模型数据的渲染,将数据用一定的形式展现给用户:模型层负责监 ...

  6. 深入解析Hyperledger Fabric启动的全过程

    在这篇文章中,使用fabric-samples/first-network中的文件进行fabric网络(solo类型的网络)启动全过程的解析.如有错误欢迎批评指正. 至于Fabric网络的搭建这里不再 ...

  7. js与原生进行交互

    由于最近做的项目我作为web前端要和原生开发者合作,所以就去踩了踩坑. 这个功能是在h5页面上点击按钮关闭当前页面. function click_fn() { var u = navigator.u ...

  8. Scala 学习之路(五)—— 集合类型综述

    一.集合简介 Scala中拥有多种集合类型,主要分为可变的和不可变的集合两大类: 可变集合: 可以被修改.即可以更改,添加,删除集合中的元素: 不可变集合类:不能被修改.对集合执行更改,添加或删除操作 ...

  9. Spark学习之路(十四)—— Spark Streaming 基本操作

    一.案例引入 这里先引入一个基本的案例来演示流的创建:获取指定端口上的数据并进行词频统计.项目依赖和代码实现如下: <dependency> <groupId>org.apac ...

  10. ELK架构下利用Kafka Group实现Logstash的高可用

    系统运维的过程中,每一个细节都值得我们关注 下图为我们的基本日志处理架构 所有日志由Rsyslog或者Filebeat收集,然后传输给Kafka,Logstash作为Consumer消费Kafka里边 ...