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添加动画的更多相关文章

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  8. [WPF自定义控件库]简单的表单布局控件

    1. WPF布局一个表单 <Grid Width="400" HorizontalAlignment="Center" VerticalAlignment ...

  9. [WPF自定义控件库]自定义Expander

    1. 前言 上一篇文章介绍了使用Resizer实现Expander简单的动画效果,运行效果也还好,不过只有展开/折叠而缺少了淡入/淡出的动画(毕竟Resizer模仿Expander只是附带的功能).这 ...

随机推荐

  1. Kali Linux安装TL-WN821N USB无线网卡驱动(make失败)

    主要有下面几个步骤: 1.刚插上网卡,network-manager识别出来了网卡,也能搜索到WiFi,但就是连接不上.查看/var/log/syslog日志或者使用nmcli m查看网络信息,显示认 ...

  2. 将WriteableBitmap转为byte[]

    原文:将WriteableBitmap转为byte[] Win8 metro中的操作与之前的版本有所不同,因此作为一个新手,我将自己的一些问题解答记录了下来,希望与大家分享!! 下面是将Writeab ...

  3. SQLServer 复制中移除和添加发布而不初始化所有项目

    原文:SQLServer 复制中移除和添加发布而不初始化所有项目 -- 若提前"禁止架构更改",新增的列不会自动添加大发布,此时应使用 sp_articlecolumn 添加列 E ...

  4. 错误代码: 0x800700b7 配置错误:定义了重复的“system.web.extensions/scripting/scriptResourceHandler”节

    操作环境:Windows 8.1  IIS8.5 VS2008 解决方法: 把应用程序池的.Net CLR 版本从4.0修改为2.0即可.如下图所示:

  5. 算法之--字符串反转【python实现】

    题目描述 给定一个字符串,要求把字符串前面的若干个字符移动到字符串的尾部,如把字符串“abcdef”前面的2个字符'a'和'b'移动到字符串的尾部,使得原字符串变成字符串“cdefab”.请写一个函数 ...

  6. Android零基础入门第84节:引入Fragment原来是这么回事

    随着大众生活水平的提高,再加上移动互联网的迅速发展,几乎每个人都至少拥有一台搭载Android系统的移动设备.Android设备的多样性给我们带来了很大的便捷,各Android设备拥有不同分辨率和不同 ...

  7. 比快更快——微软LightGBM

    LightGBM介绍 xgboost是一种优秀的boosting框架,但是在使用过程中,其训练耗时过长,内存占用比较大.微软在2016年推出了另外一种boosting框架--lightgbm,在不降低 ...

  8. linux-deployment

    官方 linux-deploymenthttp://doc.qt.io/qt-5/linux-deployment.html linuxdeployqthttps://github.com/probo ...

  9. Android消息机制架构和源码解析

    http://wangkuiwu.github.io/2014/08/26/MessageQueue/

  10. Java代码消除switch/case,if/else语句的几种实现方式

    转自:https://my.oschina.net/stefanzhlg/blog/372413 我们在平时的编码中,我们经常会遇到这样的情况: 使用过多的switch/case 或者 if else ...