[WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画
1. 前言
这篇文章介绍WPF UI元素的两步布局过程,并且通过Resizer控件介绍只使用Measure可以实现些什么内容。

我不建议初学者做太多动画的工作,但合适的动画可以引导用户视线,提升用户体验。例如上图的这种动画,这种动画挺常见的,在内容的高度改变时动态地改变自身的高度,除了好看以外,对用户体验也很有改善。可惜的是WPF本身没有默认这种这方面的支持,连Expander的展开/折叠都没有动画。为此我实现了一个可以在内容大小改变时以动画的方式改变自身大小的Resizer控件(想不到有什么好的命名,请求建议)。其实老老实实从Silverlight Toolkit移植AccordionItem就好,但我想通过这个控件介绍一些布局(及动画)的概念。Resizer使用方式如下XAML所示:
<StackPanel>
<kino:KinoResizer HorizontalContentAlignment="Stretch">
<Expander Header="Expander1">
<Rectangle Height="100"
Fill="Red" />
</Expander>
</kino:KinoResizer>
<kino:KinoResizer HorizontalContentAlignment="Stretch">
<Expander Header="Expander2">
<Rectangle Height="100"
Fill="Blue" />
</Expander>
</kino:KinoResizer>
</StackPanel>
2. 需要了解的概念
为了实现这个控件首先要了解WPF UI元素的布局过程。
2.1 两步布局过程
WPF的布局大致上分为Measure和Arrange两步,布局元素首先递归地用Measure计算所有子元素所需的大小,然后使用Arrange实现布局。
以StackPanel为例,当StackPanel需要布局的时候,它首先会得知有多少空间可用,然后用这个可用空间询问Children的所有子元素它们需要多大空间,这是Measure;得知所有子元素需要的空间后,结合自身的布局逻辑将子元素确定实际尺寸及安放的位置,这是Arrange。
当StackPanel需要重新布局(如StackPanel的大小改变),这时候StackPanel就重复两步布局过程。如果StackPanel的某个子元素需要重新布局,它也会通知StackPanel需要重新布局。
2.2 MeasureOverride
MeasureOverride在派生类中重写,用于测量子元素在布局中所需的大小。简单来说就是父元素告诉自己有多少空间可用,自己再和自己的子元素商量后,把自己需要的尺寸告诉父元素。
2.3 DesiredSize
DesiredSize指经过Measure后确定的期待尺寸。下面这段代码演示了如何使用MeasureOverride和DesiredSize:
protected override Size MeasureOverride(Size availableSize)
{
Size panelDesiredSize = new Size();
// In our example, we just have one child.
// Report that our panel requires just the size of its only child.
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
panelDesiredSize = child.DesiredSize;
}
return panelDesiredSize ;
}
2.4 InvalidateMeasure
InvalidateMeasure使元素当前的布局测量无效,并且异步地触发重新测量。
2.5 IsMeasureValid
IsMeasureValid指示布局测量返回的当前大小是否有效,可以使用InvalidateMeasure使这个值变为False。
3. 实现
Resizer不需要用到Arrange,所以了解上面这些概念就够了。Resizer的原理很简单,Reszier的ControlTemplate中包含一个ContentControl(InnerContentControl),当这个InnerContentControl的大小改变时请求Resizer重新布局,Resizer启动一个Storyboard,以InnerContentControl.DesiredSize为最终值逐渐改变Resizer的ContentHeight和ContentWidth属性:
DoubleAnimation heightAnimation;
DoubleAnimation widthAnimation;
if (Animation != null)
{
heightAnimation = Animation.Clone();
Storyboard.SetTarget(heightAnimation, this);
Storyboard.SetTargetProperty(heightAnimation, new PropertyPath(ContentHeightProperty));
widthAnimation = Animation.Clone();
Storyboard.SetTarget(widthAnimation, this);
Storyboard.SetTargetProperty(widthAnimation, new PropertyPath(ContentWidthProperty));
}
else
{
heightAnimation = _defaultHeightAnimation;
widthAnimation = _defaultWidthAnimation;
}
heightAnimation.From = ActualHeight;
heightAnimation.To = InnerContentControl.DesiredSize.Height;
widthAnimation.From = ActualWidth;
widthAnimation.To = InnerContentControl.DesiredSize.Width;
_resizingStoryboard.Children.Clear();
_resizingStoryboard.Children.Add(heightAnimation);
_resizingStoryboard.Children.Add(widthAnimation);
ContentWidth和ContentHeight改变时调用InvalidateMeasure()请求重新布局,MeasureOverride返回ContentHeight和ContentWidth的值。这样Resizer的大小就根据Storyboard的进度逐渐改变,实现了动画效果。
protected override Size MeasureOverride(Size constraint)
{
if (_isResizing)
return new Size(ContentWidth, ContentHeight);
if (_isInnerContentMeasuring)
{
_isInnerContentMeasuring = false;
ChangeSize(true);
}
return base.MeasureOverride(constraint);
}
private void ChangeSize(bool useAnimation)
{
if (InnerContentControl == null)
{
return;
}
if (useAnimation == false)
{
ContentHeight = InnerContentControl.ActualHeight;
ContentWidth = InnerContentControl.ActualWidth;
}
else
{
if (_isResizing)
{
ResizingStoryboard.Stop();
}
_isResizing = true;
ResizingStoryboard.Begin();
}
}
用Resizer控件可以简单地为Expander添加动画,效果如下:

