RadialPanel - Authoring a Custom Layout in Avalon

In this post, I'm going to talk about authoring a custom Panel.  Panels are the fundamental building blocks of layout in Avalon, so, authoring a custom Panel is authoring a custom layout.  Now, as I started writing this I realized that I needed to couch my post a bit first.

There are many steps that you can take to customize your layout in Avalon without ever writing a Panel.  In fact, writing a Panel should typically be the last thing you do.  Following, I've listed a continuum of steps showing how layout can be customized and which we hope makes your job easier.  It starts from the simple and moves to the more complex.

  1. Use the Panels in the platform with Panel specific properties.
    • Canvas can be used for absolute sizing & positioning with Canvas.Left, Canvas.Top, Canvas.Right and Canvas.Bottom.
    • StackPanel can be used to "stacking" elements horizontally or vertically using the Orientation property.
    • Grid can be used for creating partitioned space for laying out elements.
    • DockPanel allows elements to be placed relative to one another with the DockPanel.Dock and LastChildFill properties.
    • TextBlock & TextFlow are used to layout text along with a variety of typographical properties and elements.
  2. Use the Layout properties to adjust layout
  3. Combine the Panels
    • Panels can contain other panels, so, you can nest them and create increasingly complex layouts.
  4. Add code-behind
    • Yep, add some code that helps perform your layout for you.
  5. Author a Panel
    • If you've tried all of the following but still need something more, don't fret; you still have the option of authoring your own Panel...and that's what this post is aboue.

Lets look at what it takes to author a Panel.  Below, I have a sample of a Panel I've called "RadialPanel."  Basically, it arranges its children in a circle taking up the space that its given.  The key to authoring a Panel is the following:

  1. Subclass from Panel
  2. Implement your custom children-measuring logic in MeasureOverride.
  3. Implement your custom children-arranging logic in ArrangeOverride.

One of the cool things about the Avalon layout system is that it takes care of handling the layout properties for you (all the ones I mentioned in the first outline.)  You'll notice that I didn't add any code for handling Width, Height, Margin, Alignments, etc.  This is all taken care of for you by the layout system!

From here, the code is pretty well commented, so, it should be mostly self explanatory from here.  You can compile this code directly if you've got the May CTP bits. 

using System;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Media;

namespace MyCustomPanels

{

    public class RadialPanel : Panel

    {

        // This panel lays its children out on a circle

        // keeping the angular distance from each child

        // equal; MeasureOverride is called before ArrangeOverride.

        double _maxChildHeight, _perimeter, _radius, _fudgeFactor;

        protected override Size MeasureOverride(Size availableSize)

        {

            _perimeter = 0;

            _maxChildHeight = 0;

            //Find the tallest child and determine the perimeter

            //based on the width of all of the children after

            //measuring all of the them and letting them size

            //to content by passing Double.PositiveInfinity as

            //the available size.

            foreach (UIElement uie in Children)

            {

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

                _perimeter += uie.DesiredSize.Width;

                _maxChildHeight = Math.Max(_maxChildHeight, uie.DesiredSize.Height);

            }

            //If the marginal angle is not 0, 90 or 180

            //then the fudge factor is needed.

            if (Children.Count > 2 && Children.Count != 4)

                _fudgeFactor = 10;

            //Determine the radius of the circle layout and determine

            //the RadialPanel's DesiredSize.

            _radius = _perimeter / (2 * Math.PI) + _fudgeFactor;

            double _squareSize = 2 * (_radius + _maxChildHeight);

            return new Size(_squareSize, _squareSize);

         

       }

        //Perform arranging of children based on

        //the final size.

        protected override Size ArrangeOverride(Size finalSize)

        {

            //Necessary variables.

            double _currentOriginX = 0,

              _currentOriginY = 0,

                    _currentAngle = 0,

                    _centerX = 0,

                    _centerY = 0,

                    _marginalAngle = 0;

            //During measure, a fudge factor was added to the radius

            //to account for rotated children that might fall outside

            //of the desired size. Now, during arrange, we don't need

            //that extra space.

            _radius -= _fudgeFactor;

            //Find center of the circle based on arrange size.

            //DesiredSize is not used because the panel

            //is potentially being arranged across a larger

            //area from the default alignment values.

            _centerX = finalSize.Width / 2;

            _centerY = finalSize.Height / 2;

           

            //Determine the marginal angle, the angle between

            //each child on the circle.

            if (Children.Count != 0)

                _marginalAngle = 360 / Children.Count;

            foreach (UIElement uie in Children)

            {

                //Find origin from which to arrange on

                //each child of the RadialPanel (its top

                //left corner.)

                _currentOriginX = _centerX - uie.DesiredSize.Width / 2;

                _currentOriginY = _centerY - _radius - uie.DesiredSize.Height;

                //Apply a rotation on each child around the center of the

                //RadialPanel.

                uie.RenderTransform = new RotateTransform(_currentAngle, new Point(uie.DesiredSize.Width / 2, uie.DesiredSize.Height + _radius));

                uie.Arrange(new Rect(new Point(_currentOriginX, _currentOriginY), new Size(uie.DesiredSize.Width, uie.DesiredSize.Height)));

                //Increment the _currentAngle by the _marginalAngle

                //to advance the next child to the appropriate position.

                _currentAngle += _marginalAngle;

            }

            //In this case, the Panel is sizing to the space

            //given, so, return the finalSize which will be used

            //to set the ActualHeight & ActualWidth and for rendering.

            return finalSize;

        }

    }

}