23

I'd like to create an ItemsControl where child items are placed like a WrapPanel, but child Items should take as much space as it can. So that when the window size gets larger or smaller, the child items should stretch according to a certain width:height ratio. When the child items get added or removed from the ItemsControl's ItemsSource, the WrapPanel should place linebreaks among items appropriately to keep child item's width:height ratio.
Below is what I have so far. Is it possible to do this in Xaml? or should I create a custom control for this? Thanks in advance!

<Window>
<Grid>
    <ItemsControl ItemsSource="{Binding DataCollection}">
       <ItemsControl.ItemsPanel>
          <ItemsPanelTemplate>
             <WrapPanel Orientation="Vertical"/>
          </ItemsPanelTemplate>
       </ItemsControl.ItemsPanel>
       <ItemsControl.ItemTemplate>
          <DataTemplate>
             <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Stretch">
                <StackPanel Orientation="Horizontal" >
                   <TextBlock TextWrapping="Wrap" Text="{Binding Name}" />
                   <TextBlock TextWrapping="Wrap" Text="{Binding Value}"/>
                   <TextBlock TextWrapping="Wrap" Text="{Binding Time,  StringFormat='hh:mm:ss' }"/>
                </StackPanel>
             </Border>          
         </DataTemplate>
       </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>
</Window>
Yeonho
  • 3,629
  • 4
  • 39
  • 61
  • 2
    Does unlimited vertical orientation UniformGrid serve ur purpose... http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/d3895113-5157-40ba-9313-3fdefe649f9f/ – WPF-it Nov 04 '11 at 06:08
  • I think it's somewhat different from what I need, but thanks! – Yeonho Nov 04 '11 at 06:34
  • Wrap and stack panels intentionally take up as little space as possible. As far as I've seen you can't override that behavior, so you must use a different type of grouping control. – Merlyn Morgan-Graham Nov 16 '11 at 01:18

4 Answers4

21

Use a UniformGrid rather than a WrapPanel. Just set the number of columns you want with the Columns property, and it should give you the desired result.

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • Slightly extending @Thomas Levesque's answer: By setting Columns=1 explicitly makes WrapPanel to set each child item into a separate row. Like this: – Gsv May 17 '20 at 10:59
  • This works for width, but then sets the height of each child to the same value even if it's wildly a waste of space. – not_a_generic_user May 11 '22 at 20:29
  • If you want both uniform width and wrapping behaviour, bind Columns to a property that returns something like MainWindow.ActualWidth / 150, and raise property changed event on SizeChanged event. This way you can have dynamic column count based on a minimum width. – marczellm May 28 '23 at 09:12
10