最后,Resizer还提供DoubleAnimation Animation属性用于修改动画,用法如下:
<kino:KinoResizer HorizontalContentAlignment="Stretch">
<kino:KinoResizer.Animation>
<DoubleAnimation BeginTime="0:0:0"
Duration="0:0:3">
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</kino:KinoResizer.Animation>
<TextBox AcceptsReturn="True"
VerticalScrollBarVisibility="Disabled" />
</kino:KinoResizer>
4. 结语
Resizer控件我平时也不会单独使用,而是放在其它控件里面,例如Button:

由于这个控件性能也不高,以后还可能改进API,于是被放到了Primitives命名空间。
很久很久以前常常遇到“布局循环”这个错误,这常常出现在处理布局的代码中。最近很久没遇到这个错误,也许是WPF变健壮了,又也许是我的代码变得优秀了。但是一朝被蛇咬十年怕草绳,所以我很少去碰Measure和Arrange的代码,我也建议使用Measure和Arrange要慎重。
5. 参考
FrameworkElement.MeasureOverride(Size) Method (System.Windows) Microsoft Docs.html
UIElement.DesiredSize Property (System.Windows) Microsoft Docs.html
UIElement.InvalidateMeasure Method (System.Windows) Microsoft Docs
UIElement.IsMeasureValid Property (System.Windows) Microsoft Docs
6. 源码
Kino.Toolkit.Wpf_Resizer at master
[WPF自定义控件库]了解WPF的布局过程,并利用Measure为Expander添加动画的更多相关文章
- [WPF自定义控件库] 给WPF一个HyperlinkButton
1. 在WPF怎么在UI上添加超级链接 这篇文章的目的是介绍怎么在WPF里创建自定义的HyperlinkButton控件.很神奇的,WPF居然连HyperlinkButton都没有,不过它提供了另一种 ...
- [WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack)
原文:[WPF自定义控件库] 关于ScrollViewer和滚动轮劫持(scroll-wheel-hijack) 1. 什么是滚动轮劫持# 这篇文章介绍一个很简单的继承自ScrollViewer的控件 ...
- WPF 如何创建自己的WPF自定义控件库
在我们平时的项目中,我们经常需要一套自己的自定义控件库,这个特别是在Prism这种框架下面进行开发的时候,每个人都使用一套统一的控件,这样才不会每个人由于界面不统一而造成的整个软件系统千差万别,所以我 ...
- [WPF自定义控件库]使用WindowChrome自定义RibbonWindow
原文:[WPF自定义控件库]使用WindowChrome自定义RibbonWindow 1. 为什么要自定义RibbonWindow 自定义Window有可能是设计或功能上的要求,可以是非必要的,而自 ...
- [WPF自定义控件库] 让Form在加载后自动获得焦点
原文:[WPF自定义控件库] 让Form在加载后自动获得焦点 1. 需求 加载后让第一个输入框或者焦点是个很基本的功能,典型的如"登录"对话框.一般来说"登录" ...
- [WPF自定义控件库]以Button为例谈谈如何模仿Aero2主题
1. 为什么选择Aero2 除了以外观为卖点的控件库,WPF的控件库都默认使用"素颜"的外观,然后再提供一些主题包.这样做的最大好处是可以和原生控件或其它控件库兼容,而且对于大部分 ...
- [WPF自定义控件库]为Form和自定义Window添加FunctionBar
1. 前言 我常常看到同一个应用程序中的表单的按钮----也就是"确定"."取消"那两个按钮----实现得千奇百怪,其实只要使用统一的Style起码就可以统一按 ...
- [WPF自定义控件库]简单的表单布局控件
1. WPF布局一个表单 <Grid Width="400" HorizontalAlignment="Center" VerticalAlignment ...
- [WPF自定义控件库]自定义Expander
1. 前言 上一篇文章介绍了使用Resizer实现Expander简单的动画效果,运行效果也还好,不过只有展开/折叠而缺少了淡入/淡出的动画(毕竟Resizer模仿Expander只是附带的功能).这 ...
随机推荐
- 【Python】wifi开关测试
#!/usr/bin/python # -*- coding: UTF-8 -*- import os import time def find_device(): os.system('adb ki ...
- /\B(?=(?:\d{3})+$)/g 一条令人费解的正则表达式
网上浏览博客看到要用JavaScript正则表达式解决一个功能, 要在数字中间插入逗号, 用来表示书面的金额写法. JS代码是这样子的 let test1 = '1234567890' let for ...
- 建立Linux计划命令crontab
crontab翻译:排程,命令 crontab从输入设备输入命令,并将其放入crontab文件,供守护进程crond读取并执行,crond在后台每一分钟执行一次 crontab -e:创建计划命令,进 ...
- delphi Stomp客户端连接 RabbitMQ(1)
最近公司想上个消息推送系统,网上搜了很多,因公司主要产品是Delphi,我选择了开源的RabbitMQ,Erlang语言开发,天生并行. 代码下载地址:delphistomp下载地址 windows上 ...
- file.delete()与file.deleteOnExit(); 的区别
file.delete() //删除文件,删除的是创建File对象时指定与之关联创建的那个文件.这是一个立刻执行的操作 file.deleteOnExit(); //在JVM进程退出的时候删除 ...
- c# winform快捷键实现
我们在软件中经常用到快捷键,这里整理备份一下. 首先我们要定义可以作为快捷键的按键,以下是整理的 一些,自己可以根据情况来修改 public static Dictionary<int, str ...
- VirtualTreeView控件
很好用的一个列表控件,可以用来代替Delphi自带的ListView和TreeView,而且也一直在更新,目前已经支持最新的XE2 官方网站:http://www.soft-gems.net SVN地 ...
- C++实现半透明按钮控件(PNG,GDI+)
http://blog.csdn.net/witch_soya/article/details/6889904
- Layui 是一款采用自身模块规范编写的国产前端UI框架(5600个Star)
采用自身模块规范编写的前端UI框架,遵循原生HTML/CSS/JS的书写形式,极低门槛,拿来即用. http://www.layui.com Layui 是一款采用自身模块规范编写的国产前端UI框架, ...
- 【canvas】基础练习二 文字
demo1 fillText strokeText绘制文字 <!DOCTYPE html> <html lang="en"> <head> &l ...