Posts Tagged ArrangeOverride

ProportionalPanel for Silverlight


I wanted a panel that distributes its children according to the proportion of their width or by a proportion supplied by the object attached to the DataContext.  This lead to me writing PropotionalPanel – the first custom panel I have written.  It’s useful in building dashboards and other layouts that require collections of children, because Grids aren’t easy to use inside ItemsControls.  I wrote this control to be used as the ItemsPanel of a list box and it works well in that context.When writing a panel the two important routines are MeasureOverride and ArrangeOverride.  There’s lots of documentation on these in the help, but it’s interesting to see the parameters with which these methods are called at different times.  

For instance, even if you have a fixed size container, MeasureOverride is called with infinite dimensions on the first occasion, thereafter the call specifies the fixed size.  

Here’s my version of the MeasureOverride routine. I maintain a variable called totalProportion which is either the sum of the proportions of the attached objects, or the sum of the proportions of the stretched dimension based on orientation.  

  

        protected override Size MeasureOverride(Size availableSize)
        {
            Size finalSize = new Size(availableSize.Width, availableSize.Height);
            double totalProportion = 0;
            foreach (var c in Children.OfType<FrameworkElement>())
            {  

                if (c.DataContext is IProportional)
                {
                    totalProportion += (c.DataContext as IProportional).GetProportion();
                }
                else
                {
                    totalProportion += Orientation == Orientation.Vertical
                                           ? c.DesiredSize.Height
                                           : c.DesiredSize.Width;
                }
            }  

            double sizeAvailable, maxAlternate = 0;
            switch (Orientation)
            {
                case Orientation.Horizontal:
                    sizeAvailable = availableSize.Width;
                    if (double.IsNaN(sizeAvailable) || double.IsPositiveInfinity(sizeAvailable))
                    {
                        sizeAvailable = 0;
                        foreach (var c in Children)
                        {
                            c.Measure(availableSize);
                            sizeAvailable += c.DesiredSize.Width;
                            maxAlternate = Math.Max(maxAlternate, c.DesiredSize.Height);
                        }
                        finalSize.Width = sizeAvailable;
                        finalSize.Height = maxAlternate;  

                    }
                    else
                    {
                        foreach (var c in Children.OfType<FrameworkElement>())
                        {
                            double p;
                            if (c.DataContext is IProportional)
                            {
                                p = (c.DataContext as IProportional).GetProportion();
                            }
                            else
                            {
                                p = Orientation == Orientation.Vertical
                                                       ? c.DesiredSize.Height
                                                       : c.DesiredSize.Width;
                            }
                            c.Measure(new Size(Math.Max(0,Math.Floor((sizeAvailable * p)/totalProportion)), finalSize.Height));
                        }
                    }
                    break;
                case Orientation.Vertical:
                    sizeAvailable = availableSize.Height;
                    if (double.IsNaN(sizeAvailable) || double.IsPositiveInfinity(sizeAvailable))
                    {
                        sizeAvailable = 0;
                        foreach (var c in Children)
                        {
                            c.Measure(availableSize);
                            sizeAvailable += c.DesiredSize.Height;
                            maxAlternate = Math.Max(maxAlternate, c.DesiredSize.Width);
                        }
                        finalSize.Height = sizeAvailable;
                        finalSize.Width = maxAlternate;
                    }
                    else
                    {
                        foreach (var c in Children.OfType<FrameworkElement>())
                        {
                            double p;
                            if (c.DataContext is IProportional)
                            {
                                p = (c.DataContext as IProportional).GetProportion();
                            }
                            else
                            {
                                p = Orientation == Orientation.Vertical
                                                       ? c.DesiredSize.Height
                                                       : c.DesiredSize.Width;
                            }
                            c.Measure(new Size(finalSize.Width, Math.Max(0, Math.Floor((sizeAvailable * p) / totalProportion))));
                        }
                    }  

                    break;
            }  
            return finalSize;
        }
    }  

 

First the total proportion is calculated by walking through the children and summing either the proportions from the attached object, or the required size.  

  

            double totalProportion = 0;
            foreach (var c in Children.OfType<FrameworkElement>())
            {  

                if (c.DataContext is IProportional)
                {
                    totalProportion += (c.DataContext as IProportional).GetProportion();
                }
                else
                {
                    totalProportion += Orientation == Orientation.Vertical
                                           ? c.DesiredSize.Height
                                           : c.DesiredSize.Width;
                }
            }  

 

