9

The Children of WrapPanel are populated sequentially like attached screenshot.

Therefore, according to the length of each child, the Panel makes long blank space.

How can I utilize the blank space with re-arrangng the children ?

It seems only few people use WrapPanel so far and no sufficient examples.

Is there some automatic way for this ? Or do I only need to make own algorithm ?

The WrapPanel plays a very important role to display things but has limited space to display.

Thank you !

enter image description here

aybe
  • 15,516
  • 9
  • 57
  • 105
Kay Lee
  • 922
  • 1
  • 12
  • 40
  • 2
    i dont think there is any, maybe you have come up with your own algorithm which calculate childerns width and rearrange itself accordingly. – tabby Dec 16 '17 at 06:27
  • 2
    You are probably looking for a 'treemap' or a 'tag cloud'. – aybe Dec 16 '17 at 06:29
  • @tabby, Many thanks for your precious time and kind comment. It's really implementable but time-consuming work unlike we can find useful libraries for common purposes. – Kay Lee Dec 16 '17 at 09:59
  • @Aybe, Many thanks for your precious time and kind comment. I've found and tested some 'tag cloud' created by someone else and it's good but the essence of my purpose doesn't match with it. Anyway, thank you for introducing useful idea to me. – Kay Lee Dec 16 '17 at 10:03
  • 1
    @KayLee Note that what you're asking for is a special case, where all child elements have the same height. However, a WrapPanel arranges elements of aribtrary dimensions. – Clemens Dec 16 '17 at 10:03
  • @Clemens, yes, right. All elements have same height always. What do you mean by special case? like he Capital letter 'E' and lower case 'e' ? – Kay Lee Dec 16 '17 at 10:06
  • 1
    I mean that a general algorithm (for elements of different heights) would be far more complicated. – Clemens Dec 16 '17 at 10:07
  • @Clemens, Sure, creating general algorithm is beyond my capacity. But as I commented, my case would be really implementable but bothersome, time-consuming...I hope Microsoft engineer put interests on non-popular element like this. – Kay Lee Dec 16 '17 at 10:14
  • 1
    So you don't care about order of elements at all? The goal is to use available space as efficient as possible? – Evk Dec 19 '17 at 15:56

4 Answers4

7

Here is a WrapPanel which can optionally rearrange elements using FFDH algorithm, as well as optionally stretch them to remove blank areas (example).

