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;

                }

            }

        }

    }

}

2 comments:

Anonymous said...

cool

Unknown said...

Hello,

I tried your code. Thanks a lot for posting this. But am facing a problem in this customized radion panel. i binded this style to the "ItemsControl" which have 10 items in its datasource. In a window, i created 30 "ItemsControl" and total of 300 items in it. What the problem is, after 14th "ItemsControl" the customized radio panel is not binding the child automatically. instead of binding 10 items to the ItemsControl, it is binding the last item to it. Please have a look at this issue and contaact me at any time to get detailed information of this if you need.