WPF线段式布局的一种实现
线段式布局
有时候需要实现下面类型的布局方案,不知道有没有约定俗成的称呼,我个人强名为线段式布局。因为元素恰好放置在线段的端点上。
实现
wpf所有布局控件都直接或间接的继承自system.windows.controls. panel,常用的布局控件有canvas、dockpanel、grid、stackpanel、wrappanel,都不能直接满足这种使用场景。因此,我们不妨自己实现一个布局控件。
不难看出,该布局的特点是:最左侧朝右布局,最右侧朝左布局,中间点居中布局。因此,我们要做的就是在measureoverride和arrangeoverride做好这件事。另外,为了功能丰富,添加了一个朝向属性。代码如下:
using system; using system.linq; using system.windows; using system.windows.controls; namespace segmentdemo { /// <summary> /// 类似线段的布局面板,即在最左侧朝右布局,最右侧朝左布局,中间点居中布局 /// </summary> public class segmentspanel : panel { /// <summary> /// 可见子元素个数 /// </summary> private int _visiblechildcount; /// <summary> /// 朝向的依赖属性 /// </summary> public static readonly dependencyproperty orientationproperty = dependencyproperty.register( "orientation", typeof(orientation), typeof(segmentspanel), new frameworkpropertymetadata(orientation.horizontal, frameworkpropertymetadataoptions.affectsmeasure)); /// <summary> /// 朝向 /// </summary> public orientation orientation { get { return (orientation)getvalue(orientationproperty); } set { setvalue(orientationproperty, value); } } protected override size measureoverride(size constraint) { _visiblechildcount = this.countvisiblechild(); if (_visiblechildcount == 0) { return new size(0, 0); } double width = 0; double height = 0; size availablesize = new size(constraint.width / _visiblechildcount, constraint.height); if (orientation == orientation.vertical) { availablesize = new size(constraint.width, constraint.height / _visiblechildcount); } foreach (uielement child in children) { child.measure(availablesize); size desiredsize = child.desiredsize; if (orientation == orientation.horizontal) { width += desiredsize.width; height = math.max(height, desiredsize.height); } else { width = math.max(width, desiredsize.width); height += desiredsize.height; } } return new size(width, height); } protected override size arrangeoverride(size arrangesize) { if (_visiblechildcount == 0) { return arrangesize; } int firstvisible = 0; while (internalchildren[firstvisible].visibility == visibility.collapsed) { firstvisible++; } uielement firstchild = this.internalchildren[firstvisible]; if (orientation == orientation.horizontal) { this.arrangechildhorizontal(firstchild, arrangesize.height, 0); } else { this.arrangechildvertical(firstchild, arrangesize.width, 0); } int lastvisible = _visiblechildcount - 1; while (internalchildren[lastvisible].visibility == visibility.collapsed) { lastvisible--; } if (lastvisible <= firstvisible) { return arrangesize; } uielement lastchild = this.internalchildren[lastvisible]; if (orientation == orientation.horizontal) { this.arrangechildhorizontal(lastchild, arrangesize.height, arrangesize.width - lastchild.desiredsize.width); } else { this.arrangechildvertical(lastchild, arrangesize.width, arrangesize.height - lastchild.desiredsize.height); } int ordinarychildcount = _visiblechildcount - 2; if (ordinarychildcount > 0) { double uniformwidth = (arrangesize.width - firstchild.desiredsize.width / 2.0 - lastchild.desiredsize.width / 2.0) / (ordinarychildcount + 1); double uniformheight = (arrangesize.height - firstchild.desiredsize.height / 2.0 - lastchild.desiredsize.height / 2.0) / (ordinarychildcount + 1); int visible = 0; for (int i = firstvisible + 1; i < lastvisible; i++) { uielement child = this.internalchildren[i]; if (child.visibility == visibility.collapsed) { continue; } visible++; if (orientation == orientation.horizontal) { double x = firstchild.desiredsize.width / 2.0 + uniformwidth * visible - child.desiredsize.width / 2.0; this.arrangechildhorizontal(child, arrangesize.height, x); } else { double y = firstchild.desiredsize.height / 2.0 + uniformheight * visible - child.desiredsize.height / 2.0; this.arrangechildvertical(child, arrangesize.width, y); } } } return arrangesize; } /// <summary> /// 统计可见的子元素数 /// </summary> /// <returns>可见子元素数</returns> private int countvisiblechild() { return this.internalchildren.cast<uielement>().count(element => element.visibility != visibility.collapsed); } /// <summary> /// 在水平方向安排子元素 /// </summary> /// <param name="child">子元素</param> /// <param name="height">可用的高度</param> /// <param name="x">水平方向起始坐标</param> private void arrangechildhorizontal(uielement child, double height, double x) { child.arrange(new rect(new point(x, 0), new size(child.desiredsize.width, height))); } /// <summary> /// 在竖直方向安排子元素 /// </summary> /// <param name="child">子元素</param> /// <param name="width">可用的宽度</param> /// <param name="y">竖直方向起始坐标</param> private void arrangechildvertical(uielement child, double width, double y) { child.arrange(new rect(new point(0, y), new size(width, child.desiredsize.height))); } } }
连线功能
端点有了,有时为了美观,需要在端点之间添加连线功能,如下:
该连线功能是集成在布局控件里面还是单独,我个人倾向于单独使用。因为本质上这是一种装饰功能,而非布局核心功能。
装饰功能需要添加很多属性来控制连线,比如控制连线位置的属性。但是因为我懒,所以我破坏了继承自decorator的原则。又正因为如此,我也否决了继承自border的想法,因为我想使用padding属性来控制连线位置,但是除非显式改写,否则border会保留padding的空间。最后,我选择了contentcontrol作为基类,只添加了连线大小一个属性。连线位置是通过verticalcontentalignment(horizontalcontentalignment)和padding来控制,连线颜色和粗细参考border,但是没有圆角功能(又是因为我懒,你来打我啊)。
连线是通过在onrender中画线来实现的。考虑到布局控件可能用于itemscontrol,并不是要求独子是布局控件,只要n代码单传是布局控件就行。代码就不贴了,放在代码部分:
代码
博客园:segmentdemo