線段式佈局 有時候需要實現下麵類型的佈局方案,不知道有沒有約定俗成的稱呼,我個人強名為線段式佈局。因為元素恰好放置線上段的端點上。 實現 WPF所有佈局控制項都直接或間接的繼承自System.Windows.Controls. Panel,常用的佈局控制項有Canvas、DockPanel、Grid、S ...
線段式佈局
有時候需要實現下麵類型的佈局方案,不知道有沒有約定俗成的稱呼,我個人強名為線段式佈局。因為元素恰好放置線上段的端點上。
實現
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