WPF線段式布局的一種實現


線段式布局

有時候需要實現下面類型的布局方案,不知道有沒有約定俗成的稱呼,我個人強名為線段式布局。因為元素恰好放置在線段的端點上。

segment

實現

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)));
        }
    }
}

連線功能

端點有了,有時為了美觀,需要在端點之間添加連線功能,如下:

segment_line

該連線功能是集成在布局控件里面還是單獨,我個人傾向於單獨使用。因為本質上這是一種裝飾功能,而非布局核心功能。

裝飾功能需要添加很多屬性來控制連線,比如控制連線位置的屬性。但是因為我懶,所以我破壞了繼承自Decorator的原則。又正因為如此,我也否決了繼承自Border的想法,因為我想使用Padding屬性來控制連線位置,但是除非顯式改寫,否則Border會保留Padding的空間。最后,我選擇了ContentControl作為基類,只添加了連線大小一個屬性。連線位置是通過VerticalContentAlignment(HorizontalContentAlignment)和Padding來控制,連線顏色和粗細參考Border,但是沒有圓角功能(又是因為我懶,你來打我啊)。

連線是通過在OnRender中畫線來實現的。考慮到布局控件可能用於ItemsControl,並不是要求獨子是布局控件,只要N代碼單傳是布局控件就行。代碼就不貼了,放在代碼部分:

代碼

博客園:SegmentDemo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM