Saturday, October 04, 2008

Radial Panel

In Applications = Code + Markup: A Guide To The Microsoft Windows Presentation Foundation, Charles Petzold showed us how to create a radial panel. A radial panel presents its children elements around the circumference of a circle. While good, the code did not address the case when there is only 1 child, causing it to behave oddly. There are also some bugs in painting the pie lines, with the lines cutting the elements instead of drawing along the sides. So I corrected the bugs and handled the edge case. But what's the use of such a fanciful radial panel? Personally, I used it to make a Firefox style busy icon. It can be used to make the pie menu too, though some code changes is needed to rotate the icons back. I believe you have much more creative idea up in your heads! Here comes the code

//-----------------------------------------------------------------------

// <copyright file="RadialPanel.cs" company="Jeow Li Huan">

// Copyright (c) Jeow Li Huan. All rights reserved.

// </copyright>

//-----------------------------------------------------------------------

 

using System;

using System.ComponentModel;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Media;

 

namespace TetonWhitewaterKayak.WinUI

{

    /// <summary>

    ///   Arranges child elements along the circumference of a circle.

    /// </summary>

    public class RadialPanel : Panel

    {

        /// <summary>

        ///   Identifies the RadialPanel.Orientation dependency property.

        /// </summary>

        public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(

            "Orientation",

            typeof(Orientation),

            typeof(RadialPanel),

            new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure));

 

        /// <summary>

        ///   Identifies the RadialPanel.ForegroundProperty dependency property.

        /// </summary>

        public static readonly DependencyProperty ForegroundProperty = TextElement.ForegroundProperty.AddOwner(typeof(RadialPanel), new FrameworkPropertyMetadata(SystemColors.ControlTextBrush, FrameworkPropertyMetadataOptions.Inherits));

 

        /// <summary>

        ///   Backing field for the ShowPieLines property.

        /// </summary>

        private bool showPieLines;

 

        /// <summary>

        ///   The angle of the slice that each child occupies.

        /// </summary>

        private double angleEach;

 

        /// <summary>

        ///   Size of the largest child.

        /// </summary>

        private Size sizeLargest;

 

        /// <summary>

        ///   Radius of the circle that surrounds the children.

        /// </summary>

        private double radius;

 

        /// <summary>

        ///   The distance from the top of any child to the center of the circle.

        /// </summary>

        private double outerEdgeFromCenter;

 

        /// <summary>

        ///   The distance from the bottom of any child to the center of the circle.

        /// </summary>

        private double innerEdgeFromCenter;

 

        /// <summary>

        ///   Gets or sets a brush that describes the foreground color. This is a dependency property.

        /// </summary>

        [Category("Appearance")]

        [Bindable(true)]

        public Brush Foreground

        {

            get { return (Brush)GetValue(ForegroundProperty); }

            set { SetValue(ForegroundProperty, value); }

        }

 

        /// <summary>

        ///   Gets or sets a value that indicates whether child elements span the circumference of the panel by their widths or by their heights.

        ///   This is a dependency property.

        /// </summary>

        [Category("Layout")]

        public Orientation Orientation

        {

            get { return (Orientation)GetValue(OrientationProperty); }

            set { SetValue(OrientationProperty, value); }

        }

 

        /// <summary>

        ///    Gets or sets a value indicating whether to draw lines along the circumference and

        ///    along the spooks that separates the child elements.

        /// </summary>

        [Category("Appearance")]

        [DefaultValue(false)]

        public bool ShowPieLines

        {

            set

            {

                if (this.showPieLines != value)

                    this.showPieLines = value;

                this.InvalidateVisual();

            }

 

            get

            {

                return this.showPieLines;

            }

        }

 

        /// <summary>

        ///   Measures the child elements of a RadialPanel in anticipation

        ///   of arranging them during the RadialPanel.ArrangeOverride(System.Windows.Size)

        ///   pass.

        /// </summary>

        /// <param name="sizeAvailable">

        ///   An upper limit <see cref="T:System.Windows.Size"/> that should not be exceeded.

        /// </param>

        /// <returns>

        ///   The <see cref="T:System.Windows.Size"/> that represents the desired size of the element.

        /// </returns>

        protected override Size MeasureOverride(Size sizeAvailable)

        {

            if (this.InternalChildren.Count == 0)

                return new Size();

 

            this.angleEach = 360.0 / this.InternalChildren.Count;

            this.sizeLargest = new Size();

 

            foreach (UIElement child in this.InternalChildren)

            {

                // Call Measure for each child ...

                child.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));

 

                // Examine DesiredSize property of child.

                this.sizeLargest.Width = Math.Max(this.sizeLargest.Width, child.DesiredSize.Width);

                this.sizeLargest.Height = Math.Max(this.sizeLargest.Height, child.DesiredSize.Height);

            }

 

            if (this.InternalChildren.Count == 1)

            {

                double diagonal = Math.Sqrt((this.sizeLargest.Width * this.sizeLargest.Width) + (this.sizeLargest.Height * this.sizeLargest.Height));

                return new Size(diagonal, diagonal);

            }

 

            double halfLargestSpan;

            double largestHeight;

            if (this.Orientation == Orientation.Horizontal)

            {

                halfLargestSpan = this.sizeLargest.Width / 2.0;

                largestHeight = this.sizeLargest.Height;

            }

            else

            {

                halfLargestSpan = this.sizeLargest.Height / 2.0;

                largestHeight = this.sizeLargest.Width;

            }

 

            // Calculate the distance from the center to element edges.

            this.innerEdgeFromCenter = (this.InternalChildren.Count == 2) ? 0.0 : halfLargestSpan / Math.Tan(this.angleEach * Math.PI / 360.0);

            this.outerEdgeFromCenter = this.innerEdgeFromCenter + largestHeight;

 

            // Calculate the radius of the circle based on the largest child.

            this.radius = Math.Sqrt((halfLargestSpan * halfLargestSpan) + (this.outerEdgeFromCenter * this.outerEdgeFromCenter));

 

            // Return the size of that circle.

            return new Size(2.0 * this.radius, 2.0 * this.radius);

        }

 

        /// <summary>

        ///   Arranges the content of a RadialPanel element.

        /// </summary>

        /// <param name="sizeFinal">

        ///   The <see cref="T:System.Windows.Size"/> that this element should use to arrange its child elements.

        /// </param>

        /// <returns>

        ///   The <see cref="T:System.Windows.Size"/> that represents the arranged size of this RadialPanel

        ///   element and its child elements.

        /// </returns>

        protected override Size ArrangeOverride(Size sizeFinal)

        {

            if (this.InternalChildren.Count == 0)

                return sizeFinal;

            if (this.InternalChildren.Count == 1)

            {

                UIElement child = this.InternalChildren[0];

                child.RenderTransform = Transform.Identity;

                Point center = new Point(

                    (sizeFinal.Width - this.sizeLargest.Width) / 2.0,

                    (sizeFinal.Height - this.sizeLargest.Height) / 2.0);

                child.Arrange(new Rect(center, new Size(this.sizeLargest.Width, this.sizeLargest.Height)));

 

                if (this.Orientation == Orientation.Vertical)

                {

                    Point rotatePoint = this.TranslatePoint(center, child);

                    rotatePoint.X += this.sizeLargest.Width / 2.0;

                    rotatePoint.Y += this.sizeLargest.Height / 2.0;

                    child.RenderTransform = new RotateTransform(-90.0, rotatePoint.X, rotatePoint.Y);

                }

 

                return sizeFinal;

            }

 

            double angleChild = (this.Orientation == Orientation.Horizontal) ? 0.0 : -90.0;

            Point centerPoint = new Point(sizeFinal.Width / 2.0, sizeFinal.Height / 2.0);

            double multiplier = Math.Min(sizeFinal.Width, sizeFinal.Height) / (2.0 * this.radius);

 

            foreach (UIElement child in this.InternalChildren)

            {

                // Reset RenderTransform.

                child.RenderTransform = Transform.Identity;

 

                if (this.Orientation == Orientation.Horizontal)

                {

                    // Position the child at the top.

                    child.Arrange(

                        new Rect(

                            centerPoint.X - (multiplier * this.sizeLargest.Width / 2.0),

                            centerPoint.Y - (multiplier * this.outerEdgeFromCenter),

                            multiplier * this.sizeLargest.Width,

                            multiplier * this.sizeLargest.Height));

                }

                else

                {

                    // Position the child at the right.

                    child.Arrange(

                        new Rect(

                            centerPoint.X + (multiplier * this.innerEdgeFromCenter),

                            centerPoint.Y - (multiplier * this.sizeLargest.Height / 2.0),

                            multiplier * this.sizeLargest.Width,

                            multiplier * this.sizeLargest.Height));

                }

 

                // Rotate the child around the center (relative to the child).

                Point pt = TranslatePoint(centerPoint, child);

                child.RenderTransform = new RotateTransform(angleChild, pt.X, pt.Y);

 

                angleChild += this.angleEach;

            }

 

            return sizeFinal;

        }

 

        /// <summary>

        ///   Draws the content of a <see cref="T:System.Windows.Media.DrawingContext"/> object during

        ///   the render pass of a RadialPanel element.

        /// </summary>

        /// <param name="dc">

        ///   The <see cref="T:System.Windows.Media.DrawingContext"/> object to draw.

        /// </param>

        protected override void OnRender(DrawingContext dc)

        {

            base.OnRender(dc);

 

            if (this.ShowPieLines)

            {

                Point centerPoint = new Point(RenderSize.Width / 2.0, RenderSize.Height / 2.0);

                double radius = Math.Min(RenderSize.Width, RenderSize.Height) / 2;

                Pen pen = new Pen(this.Foreground, 1.0);

                pen.DashStyle = DashStyles.Dash;

 

                // Display circle.

                dc.DrawEllipse(null, pen, centerPoint, radius, radius);

 

                if (this.InternalChildren.Count == 1)

                    return;

 

                // Initialize angle.

                double angleChild = -(this.angleEach / 2.0) - 90.0;

 

                // Loop through each child to draw radial lines from center.

                foreach (UIElement child in this.InternalChildren)

                {

                    double angleChildInRadian = 2.0 * Math.PI * angleChild / 360;

                    dc.DrawLine(pen, centerPoint, new Point(centerPoint.X + (radius * Math.Cos(angleChildInRadian)), centerPoint.Y + (radius * Math.Sin(angleChildInRadian))));

                    angleChild += this.angleEach;

                }

            }

        }

    }

}

1 comment:

Anonymous said...

cool