public class StretchyWrapPanel : Panel {
    public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register(nameof(ItemWidth), typeof(double),
            typeof(StretchyWrapPanel), new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure, (o, e) => {
                ((StretchyWrapPanel)o)._itemWidth = (double)e.NewValue;
            }));

    private double _itemWidth = double.NaN;

    [TypeConverter(typeof(LengthConverter))]
    public double ItemWidth {
        get => _itemWidth;
        set => SetValue(ItemWidthProperty, value);
    }

    public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register(nameof(ItemHeight), typeof(double),
            typeof(StretchyWrapPanel), new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure, (o, e) => {
                ((StretchyWrapPanel)o)._itemHeight = (double)e.NewValue;
            }));

    private double _itemHeight = double.NaN;

    [TypeConverter(typeof(LengthConverter))]
    public double ItemHeight {
        get => _itemHeight;
        set => SetValue(ItemHeightProperty, value);
    }

    public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(Orientation),
            typeof(StretchyWrapPanel), new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsArrange, (o, e) => {
                ((StretchyWrapPanel)o)._orientation = (Orientation)e.NewValue;
            }));

    private Orientation _orientation = Orientation.Horizontal;

    public Orientation Orientation {
        get => _orientation;
        set => SetValue(OrientationProperty, value);
    }

    public static readonly DependencyProperty StretchToFillProperty = DependencyProperty.Register(nameof(StretchToFill), typeof(bool),
            typeof(StretchyWrapPanel), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsArrange, (o, e) => {
                ((StretchyWrapPanel)o)._stretchToFill = (bool)e.NewValue;
            }));

    private bool _stretchToFill = true;

    public bool StretchToFill {
        get => _stretchToFill;
        set => SetValue(StretchToFillProperty, value);
    }

    public static readonly DependencyProperty StretchProportionallyProperty = DependencyProperty.Register(nameof(StretchProportionally), typeof(bool),
            typeof(StretchyWrapPanel), new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsArrange, (o, e) => {
                ((StretchyWrapPanel)o)._stretchProportionally = (bool)e.NewValue;
            }));

    private bool _stretchProportionally = true;

    public bool StretchProportionally {
        get => _stretchProportionally;
        set => SetValue(StretchProportionallyProperty, value);
    }

    public static readonly DependencyProperty RearrangeForBestFitProperty = DependencyProperty.Register(nameof(RearrangeForBestFit), typeof(bool),
            typeof(StretchyWrapPanel), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsMeasure, (o, e) => {
                ((StretchyWrapPanel)o)._rearrangeForBestFit = (bool)e.NewValue;
            }));

    private bool _rearrangeForBestFit;

    public bool RearrangeForBestFit {
        get => _rearrangeForBestFit;
        set => SetValue(RearrangeForBestFitProperty, value);
    }

    private struct UVSize {
        internal UVSize(Orientation orientation, Size size) {
            U = V = 0d;
            _isHorizontal = orientation == Orientation.Horizontal;
            Width = size.Width;
            Height = size.Height;
        }

        internal UVSize(Orientation orientation, double width, double height) {
            U = V = 0d;
            _isHorizontal = orientation == Orientation.Horizontal;
            Width = width;
            Height = height;
        }

        internal UVSize(Orientation orientation) {
            U = V = 0d;
            _isHorizontal = orientation == Orientation.Horizontal;
        }

        internal double U;
        internal double V;
        private bool _isHorizontal;

        internal double Width {
            get => _isHorizontal ? U : V;
            set {
                if (_isHorizontal) {
                    U = value;
                } else {
                    V = value;
                }
            }
        }

        internal double Height {
            get => _isHorizontal ? V : U;
            set {
                if (_isHorizontal) {
                    V = value;
                } else {
                    U = value;
                }
            }
        }
    }

    protected override Size MeasureOverride(Size constraint) {
        return RearrangeForBestFit ? MeasureBestFit(constraint) : MeasureKeepInOrder(constraint);
    }

    private Size MeasureKeepInOrder(Size constraint) {
        var orientation = Orientation;
        var uLimit = new UVSize(orientation, constraint.Width, constraint.Height).U;
        var curLineSize = new UVSize(orientation);
        var panelSize = new UVSize(orientation);
        var itemWidth = ItemWidth;
        var itemHeight = ItemHeight;
        var itemWidthSet = !double.IsNaN(itemWidth);
        var itemHeightSet = !double.IsNaN(itemHeight);

        var childConstraint = new Size(
                itemWidthSet ? itemWidth : constraint.Width,
                itemHeightSet ? itemHeight : constraint.Height);

        var children = InternalChildren;

        for (int i = 0, count = children.Count; i < count; i++) {
            var child = children[i];
            if (child == null) continue;

            // Flow passes its own constrint to children
            child.Measure(childConstraint);

            // This is the size of the child in UV space
            var sz = new UVSize(orientation,
                    itemWidthSet ? itemWidth : child.DesiredSize.Width,
                    itemHeightSet ? itemHeight : child.DesiredSize.Height);

            if (curLineSize.U + sz.U > uLimit) {
                // Need to switch to another line
                panelSize.U = Math.Max(curLineSize.U, panelSize.U);
                panelSize.V += curLineSize.V;
                curLineSize = sz;

                if (sz.U > uLimit) {
                    // The element is wider then the constrint - give it a separate line
                    panelSize.U = Math.Max(sz.U, panelSize.U);
                    panelSize.V += sz.V;
                    curLineSize = new UVSize(orientation);
                }
            } else {
                // Continue to accumulate a line
                curLineSize.U += sz.U;
                curLineSize.V = Math.Max(sz.V, curLineSize.V);
            }
        }

        // The last line size, if any should be added
        panelSize.U = Math.Max(curLineSize.U, panelSize.U);
        panelSize.V += curLineSize.V;

        // Go from UV space to W/H space
        return new Size(panelSize.Width, panelSize.Height);
    }

    private Size MeasureBestFit(Size constraint) {
        var orientation = Orientation;
        var uLimit = new UVSize(orientation, constraint.Width, constraint.Height).U;
        var itemWidth = ItemWidth;
        var itemHeight = ItemHeight;
        var itemWidthSet = !double.IsNaN(itemWidth);
        var itemHeightSet = !double.IsNaN(itemHeight);

        var childConstraint = new Size(
                itemWidthSet ? itemWidth : constraint.Width,
                itemHeightSet ? itemHeight : constraint.Height);

        var children = InternalChildren;

        // First-Fit Decreasing Height (FFDH) algorithm
        var lines = new List<UVSize>();

        for (int i = 0, count = children.Count; i < count; i++) {
            var child = children[i];
            if (child == null) continue;

            // Flow passes its own constrint to children
            child.Measure(childConstraint);

            // This is the size of the child in UV space
            var childSize = new UVSize(orientation,
                    itemWidthSet ? itemWidth : child.DesiredSize.Width,
                    itemHeightSet ? itemHeight : child.DesiredSize.Height);

            for (var j = 0; j < lines.Count; j++) {
                var line = lines[j];
                if (line.U + childSize.U <= uLimit) {
                    lines[j] = new UVSize(orientation) { U = childSize.U, V = Math.Max(childSize.V, line.V) };
                    goto Next;
                }
            }

            lines.Add(childSize);

            Next:
            { }
        }

        var panelSize = new UVSize(orientation);
        for (var i = 0; i < lines.Count; i++) {
            var line = lines[i];
            panelSize.U = Math.Max(line.U, panelSize.U);
            panelSize.V += line.V;
        }

        // Go from UV space to W/H space
        return new Size(panelSize.Width, panelSize.Height);
    }

    protected override Size ArrangeOverride(Size finalSize) {
        return RearrangeForBestFit ? ArrangeBestFit(finalSize) : ArrangeKeepInOrder(finalSize);
    }

    private static UVSize GetChildSize(Orientation orientation, UIElement child, UVSize fixedChildSize) {
        var childSize = new UVSize(orientation, child.DesiredSize);
        if (!double.IsNaN(fixedChildSize.U)) childSize.U = fixedChildSize.U;
        if (!double.IsNaN(fixedChildSize.V)) childSize.V = fixedChildSize.V;
        return childSize;
    }

    private Size ArrangeKeepInOrder(Size finalSize) {
        var orientation = Orientation;
        var fixedChildSize = new UVSize(orientation, ItemWidth, ItemHeight);
        var children = InternalChildren;
        var firstInLine = 0;
        var uLimit = new UVSize(orientation, finalSize).U;
        var currentLineSize = new UVSize(orientation);
        var accumulatedV = 0d;

        for (int i = 0, count = children.Count; i < count; i++) {
            var child = children[i];
            if (child == null) continue;

            var childSize = GetChildSize(orientation, child, fixedChildSize);
            if (currentLineSize.U + childSize.U > uLimit) {
                // Need to switch to another line
                if (!double.IsNaN(fixedChildSize.U)) {
                    ArrangeLineFixedSize(orientation, children, accumulatedV, currentLineSize.V, firstInLine, i, fixedChildSize.U);
                } else if (!StretchToFill) {
                    ArrangeLineDefault(orientation, children, accumulatedV, currentLineSize.V, firstInLine, i);
                } else {
                    ArrangeLineStretch(orientation, children, accumulatedV, currentLineSize.V, firstInLine, i, uLimit, StretchProportionally);
                }

                accumulatedV += currentLineSize.V;
                currentLineSize = childSize;
                firstInLine = i;
            } else {
                // Continue to accumulate a line
                currentLineSize.U += childSize.U;
                currentLineSize.V = Math.Max(childSize.V, currentLineSize.V);
            }
        }

        // Arrange the last line, if any
        if (!double.IsNaN(fixedChildSize.U)) {
            ArrangeLineFixedSize(orientation, children, accumulatedV, currentLineSize.V, firstInLine, children.Count, fixedChildSize.U);
        } else if (!StretchToFill) {
            ArrangeLineDefault(orientation, children, accumulatedV, currentLineSize.V, firstInLine, children.Count);
        } else {
            ArrangeLineStretch(orientation, children, accumulatedV, currentLineSize.V, firstInLine, children.Count, uLimit, StretchProportionally);
        }

        return finalSize;
    }

    private static void ArrangeLineDefault(Orientation orientation, UIElementCollection children, double v, double lineV, int start, int end) {
        var position = new UVSize(orientation){ U = 0d, V = v };
        for (var i = start; i < end; i++) {
            var child = children[i];
            if (child != null) {
                var childSize = new UVSize(orientation, child.DesiredSize) { V = lineV };
                child.Arrange(new Rect(position.Width, position.Height, childSize.Width, childSize.Height));
                position.U += childSize.U;
            }
        }
    }

    private static void ArrangeLineStretch(Orientation orientation, UIElementCollection children, double v, double lineV, int start, int end,
            double limitU, bool stretchProportionally) {
        var totalU = 0d;
        for (var i = start; i < end; i++) {
            totalU += new UVSize(orientation, children[i].DesiredSize).U;
        }

        var position = new UVSize(orientation) { U = 0d, V = v };
        var uExtra = stretchProportionally ? limitU / totalU : (limitU - totalU) / (end - start);
        for (var i = start; i < end; i++) {
            var child = children[i];
            if (child != null) {
                var childSize = new UVSize(orientation, child.DesiredSize) { V = lineV };
                childSize.U = stretchProportionally ? childSize.U * uExtra : Math.Max(childSize.U + uExtra, 1d);
                child.Arrange(new Rect(position.Width, position.Height, childSize.Width, childSize.Height));
                position.U += childSize.U;
            }
        }
    }

    private static void ArrangeLineFixedSize(Orientation orientation, UIElementCollection children, double v, double lineV, int start, int end, double itemU) {
        var position = new UVSize(orientation) { U = 0d, V = v };
        var childSize = new UVSize(orientation){ U = itemU, V = lineV };
        for (var i = start; i < end; i++) {
            var child = children[i];
            if (child != null) {
                child.Arrange(new Rect(position.Width, position.Height, childSize.Width, childSize.Height));
                position.U += childSize.U;
            }
        }
    }

    private class ArrangeBestFitLine {
        public UVSize Size;
        public readonly List<int> ItemIndices = new List<int>();

        public void ArrangeDefault(Orientation orientation, UIElementCollection children, double v) {
            var position = new UVSize(orientation){ U = 0d, V = v };
            for (var i = 0; i < ItemIndices.Count; i++) {
                var child = children[ItemIndices[i]];
                if (child != null) {
                    var childSize = new UVSize(orientation, child.DesiredSize) { V = Size.V };
                    child.Arrange(new Rect(position.Width, position.Height, childSize.Width, childSize.Height));
                    position.U += childSize.U;
                }
            }
        }

        public void ArrangeStretch(Orientation orientation, UIElementCollection children, double v, double limitU, bool stretchProportionally) {
            var totalU = 0d;
            for (var i = 0; i < ItemIndices.Count; i++) {
                totalU += new UVSize(orientation, children[ItemIndices[i]].DesiredSize).U;
            }

            var position = new UVSize(orientation) { U = 0d, V = v };
            var uExtra = stretchProportionally ? limitU / totalU : (limitU - totalU) / ItemIndices.Count;
            for (var i = 0; i < ItemIndices.Count; i++) {
                var child = children[ItemIndices[i]];
                if (child != null) {
                    var childSize = new UVSize(orientation, child.DesiredSize) { V = Size.V };
                    childSize.U = stretchProportionally ? childSize.U * uExtra : Math.Max(childSize.U + uExtra, 1d);
                    child.Arrange(new Rect(position.Width, position.Height, childSize.Width, childSize.Height));
                    position.U += childSize.U;
                }
            }
        }

        public void ArrangeFixedSize(Orientation orientation, UIElementCollection children, double v, double itemU) {
            var position = new UVSize(orientation) { U = 0d, V = v };
            var childSize = new UVSize(orientation){ U = itemU, V = Size.V };
            for (var i = 0; i < ItemIndices.Count; i++) {
                var child = children[ItemIndices[i]];
                if (child != null) {
                    child.Arrange(new Rect(position.Width, position.Height, childSize.Width, childSize.Height));
                    position.U += childSize.U;
                }
            }
        }
    }

    private Size ArrangeBestFit(Size finalSize) {
        var orientation = Orientation;
        var fixedChildSize = new UVSize(orientation, ItemWidth, ItemHeight);
        var uLimit = new UVSize(orientation, finalSize).U;

        // First-Fit Decreasing Height (FFDH) algorithm
        var lines = new List<ArrangeBestFitLine>();
        var children = InternalChildren;
        for (int i = 0, count = children.Count; i < count; i++) {
            var child = children[i];
            if (child == null) continue;

            var childSize = GetChildSize(orientation, child, fixedChildSize);
            for (var j = 0; j < lines.Count; j++) {
                var line = lines[j];
                if (line.Size.U + childSize.U <= uLimit) {
                    line.Size.U += childSize.U;
                    line.Size.V = Math.Max(childSize.V, line.Size.V);
                    line.ItemIndices.Add(i);
                    goto Next;
                }
            }

            lines.Add(new ArrangeBestFitLine {
                Size = childSize,
                ItemIndices = { i }
            });

            Next:
            { }
        }

        var accumulatedV = 0d;
        for (var i = 0; i < lines.Count; i++) {
            var line = lines[i];

            if (!double.IsNaN(fixedChildSize.U)) {
                line.ArrangeFixedSize(orientation, children, accumulatedV, fixedChildSize.U);
            } else if (!StretchToFill) {
                line.ArrangeDefault(orientation, children, accumulatedV);
            } else {
                line.ArrangeStretch(orientation, children, accumulatedV, uLimit, StretchProportionally);
            }

            accumulatedV += line.Size.V;
        }

        return finalSize;
    }
}
Surfin Bird
  • 488
  • 7
  • 16
  • Hello, Mr. Bird, if your example with various option is displayed by this code really, there is no way but happy to mark this as answer. My deepest appreciation for your contribution ! – Kay Lee Dec 21 '17 at 00:11