As another solution (sometimes width might be different, so UniformGrid won’t work), here is modified WrapPanel which would stretch elements to fit lines (while keeping proportion the same):

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

    [TypeConverter(typeof(LengthConverter))]
    public double ItemWidth {
        get { return (double)GetValue(ItemWidthProperty); }
        set { SetValue(ItemWidthProperty, value); }
    }

    public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register(nameof(ItemHeight),
            typeof(double), typeof(StretchyWrapPanel), new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure));

    [TypeConverter(typeof(LengthConverter))]
    public double ItemHeight {
        get { return (double)GetValue(ItemHeightProperty); }
        set { SetValue(ItemHeightProperty, value); }
    }

    public static readonly DependencyProperty OrientationProperty = StackPanel.OrientationProperty.AddOwner(typeof(StretchyWrapPanel),
            new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure, OnOrientationChanged));

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

    private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
        ((StretchyWrapPanel)d)._orientation = (Orientation)e.NewValue;
    }

    private Orientation _orientation = Orientation.Horizontal;

    public static readonly DependencyProperty StretchProportionallyProperty = DependencyProperty.Register(nameof(StretchProportionally), typeof(bool),
            typeof(StretchyWrapPanel), new PropertyMetadata(true, OnStretchProportionallyChanged));

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

    private static void OnStretchProportionallyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
        ((StretchyWrapPanel)o)._stretchProportionally = (bool)e.NewValue;
    }

    private bool _stretchProportionally = true;

    private struct UVSize {
        internal UVSize(Orientation orientation, double width, double height) {
            U = V = 0d;
            _orientation = orientation;
            Width = width;
            Height = height;
        }

        internal UVSize(Orientation orientation) {
            U = V = 0d;
            _orientation = orientation;
        }

        internal double U;
        internal double V;
        private readonly Orientation _orientation;

        internal double Width {
            get { return _orientation == Orientation.Horizontal ? U : V; }
            set {
                if (_orientation == Orientation.Horizontal) {
                    U = value;
                } else {
                    V = value;
                }
            }
        }

        internal double Height {
            get { return _orientation == Orientation.Horizontal ? V : U; }
            set {
                if (_orientation == Orientation.Horizontal) {
                    V = value;
                } else {
                    U = value;
                }
            }
        }
    }

    protected override Size MeasureOverride(Size constraint) {
        var curLineSize = new UVSize(Orientation);
        var panelSize = new UVSize(Orientation);
        var uvConstraint = new UVSize(Orientation, constraint.Width, constraint.Height);
        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 > uvConstraint.U) {
                // Need to switch to another line
                panelSize.U = Math.Max(curLineSize.U, panelSize.U);
                panelSize.V += curLineSize.V;
                curLineSize = sz;

                if (sz.U > uvConstraint.U) {
                    // 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);
    }

    protected override Size ArrangeOverride(Size finalSize) {
        var firstInLine = 0;
        var itemWidth = ItemWidth;
        var itemHeight = ItemHeight;
        double accumulatedV = 0;
        var itemU = Orientation == Orientation.Horizontal ? itemWidth : itemHeight;
        var curLineSize = new UVSize(Orientation);
        var uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height);
        var itemWidthSet = !double.IsNaN(itemWidth);
        var itemHeightSet = !double.IsNaN(itemHeight);
        var useItemU = Orientation == Orientation.Horizontal ? itemWidthSet : itemHeightSet;

        var children = InternalChildren;

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

            var sz = new UVSize(Orientation, itemWidthSet ? itemWidth : child.DesiredSize.Width, itemHeightSet ? itemHeight : child.DesiredSize.Height);
            if (curLineSize.U + sz.U > uvFinalSize.U) {
                // Need to switch to another line
                if (!useItemU && StretchProportionally) {
                    ArrangeLineProportionally(accumulatedV, curLineSize.V, firstInLine, i, uvFinalSize.Width);
                } else {
                    ArrangeLine(accumulatedV, curLineSize.V, firstInLine, i, true, useItemU ? itemU : uvFinalSize.Width / Math.Max(1, i - firstInLine - 1));
                }

                accumulatedV += curLineSize.V;
                curLineSize = sz;

                if (sz.U > uvFinalSize.U) {
                    // The element is wider then the constraint - give it a separate line     
                    // Switch to next line which only contain one element
                    if (!useItemU && StretchProportionally) {
                        ArrangeLineProportionally(accumulatedV, sz.V, i, ++i, uvFinalSize.Width);
                    } else {
                        ArrangeLine(accumulatedV, sz.V, i, ++i, true, useItemU ? itemU : uvFinalSize.Width);
                    }

                    accumulatedV += sz.V;
                    curLineSize = new UVSize(Orientation);
                }
                firstInLine = i;
            } else {
                // Continue to accumulate a line
                curLineSize.U += sz.U;
                curLineSize.V = Math.Max(sz.V, curLineSize.V);
            }
        }

        // Arrange the last line, if any
        if (firstInLine < children.Count) {
            if (!useItemU && StretchProportionally) {
                ArrangeLineProportionally(accumulatedV, curLineSize.V, firstInLine, children.Count, uvFinalSize.Width);
            } else {
                ArrangeLine(accumulatedV, curLineSize.V, firstInLine, children.Count, true,
                        useItemU ? itemU : uvFinalSize.Width / Math.Max(1, children.Count - firstInLine - 1));
            }
        }

        return finalSize;
    }

    private void ArrangeLineProportionally(double v, double lineV, int start, int end, double limitU) {
        var u = 0d;
        var horizontal = Orientation == Orientation.Horizontal;
        var children = InternalChildren;

        var total = 0d;
        for (var i = start; i < end; i++) {
            total += horizontal ? children[i].DesiredSize.Width : children[i].DesiredSize.Height;
        }

        var uMultipler = limitU / total;
        for (var i = start; i < end; i++) {
            var child = children[i];
            if (child != null) {
                var childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
                var layoutSlotU = childSize.U * uMultipler;
                child.Arrange(new Rect(horizontal ? u : v, horizontal ? v : u,
                        horizontal ? layoutSlotU : lineV, horizontal ? lineV : layoutSlotU));
                u += layoutSlotU;
            }
        }
    }

    private void ArrangeLine(double v, double lineV, int start, int end, bool useItemU, double itemU) {
        var u = 0d;
        var horizontal = Orientation == Orientation.Horizontal;
        var children = InternalChildren;
        for (var i = start; i < end; i++) {
            var child = children[i];
            if (child != null) {
                var childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
                var layoutSlotU = useItemU ? itemU : childSize.U;
                child.Arrange(new Rect(horizontal ? u : v, horizontal ? v : u,
                        horizontal ? layoutSlotU : lineV, horizontal ? lineV : layoutSlotU));
                u += layoutSlotU;
            }
        }
    }
}

