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. Win8Metro(C#)数字图像处理--2.17图像木刻效果

    原文:Win8Metro(C#)数字图像处理--2.17图像木刻效果  [函数名称] 图像木刻效果函数WoodCutProcess(WriteableBitmap src) [函数代码] ///& ...

  2. Win10《芒果TV》商店版跻身Windows商店《热门免费应用》前12强

    2017立春上班的第一天,让人惊喜的好日子,春节过后,Win10<芒果TV>商店版跻身Windows商店<热门免费应用>前12强,露出尖尖头,这个来自广大用户和合作伙伴们一直以 ...

  3. win10 uwp 获得Slider拖动结束的值

    原文:win10 uwp 获得Slider拖动结束的值 本文讲的是如何获得Slider移动结束的值,也就是触发移动后的值.如果我们监听ValueChanged,在我们鼠标放开之前,只要拖动不放,那么就 ...

  4. Cookieless.js —— 无需 Cookie 实现访客跟踪

    直击现场 https://github.com/Colex/node-cookieless Cookieless.js 是一个轻量级的使用 Etag 进行访客跟踪的 Node.js 扩展库.使用该库无 ...

  5. CentOS7 无法使用yum命令,无法更新解决方法

    前言 设置网卡开机自动启动 设置国内dns服务器系统 修改CentOS-Base.repo中的地址 所参考的文章地址 前言 刚安装完的CentOS7的系统,发现无法使用yum命令进行更新,在更新的时候 ...

  6. 再谈Delphi关机消息拦截 -- 之控制台程序 SetConsoleCtrlHandler(控制台使用回调函数拦截,比较有意思)

    这里补充一下第一篇文章中提到的拦截关机消息 Delphi消息拦截:http://blog.csdn.net/cwpoint/archive/2011/04/05/6302314.aspx 下面我再介绍 ...

  7. play框架之模板

    现在网站发展日新月异,网页上显示的东西越来越复杂,看看HTML源码就知道,这东西不是正常人能拼出来的.因此模板应运而生,自我感觉,好的模板应该支持一下功能: 1.支持HTML代码段的复用,即在HTML ...

  8. Android WebView设置背景透明

    Adndroid 2.x的设置 在Android 2.x下,设置webview背景为透明的方法: wvContent.setBackgroundColor(0); Adndroid 4.0 由于硬件加 ...

  9. U盘刻录kali linux启动盘提示找不到镜像解决方案

    选择“继续”后会来到步骤菜单,选择从shell启动,命令 df -m 查看当前磁盘挂载情况,看到  /media 目录 输入命令 umount /media 进行挂载然后输入 exit 退出

  10. Go 程序是怎样跑起来的

    目录 引入 编译链接概述 编译过程 词法分析 语法分析 语义分析 中间代码生成 目标代码生成与优化 链接过程 Go 程序启动 GoRoot 和 GoPath Go 命令详解 go build go i ...