2

Are all your elements in the WrapPanel of the same height? In either case, why not start with a TreeMap control like https://marketplace.visualstudio.com/items?itemName=DevExpress.WPFTreeMapControl and customize it?

EDIT 1 - Based on the comment

It shouldn't be too difficult to implement - I am pretty sure the minimal functionality that you need can be pulled off by inheriting from a panel and hooking the rearrange code in its MeasureOverride and ArrangeOverride methods. Take a look here too (implements a similar control), could be a good start if you choose to do so: https://codeblitz.wordpress.com/2009/03/20/wpf-auto-arrange-animated-panel/

jester
  • 3,491
  • 19
  • 30
  • thank you so much for introducing good idea. if you say so, customization of Treemap might be good solution but..I already invested too much resources for non-profit application and seems beyond my coding capacity.. – Kay Lee Dec 20 '17 at 00:24
2

This simple Panel should do the job:

public class MySpecialWrapPanel : Panel
{
    protected override Size MeasureOverride(Size availableSize)
    {
        foreach (UIElement child in InternalChildren)
        {
            child.Measure(availableSize);
        }

        return new Size();
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        var x = 0d;
        var y = 0d;
        var height = 0d;
        var children = InternalChildren.Cast<UIElement>().ToList();

        while (children.Count > 0)
        {
            var child = children.First();

            if (x > 0d && x + child.DesiredSize.Width > finalSize.Width)
            {
                // try to find child that fits
                var fit = children.FirstOrDefault(
                    c => x + c.DesiredSize.Width <= finalSize.Width);

                child = fit ?? child;

                if (x + child.DesiredSize.Width > finalSize.Width)
                {
                    x = 0d;
                    y = height;
                }
            }

            children.Remove(child);
            child.Arrange(
                new Rect(x, y, child.DesiredSize.Width, child.DesiredSize.Height));

            x += child.DesiredSize.Width;
            height = Math.Max(height, y + child.DesiredSize.Height);
        }

        return finalSize;
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Many thanks for your solution. Please understand I communicated with Mr. Bird yesterday. I'll also, of course, test this solution and if it deserves another answer, I'll provide non-official bounty for your kindness. Please wait until weekend for test. – Kay Lee Dec 21 '17 at 01:39
  • @KayLee Please note that you should usually accept that answer that best solves your problem. I think the code shown here is far simpler and more on topic, as it exactly does what you describe in your question, nothing more and nothing less. – Clemens Dec 21 '17 at 06:59
1

You need a custom algorithm for this. I believe the StretchyWrapPanel mentioned here is a good start. After measuring the child items, you can then sort them using custom logic (e.g. bin packing).

l33t
  • 18,692
  • 16
  • 103
  • 180
  • the attached image by Surfin Bird looks matching my purpose but said streching elements. I'll deeply look into this and test the code. Please wait. Thank you for sharing the link. – Kay Lee Dec 20 '17 at 00:19
  • Helli, the author in the link provided a real answer. I'll provide non-official bounty 100 by upvoting through your profile. Thank you for introduction. – Kay Lee Dec 21 '17 at 00:14