In action:

List of stretched tags

Surfin Bird
  • 488
  • 7
  • 16
  • Hello Mr. Bird, somebody provided a link to here but I think your solution might be one of candidate if modified. I'll deeply look into and test your code with some doubt due to your comment 'stretch elements'. My case is that all heights are same, different length of text. The image you attached seems matching well. Do you think your code is direct solution to my case? Politely, can you please help some advice? Thank you so much ! https://stackoverflow.com/questions/47843080/is-there-any-way-to-occupy-blank-space-in-wrappanel-automatically – Kay Lee Dec 20 '17 at 03:48
  • @KayLee sorry if “stretch elements” was misguiding, I didn’t mean scaling, if this is what you mean. Instead, it simply increases widths, either proportionally or to fixed amount for each element. If, instead, you want to rearrange those blank spaces on the ends of each lines evenly between the elements, I can adjust the code and copy it as a response to your question. – Surfin Bird Dec 20 '17 at 11:38
  • Oh, many thanks for your suggestion. Can you understand exactly what I need? Because my panel has limited, small space, I have to utilize the blank space to display things as many as possible.Like your image, All children are buttons with different length according to contained text like your france, future, chocolate, kiss and so on. I have to provide a bounty who provided the link but if you make an answer to my question, I'll upvote your answers through your profile as non-official bounty because it would be the real solution for me. Thank you ! – Kay Lee Dec 20 '17 at 12:17
  • To help you to understand what I want, if I have a blank space(4 text length) and I have an element with text 'star', I want the panel to re-arrange the 'star' element into the blank space(4 text length) to maximize the capacity of limited space. If the length of text is less than 4 like 3, 2, it's ok but 4 length element has higher priority as efficiency. It's simple idea..Because my major is just bio-chemistry and self-learning with internet for non-profit project, customization by myself would be really difficult. I deeply appreciate your time and contribution. – Kay Lee Dec 20 '17 at 12:33
  • @KayLee oh, I see what you mean now… That’s an interesting task, I’ll try to find a way to solve it. – Surfin Bird Dec 20 '17 at 12:54
  • I deeply appreciate no matter what the result will be. Just many thanks for kind and warm someone in somewhere on the earth.. – Kay Lee Dec 20 '17 at 12:59
  • @KayLee almost there… I’m using FFDH algorithm from [here](https://stackoverflow.com/a/4264497/4267982) for now. Sorry for the delay, there were some issues I found in `StretchyWrapPanel` I need to fix first (that rearrangement will be just a new option). – Surfin Bird Dec 20 '17 at 16:19
  • Having a very similar need,I just copied/pasted your Control into my solution : works like a charm. Only had to modifiy a little my ItemTemplates. Great job, thanks – xum59 Jul 01 '19 at 08:51
1

To achieve the desired behavior you may try to use Width or MaxWidth properties of WrapPanel, for example by this way

        <ListView
            Name="RootControl"
            ItemsSource="{Binding [icons]}"
            ItemTemplate="{StaticResource IconTemplate}">
            <ListView.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel
                        Width="{Binding ElementName=RootControl, Path=ActualWidth}"/>
                </ItemsPanelTemplate>
            </ListView.ItemsPanel>
        </ListView>

It works enter image description here

Makeman
  • 884
  • 9
  • 16
-2

Have you tried the HorizontalContentAlignment="Stretch" property in the itemscontrol.

Tan
  • 2,148
  • 3
  • 33
  • 54