Saturday, March 2, 2013

WPF UniformGrid variant

I've been using the WPF UniformGrid to layout the UI representation of collections in some of the applications I work on.  The apps generally follow the MVVM pattern and to get the layout I require I usually need to apply a converter to a bound viewmodel property to set the number of rows or columns.

This usually works ok but in a application where the aspect ratio of the area occupied by the UniformGrid can be changed by the user the UniformGrid doesn't doesn't alter it's row or column count to optimise the layout.

I have searched for a WPF layout that is similar to the UniformGrid but that is more flexible in it's automatic calculation of rows and columns without success so have written my own.

The following AutoUniformGrid checks its constrained size and if this is fixed (i.e. neither the width or height is infinite) it will dynamically set the number of rows and columns to maximise the height & width of its cell.

So, for example, if the AutoUniformGrid has 8 children and is sized to an area that is twice as wide as it is high, it will set its column count to 4 and its row count to 2.

In this situation, the UniformGrid would default to a 3 x 3 grid which would result in badly proportioned cells.

The AutoUniformGrid is similar to a WrapPanel except that the children all occupy the same sized area.  In my usage of this control, the children are all wrapped in a ViewBox to ensure they make full use of their allocated cell.


public class AutoUniformGrid : Panel
    {
        // Fields
        private int _columns;
        private int _rows;
        public static readonly DependencyProperty ColumnsProperty = 
            DependencyProperty.Register("Columns"typeof(int), typeof(AutoUniformGrid), 
                        new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsMeasure), 
                        new ValidateValueCallback(AutoUniformGrid.ValidateColumns));
        public static readonly DependencyProperty RowsProperty = DependencyProperty.Register("Rows"typeof(int), typeof(AutoUniformGrid), 
                        new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.AffectsMeasure), 
                        new ValidateValueCallback(AutoUniformGrid.ValidateRows));
 
        
        protected override Size ArrangeOverride(Size arrangeSize)
        {
            double width = OptimalCalculator.CalculateMinDimensionValue(arrangeSize, this._rows, this._columns);
            Size requiredSize = new Size(width * this._columns, width * this._rows);
 
            double xOffset = (arrangeSize.Width - requiredSize.Width) / 2;
            double yOffset = (arrangeSize.Height - requiredSize.Height) / 2;
 
            Rect finalRect = new Rect(xOffset, yOffset, width, width);
            double rightBoundary = xOffset + requiredSize.Width - 1.0;
            foreach (UIElement element in base.InternalChildren)
            {
                element.Arrange(finalRect);
                if (element.Visibility != Visibility.Collapsed)
                {
                    finalRect.X += width;
                    if (finalRect.X >= rightBoundary)
                    {
                        finalRect.Y += finalRect.Height;
                        finalRect.X = xOffset;
                    }
                }
            }
            return arrangeSize;
        }
 
        protected override Size MeasureOverride(Size constraint)
        {
            this.UpdateComputedValues(constraint);
 
            Size availableSize = new Size(constraint.Width / ((double)this._columns), constraint.Height / ((double)this._rows));
            double width = 0.0;
            double height = 0.0;
            int childIndex = 0;
            int count = base.InternalChildren.Count;
            while (childIndex < count)
            {
                UIElement element = base.InternalChildren[childIndex];
                element.Measure(availableSize);
                Size desiredSize = element.DesiredSize;
                if (width < desiredSize.Width)
                {
                    width = desiredSize.Width;
                }
                if (height < desiredSize.Height)
                {
                    height = desiredSize.Height;
                }
                childIndex++;
            }
            return new Size(width * this._columns, height * this._rows);
        }
 
 
        int CountVisibleChildren()
        {
            int visibleCount = 0;
            int childIndex = 0;
            int count = base.InternalChildren.Count;
            while (childIndex < count)
            {
                UIElement element = base.InternalChildren[childIndex];
                if (element.Visibility != Visibility.Collapsed)
                {
                    visibleCount++;
                }
                childIndex++;
            }
            return visibleCount;
        }
 
 
        private void UpdateComputedValues(Size constraint)
        {
            this._columns = this.Columns;
            this._rows = this.Rows;
            if ((this._rows == 0) || (this._columns == 0))
            {
                int visibleCount = CountVisibleChildren();
                if (visibleCount == 0)
                {
                    visibleCount = 1;
                }
                if (this._rows == 0)
                {
                    if (this._columns > 0)
                    {
                        this._rows = (visibleCount + (this._columns - 1)) / this._columns;
                    }
                    else
                    {
                        OptimalCalculator optimal = new OptimalCalculator( constraint, visibleCount);
                        this._rows = optimal.OptimalRows;
                        this._columns = optimal.OptimalColumns;
                    }
                } else if (this._columns == 0)
                {
                    this._columns = (visibleCount + (this._rows - 1)) / this._rows;
                }
            }
        }
 
        private static bool ValidateColumns(object o)
        {
            return (((int)o) >= 0);
        }
 
        private static bool ValidateRows(object o)
        {
            return (((int)o) >= 0);
        }
 
        public int Columns
        {
            get
            {
                return (int)base.GetValue(ColumnsProperty);
            }
            set
            {
                base.SetValue(ColumnsProperty, value);
            }
        }
 
        public int Rows
        {
            get
            {
                return (int)base.GetValue(RowsProperty);
            }
            set
            {
                base.SetValue(RowsProperty, value);
            }
        }
 
        class OptimalCalculator
        {
 
            public int OptimalRows { getprivate set; }
            public int OptimalColumns { getprivate set; }
 
            double DimensionAtOptimal { getset; }
            Size AvailableSize { getset; }
            int VisibleChildCount { getset; }
 
            public OptimalCalculator( Size availableSize, int visibleChildCount )
            {
                AvailableSize = availableSize;
                VisibleChildCount = visibleChildCount;
                DimensionAtOptimal = 0.0;
                Calculate( );
            }
 
            private void Calculate()
            {
                OptimalRows = 0;
                OptimalColumns = 0;
 
                double area = AvailableSize.Height * AvailableSize.Width;
 
                if (!double.IsInfinity(area) && !double.IsNaN(area) && area != 0.0 && VisibleChildCount > 1)
                {
                    double aspect = AvailableSize.Width / AvailableSize.Height;
                    int columns = 1, rows = 1;
 
                    if (aspect > 1)
                    {
                        for (; columns <= VisibleChildCount; columns++)
                        {
                            rows = (intMath.Ceiling( VisibleChildCount/(double)columns );                            
                            MaybeUpdateOptimal(rows, columns);
                        }
                    }
                    else
                    {
                        for (; rows <= VisibleChildCount; rows++)
                        {
                            columns = (intMath.Ceiling( VisibleChildCount / (double) rows);                            
                            MaybeUpdateOptimal(rows, columns);
                        }
                    }
                }
 
                if (OptimalRows <= 0)
                {
                    OptimalRows = (int)Math.Round(Math.Sqrt((double)VisibleChildCount));
                    if (OptimalRows * OptimalRows < VisibleChildCount)
                        OptimalRows++;
                    OptimalColumns = OptimalRows;
                }                
            }
 
            private void MaybeUpdateOptimal(int rows, int columns)
            {
                double dimensionValue = CalculateMinDimensionValue(AvailableSize, rows, columns);
                if (dimensionValue > DimensionAtOptimal)
                {
                    DimensionAtOptimal = dimensionValue;
                    OptimalRows = rows;
                    OptimalColumns = columns;
                }
            }   
        
            static internal double CalculateMinDimensionValue(Size availableSize,  int rows, int columns)
            {
                if (rows <= 0 || columns <= 0)
                    return 0.0;
                double width = availableSize.Width / columns;
                double height = availableSize.Height / rows;
                return Math.Min(width, height);
            }
        }
 
    }

No comments:

Post a Comment