原文:【全面解禁!真正的Expression Blend实战开发技巧】十一章 全面解析布局(Grid & Canvas &StackPanel &Wrappanel)
写这篇文章前,特意在百度搜索了一下,发现目前网上介绍布局的文章不多,质量也不是很高。拿grid和canvas来讲,这两个布局容器还是有许多小细节值得讲的,如果你不了解的话,开发中经常会遇到一些让人匪夷所思的事情。学习silverlight xaml的过程可以分为布局,样式,模板,资源,动画,行为,绑定等几个知识点。我个人觉得布局是最难熟练掌握的。
布局,既是基础中的基础,也是难点中的难点:
1、无法复用:模板,样式,上手虽难,但因为控件的基本结构是不变的,当你学会为button定义样式后,以后都不会在为button的样式发愁,但是布局不同。几乎没有一个项目的布局是完全一样的。
2、标准不一:表面上看起来一摸一样的布局,可能实现的细节完全不同。大多数情况下而你很难用一个标准去规范或者界定,什么时候,应怎样做。
3、布局不是一堆控件的堆叠,而是合理的组织,但如何合理的组织,这个经验需要从众多项目中磨练才能具备。
4、随着需求的变化,最容易产生变动的就是布局。
5、你没办法为布局去写注释,所以一个层次很深很复杂的布局,维护成本就特别高。比如让你修改一个一年以前你写的项目,还要要彻底改变布局结构的时候,即使是我自己写的,我也很难看懂。
6、客户对silverlight期待很高,希望界面能够创新,不走寻常路,这样你就可能遇到一些非常规的布局,他们奇特,狂野,而从使用的角度讲,还必须要健壮,有一定的适应性。比如一个异型,镂空,半透明,带阴影的窗体设计,也必须能正确的适应浏览器宽高的变化,能够完整显示所有信息,描述信息能够正确的换行等等。我遇到过的最夸张的需求,客户有一台只支持1024*768分辨率的投影机,和一个分辨率4000:1000(具体数字记不清楚了) 这种宽高比超级特殊的LED墙。他希望你的软件既可以在1024*768下完全显示,又要在LED下可以充满全屏且不变形,而且距离LED十米开发也要能看清楚你界面上的每一个字。
7、每次修改布局,都会对他周围甚至整体产生影响,必须非常全面的观察和测试。
8、设计布局前还要充分考虑动画设计。
9、还有一些特殊情况,你无法在blend中看到设计视图。这个时候只能用一行一行看代码,在脑子里还原程序的轮廓。
什么是好的布局?根据我个人的经验,在满足设计需求的前提下,一个好的布局,还需要考虑以下一些方面,
1、结构简单
2、分层清晰
3、层次不要太深
3、相对独立
4、低耦合
5、自适应性强
6、使动画实现更简单
上面几点不分先后和优先级,你需要根据项目的实际需求和设计以及开发周期,权衡他们的比重。
详解Grid
Grid的最大特点是他可以向四个方向(HorizontalAlignment,VerticalAlignment)填充Usercontrol。同时,他的子元素也可以向四个方向填充他们自己。Grid中所有的子元素都是用margin进行相对定位的(这个相对指相对于父容器Grid)。
有几种情况需要特殊注意:
1、当我们没有HorizontalAlignment和VerticalAlignment的默认值就是"Stretch",换句话说如果我们这样写<Rectangle Fill="#FF701313"/>,虽然没有提到明确指定HorizontalAlignment和VerticalAlignment,但此时Rectangle的水平和垂直的填充方式默认就是Stretch。
2、请不要忽略margin的细微差别。如下面两张图片中,这个矩形的宽度,对齐方式都是左对齐,上下填充对齐,唯一的区别是margin.right的值一个为88,另一个为0。
可如果你此时改变这个矩形父容器的大小,就可以看出两者的区别。其中第一个永远和他父容器的右边保持88的间距。
Grid的一个另外一个特点就如他名字一样,你可以在grid中根据自己的需要定义多个行和列来构成你需要的网格。
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="210"/>
<ColumnDefinition Width="Auto" MinWidth="244"/>
</Grid.ColumnDefinitions>
</Grid>
上面的图中是一个最基本的Grid,他分为三列。每一列的宽度都不同,请注意图中的三个图标,他们分别代表了三种定义列宽度的方式。
称为"Star",对应第一行绿色代码, <ColumnDefinition Width="*"/> ,一颗*在本例中代表的第一列的宽度为LayoutRoot的实际宽度 减去 210 再减去 244后剩下的全部宽度。同时因为LayoutRoot默认是跟随浏览器大小而改变的,所以第一列的*代表的宽度总是不固定的。另外你还以得知star在“Star-Pixel-auto“中优先级最低,他总是最后计算得来的。
称为"Pixel", 对应第二行绿色代码,<ColumnDefinition Width=""/>,这个是最好理解的,第二列的宽度永远都是210像素。永恒不变。
称为"Auto", 对应第三行绿色代码、<ColumnDefinition Width="Auto" MinWidth="244"/>,第三列的宽度为自动,同时因为我们指定了另外一个属性,最小宽度MinWidth="244",所以得到了如上图中看到的效果。假如我们删掉MinWidth="244",这行代码,在当前情况下,我们没有在第三列放任何控件,那么第三列的宽度为0。而当我们在第三列放入一个或多个元素时,这一列的宽度由这一列所包含子元素中,自身宽度加margin.left和margin.right的总和最大的哪个元素来决定。
如下图所示
第三列的此时的宽度是受灰色矩形影响的而不是红色圆形,因为灰色矩形的宽度为100 + 40(margin.left) + 40(margin.right),所以第三列此时的宽度是180,而红色圆形是居中对齐的,同时它的宽度只有60,远远小于矩形,所以他不会影响第三列的宽度。但如果我们此时设置他的margin.right为200,由于60+200>100+40+40,那么第三列的宽度会变成260。
下面我们做几个简单的算术题来巩固一下刚才学的知识:
第一题,下面代码定义的列,每一列的宽度是多少?
答案:宽度100,分为5列,每一列的宽度为20。
第二题,下面代码定义的列,每一列的宽度是多少?
答案,第一列宽度为2*,换句话说此时宽度100的Grid,我们首先要将它分为6分,第一列站2分,其列各占一份,因为会出现除不尽的情况,所以5列的宽度依次为33,17,17,17,16。(注意:grid会自动根据某种规律对除不尽进行四舍五入,所以最后一列宽度是16)
第三题,下面代码定义的列,每一列的宽度是多少?
答案,一个宽度为100的Gird,他的第三列宽度为pixel =50, 而其他的为"star",所以所有为"Star"的列平分100-50后剩下的宽度,也就是50/4。所以5列的宽度依次是13,12,50,13,12。
第四题,下面代码定义的列,每一列的宽度是多少?
答案,当出现三种标记混合时,请首先区分出他们的优先级,pixel > auto > star, 所以我们可以得知,所有定义为star的列的宽度的总和为 100-30-auto,即70-auto. 我们再假设我们不在第二列(width=auto)放任何元素,那么第二列宽度就是0,所以所有定义为star的列的宽度的总和为70。然后我们再来计算一下,上面代码中出现了多少次"star", 1*和*写法不同,但效果完全一样,都记做1个"Star",所以我们可以得到1+2+3+4+1+1+1一共13颗star。那么每颗"Star"的宽度为70/13约为5.384...。所以宽度为100的 Grid,9列的宽度依次为5,0,11,30,16,22,5,6,5。
关于Grid中的位置动画:Gird中所有的元素定位都是用margin属性,而这个margin属性是Object类型,silverlight 中 ObjectAnimation是不会自动插值的,如果你对margin做向左移动的来改变子元素的位置,会得到一个意外的不流畅的动画(子元素从A点直接跳到B点)。当然也有解决办法,如果你一定要在Gird中位移某个元素,可以使用CompositeTransform的属性来做动画,CompositeTransform的属性不是object类型,所以动画支持自动插值。
但如果你非要对margin做动画,而且还要流畅显示的话,就要自定义一个double类型的依赖属性,然后专门针对这个属性写一个动画,当这个属性改变时,在他的属性改变事件里将它与子元素的margin.left关联。这样也可以实现流畅的动画。但这种做法效率很低,不建议大量使用。
Canvas详解:
Canvas直译为画布,他的最大特点是,Canvas中所有子元素定位的方式都是用过Canvas.Left和Canvas.top来进行的(我们可以把Canvas.left理解为x,Canvas.top理解为y)所以Canvas中的子元素永远是相对于Canvas的起点,也就是(0,0点)进行绝对定位。不管Canvas的宽高今后变成无穷大,还是0,他的子元素永远不会改变位置。
Canvas的另一个特点是,在Canvas中的所有子元素,指定对齐方式(HorizontalAlignment,VerticalAlignment)不会产生任何作用。
另外,在Canvas中也是可以使用margin的(感谢kklldog网友指正)。
Stackpanel详解:
关于StackPanel有两个特点,第一是子元素的排列方式永远是流式的(从左到右,或者从上到下),这点我就不多介绍了。
第二个特点十分特殊,就是Stackpanel具有截断的特性:
如何理解截断特性:
我们可以把截断理解为是stackpanel内部的一个bool变量。默认为false,当满足某条件时,自动变成true;
当截断=false时我们能做出如下图所示的效果:
图中Stackpanel的背景色为黑色,他里面放了一个Canvas,Canvas中放了一个红色的矩形,此时只需要设置Canvas.left属性,就可以让红色的矩形移动到黑色stackpanel之外。在做一些特效动画时,可能需要出现子元素移动到父容器之外的效果,此时你可以使用Canvas的这个特性。
除此之外,在StackPanel"截断=false"时,其实直接改变Stackpanel中子元素的CompositeTransform也可以达到这个效果,无需在每个元素的外面都包裹一层Canvas。如下图所示:
这两种移动出StackPanel方法各有利弊,特点分明。可以根据开发中的实际情况来权衡到底使用哪种方法。
何时会发生截断?截断会有哪些影响?
当StackPanel水平排列子元素时,所有子元素的宽度总和大于StackPanel宽度时,会发生截断。
当StackPanel垂直排列子元素时,所有子元素的高度总和大于StackPanel高度时,会发生截断。
截断的影响,请看下图:
图中我们虽然指定了Stackpanel中子元素的CompositeTransform的TranslateX=176。但由于StackPanel是垂直排列的,三个矩形的高度总和超过的StackPanel的高度,所以此时发生了截断。就如你看到的,此时你永远不可能实现子元素移出父容器的效果了。
WrapPanel详解:
WrapPanel和StackPanel一样,也会发生“截断”,并且截断的时机是完全相同的。除此之外,当Wrappanel的Orientation=Horizonta时,子元素默认先从左向右排列,当子元素的宽度总和大于Wrappanel的宽度时,子元素自动向下换行。当Orientation=Vertical时,子元素默认先从上到下排列,当子元素的高度总和大于Wrappanel的高度时,子元素自动向右换行。
制空权详解:
在所有的容器中的子元素,都涉及到制空权问题,如下图所示:
在同一个容器中,三个大小相同的矩形彼此相互覆盖,制空权最高的为什么是绿色?从代码中我们可以看出,因为名为green的矩形是最后一个声明的。所以我们可以得出,在同一个容器中,不指定Canvas.ZIndex的前提下,所有元素制空权的优先级是由他们在xaml代码中声明的顺序决定的。由于编译器解析xaml代码时是按照自上而下的顺序解析,所以后实例化的对象总会获得更高的制空权。
图中左下角红色框中的按钮名为"arrange by Z order",他表示在当前的object and time line窗口中所有元素按照制空权的降序排序,点击一下这个按钮后,所有的元素会按照制空权的降序升序排序,但这不会改变任何代码和界面效果,仅仅是改变了object and time line窗口中元素的显示顺序。
在任何容器中(无论grid,canvas,stackpanel,wrappanel等等),你都可以通过Canvas.ZIndex来重新定制制空权。例如:
按照上面代码来重新定制制空权后,红色矩形会获得最高的制空权,绿色最低。Canvas.Zindex的是int32类型,所以他的取值可以是负数。
<Grid x:Name="LayoutRoot" Background="White">
<Rectangle x:Name="red" Canvas.ZIndex="3" Fill="#FF701313" Stroke="Black" Margin="55,83,0,81" Width="417" HorizontalAlignment="Left"/>
<Rectangle x:Name="blue" Canvas.ZIndex="2" Fill="#FF1F93FF" Stroke="Black" Margin="135,83,0,81" Width="417" HorizontalAlignment="Left"/>
<Rectangle x:Name="green" Canvas.ZIndex="1" Fill="#FF00A37A" Stroke="Black" Margin="215,83,0,81" Width="417" HorizontalAlignment="Left"/>
</Grid>
下面我们再来做一道题来检查一下大家的学习情况。
如上图所示,我们有五个矩形,分别为蓝(zindex50),黄(zindex80),绿(zindex120),黑(zindex20),红(zindex-100),那么他们的制空权优先级是如何的呢?
答案是:black>green>yello>blue>red 。得到这个结果的依据是,首先LayoutRoot的第一级子元素是firstGird,secondGrid和Red。他们之间的制空权关系是SecondGrid>FirstGrid>Red,(因为red的zindex=-100),所以secondGrid的制空权最高。而在这个基础上,虽然green的zindex为120远远大于black的zindex,可由于secondGrid的制空权高于firstGird,所以black依然会覆盖blue yello green。
结果如下图所示:
补充一点,不管什么样的容器,grid,canvas,stackpanel,wrappanel等等他们之间以及他们子元素之间的制空权关系都和上文中介绍的是一摸一样的,没有任何区别。
总结
没有万金油的布局方法,简单的几个布局可以有千千万万种组合,就像7个音符可以组合出无数中音乐一样,希望大家能活学活用,总结自己的布局流派和风格。
我自己的布局使用习惯
估计会有人问这个问题,先声明我的做法不是标准,仅仅是一种使用习惯。我最常用布局的就是Grid,其次是StackPanel(在构建表单的时候我个人更喜欢用StackPanel嵌套StackPanel的方法,而不是用Grid画表格,因为我觉得这样在Object and timeline视图里看过去层次非常清晰),我在一个项目中Canvas和wrappanel使用次数非常的少,只有当非常明确用途,而且确定必须使用Canvas时我才会使用。另外还有一种情况我会使用Canvas,就是直接从AI文件导入一些矢量图形时,往往我会先将这些矢量图形分组,分块,每组都用Canvas包裹。好处是Canvas即使改变大小或者形状,也不会造成矢量图形变形。
另外除非要对grid,canvas或者其他静态元素做动画,行为或者需要在后台代码做控制,否则我不会为他们命名。(静态元素通常指logo,花边,装饰,大多数情况下一些grid和canvas也属于静态元素)这样可以使我在vs中语法提示树看起来非常干净,同时编译后xap文件也会小一些。
除此之外,当你从Blend中拖拽一个控件到grid中时,你会发现他的宽度往往都是相对的,此时我都会把宽高换成绝对高度,然后去掉所有自动生成margin属性,只有当非常明确需要让控件呈现相对宽高时,我才会使用margin。虽然这么做有点累,但这是一个良好的习惯,他可以确保你的界面总是严格的按照你的预期呈现各种效果,你甚至可以从这点上轻易区分出一个界面是由程序员做的还是美工做的。