If the parameters of the MeasureOverride call have infinite dimensions I need to measure all of the children for their desired size, if there is a specific size then I need to measure each child providing guidance on how much space they will be allocated.  

  

           case Orientation.Horizontal:
                    sizeAvailable = availableSize.Width;
                    if (double.IsNaN(sizeAvailable) || double.IsPositiveInfinity(sizeAvailable))
                    {
                        sizeAvailable = 0;
                        foreach (var c in Children)
                        {
                            c.Measure(availableSize);
                            sizeAvailable += c.DesiredSize.Width;
                            maxAlternate = Math.Max(maxAlternate, c.DesiredSize.Height);
                        }
                        finalSize.Width = sizeAvailable;
                        finalSize.Height = maxAlternate;  

                    }
                    else
                    {
                        foreach (var c in Children.OfType<FrameworkElement>())
                        {
                            double p;
                            if (c.DataContext is IProportional)
                            {
                                p = (c.DataContext as IProportional).GetProportion();
                            }
                            else
                            {
                                p = Orientation == Orientation.Vertical
                                                       ? c.DesiredSize.Height
                                                       : c.DesiredSize.Width;
                            }
                            c.Measure(new Size(Math.Max(0,Math.Floor((sizeAvailable * p)/totalProportion)), finalSize.Height));
                        }
                    }
                    break;  

 

In the fixed size measurement I use the proportion to calculate the amount of stretched space available to the control.  It is vital that you both measure and arrange children in the correct size – if you don’t measure then it initially appears to work, but strange sizing artefacts occur.  

The arrangement pass is very similar to the fixed size measurement, except you pass a rectangle to the child giving it the layout slot into which it will fit:  

  

            if (Orientation == Orientation.Vertical)
            {
                foreach (var c in Children.OfType<FrameworkElement>())
                {
                    double p = 0;
                    if (c.DataContext is IProportional)
                    {
                        p = (c.DataContext as IProportional).GetProportion();
                    }
                    else
                    {
                        p = c.DesiredSize.Height;
                    }
                    double d = Math.Max(0,Math.Floor((finalSize.Height * p) / totalProportion));
                    c.Arrange(new Rect(0, offset, finalSize.Width, d));
                    offset += d;
                }
            }  

 

The IProportional interface is implemented on objects that are used as DataContexts for the items in my panel:  

  

    public interface IProportional
    {
        double GetProportion();
    } 

 

When you have a proportional panel like this it’s a good idea to provide some way for the user to resize things!  I use a thing called FrameControl in my ListBox’s DataTemplate – this provides sizing thumbs that can change the horizontal and vertical proportions of the underlying DataContext objects. 

 

        private void WidthThumb_OnDragDelta(object sender, DragDeltaEventArgs e)
        {
            var pp = this.FirstVisualAncestorOfType<ProportionalPanel>();
            var thisIndex = pp.Children.IndexOf(this.FirstVisualAncestorOfType<ListBoxItem>());
            var thisItem = DataContext as DashboardItem;
            var nextItem = pp.Children[thisIndex + 1].Cast<FrameworkElement>().DataContext as DashboardItem; 

            Debug.Assert(thisItem != null);
            Debug.Assert(nextItem != null); 

            var move = (e.HorizontalChange * GetProportions()) / pp.ActualWidth;
            if (thisItem.Proportion + move < 0)
                move = - thisItem.Proportion;
            if (nextItem.Proportion - move < 0)
                move = nextItem.Proportion;
            thisItem.Proportion += move;
            nextItem.Proportion -= move;
            pp.InvalidateMeasure();
        } 

 

This example uses my FirstVisualAncestorOfType extension method found elsewhere on this blog.  For the FrameControl it finds the ProportionalPanel that owns it then works out what the pixel change in the Thumb means in terms of the change to the arbitrary proportions returning by the IProportional implementation.  If you just used heights then this would be a lot simpler, but harder to store.  Obviously you need a thumb for the sizable dimension, but not both, so I show just the relevant thumb based on the panel’s orientation. 

To build a dashboard I layer panel within panel as you can see in this video – which also shows the thumb action and the ability to react to drag and drop (not allowing containers of the same type to be embedded within containers). 

Here’s the complete code for the panel.  I have the IProportional interface defined in a library that is used by the items I attach to the DataContext, so that they don’t have to reference the Panel – but you might just want to insert it in this namespace. 

 

using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls; 

namespace DashboardRenderers
{
    public class ProportionalPanel : Panel
    {
        public Orientation Orientation
        {
            get
            {
                return (Orientation)GetValue(OrientationProperty);
            }
            set
            {
                SetValue(OrientationProperty, value);
            }
        } 

        // Using a DependencyProperty as the backing store for Orientation.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OrientationProperty =
            DependencyProperty.Register("Orientation", typeof(Orientation), typeof(ProportionalPanel), new PropertyMetadata(System.Windows.Controls.Orientation.Vertical)); 

        protected override Size ArrangeOverride(Size finalSize)
        {
            double offset = 0;
            double totalProportion = 0;
            foreach (var c in Children.OfType<FrameworkElement>())
            { 

                if (c.DataContext is IProportional)
                {
                    totalProportion += (c.DataContext as IProportional).GetProportion();
                }
                else
                {
                    totalProportion += Orientation == Orientation.Vertical
                                           ? c.DesiredSize.Height
                                           : c.DesiredSize.Width;
                }
            }
            if (Orientation == Orientation.Vertical)
            {
                foreach (var c in Children.OfType<FrameworkElement>())
                {
                    double p = 0;
                    if (c.DataContext is IProportional)
                    {
                        p = (c.DataContext as IProportional).GetProportion();
                    }
                    else
                    {
                        p = c.DesiredSize.Height;
                    }
                    double d = Math.Max(0,Math.Floor((finalSize.Height * p) / totalProportion));
                    c.Arrange(new Rect(0, offset, finalSize.Width, d));
                    offset += d;
                }
            }
            else
            {
                foreach (var c in Children.OfType<FrameworkElement>())
                {
                    double p = 0;
                    if (c.DataContext is IProportional)
                    {
                        p = (c.DataContext as IProportional).GetProportion();
                    }
                    else
                    {
                        p = c.DesiredSize.Width;
                    }
                    double d = Math.Max(0, Math.Floor((finalSize.Width * p) / totalProportion));
                    c.Arrange(new Rect(offset, 0, d, finalSize.Height));
                    offset += d;
                } 

            }
            return finalSize;
        } 
        protected override Size MeasureOverride(Size availableSize)
        {
            Size finalSize = new Size(availableSize.Width, availableSize.Height);
            double totalProportion = 0;
            foreach (var c in Children.OfType<FrameworkElement>())
            { 

                if (c.DataContext is IProportional)
                {
                    totalProportion += (c.DataContext as IProportional).GetProportion();
                }
                else
                {
                    totalProportion += Orientation == Orientation.Vertical
                                           ? c.DesiredSize.Height
                                           : c.DesiredSize.Width;
                }
            } 

            double sizeAvailable, maxAlternate = 0;
            switch (Orientation)
            {
                case Orientation.Horizontal:
                    sizeAvailable = availableSize.Width;
                    if (double.IsNaN(sizeAvailable) || double.IsPositiveInfinity(sizeAvailable))
                    {
                        sizeAvailable = 0;
                        foreach (var c in Children)
                        {
                            c.Measure(availableSize);
                            sizeAvailable += c.DesiredSize.Width;
                            maxAlternate = Math.Max(maxAlternate, c.DesiredSize.Height);
                        }
                        finalSize.Width = sizeAvailable;
                        finalSize.Height = maxAlternate; 

                    }
                    else
                    {
                        foreach (var c in Children.OfType<FrameworkElement>())
                        {
                            double p;
                            if (c.DataContext is IProportional)
                            {
                                p = (c.DataContext as IProportional).GetProportion();
                            }
                            else
                            {
                                p = Orientation == Orientation.Vertical
                                                       ? c.DesiredSize.Height
                                                       : c.DesiredSize.Width;
                            }
                            c.Measure(new Size(Math.Max(0,Math.Floor((sizeAvailable * p)/totalProportion)), finalSize.Height));
                        }
                    }
                    break;
                case Orientation.Vertical:
                    sizeAvailable = availableSize.Height;
                    if (double.IsNaN(sizeAvailable) || double.IsPositiveInfinity(sizeAvailable))
                    {
                        sizeAvailable = 0;
                        foreach (var c in Children)
                        {
                            c.Measure(availableSize);
                            sizeAvailable += c.DesiredSize.Height;
                            maxAlternate = Math.Max(maxAlternate, c.DesiredSize.Width);
                        }
                        finalSize.Height = sizeAvailable;
                        finalSize.Width = maxAlternate;
                    }
                    else
                    {
                        foreach (var c in Children.OfType<FrameworkElement>())
                        {
                            double p;
                            if (c.DataContext is IProportional)
                            {
                                p = (c.DataContext as IProportional).GetProportion();
                            }
                            else
                            {
                                p = Orientation == Orientation.Vertical
                                                       ? c.DesiredSize.Height
                                                       : c.DesiredSize.Width;
                            }
                            c.Measure(new Size(finalSize.Width, Math.Max(0, Math.Floor((sizeAvailable * p) / totalProportion))));
                        }
                    } 

                    break;
            } 
            return finalSize;
        }
    }
} 

, , , , , , , ,

Leave a comment