3

I am working on an app on Windows, and I want a Panel that layouts its children like this:

This is my desired layout

The closest I find in WPF panels is a WrapPanel with a vertical orientation which has a layout like this:

This is WrapPanel (with vertical orientation) layout

My solution so far has been creating a derived class from WrapPanel and rotating it and each of its children 180 degrees so that my goal is achieved:

public class NotificationWrapPanel : WrapPanel
{
    public NotificationWrapPanel()
    {
        this.Orientation = Orientation.Vertical;
        this.RenderTransformOrigin = new Point(.5, .5);
        this.RenderTransform = new RotateTransform(180);
    }
    protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
    {
        var addedChild = visualAdded as UIElement;
        if (addedChild != null)
        {
            addedChild.RenderTransformOrigin = new Point(.5, .5);
            addedChild.RenderTransform = new RotateTransform(180);
        }

        base.OnVisualChildrenChanged(addedChild, visualRemoved);
    }
}

The problem is that the addedChild.RenderTransform = new RotateTransform(180) part in the overridden OnVisualChildrenChanged does not seem to work. Any solutions (even very unrelated to my approach) are welcomed.

UPDATE:

I re-examined my code and realized I am changing RenderTransform somewhere else, preventing this from working. So my problem is solved. However, I'd appreciate if you offer any solutions that may be more elegant, e.g. using MeasureOverride/ArrangeOverride.

Pejman
  • 3,784
  • 4
  • 24
  • 33
  • I just pasted your code into a fresh project and it works fine... – Grx70 Aug 13 '17 at 11:37
  • @Grx70 Yeah thanks for the comment. Please refer to UPDATE section I added to my post. – Pejman Aug 13 '17 at 12:22
  • 1
    It's possible (and perhaps even advisable) to create custom panel from scratch overriding both `MeasureOverride` and `ArrangeOverride` methods, but to do that you'd have to be more specific about your requirements. E.g. if there are only 8 items, which "tile" is vacant? And are all the tiles of the same size? – Grx70 Aug 13 '17 at 13:42
  • @Grx70 My desired item order is exactly what I depicted in first image. So, in case of only 8 items, tile 9 will be vacant. Actually I would like to be able to set a row and column count, and yeah, all tiles are of the same size. Actually now I'm thinking maybe a uniform grid with my desired item order would do the trick. – Pejman Aug 13 '17 at 20:23
  • Is it necessary to do this through the UI? Could you not just simply reverse the bound Collection/ViewModels? – bic Aug 13 '17 at 22:45
  • @bic It is not just the sequential order. I want my items to stack from bottom to top, and I want to be able to set a row limit. And once the limit is reached, I want the items to stack from the left of the existing column to top again. – Pejman Aug 14 '17 at 06:28

1 Answers1

2

Based on your requirements following is a custom panel that I think should get you what you're after. It arranges child elements in bottom-to-top then right-to-left manner, stacking up to MaxRows elements in a column (or all elements if it is null). All slots are of the same size. It also does take into account elements' Visibility value, i.e. if an item is Hidden, it leaves an empty slot, and if it is Collapsed, it is skipped and next element "jumps" into its place.

public class NotificationWrapPanel : Panel
{
    public static readonly DependencyProperty MaxRowsProperty =
        DependencyProperty.Register(
            name: nameof(MaxRows),
            propertyType: typeof(int?),
            ownerType: typeof(NotificationWrapPanel),
            typeMetadata: new FrameworkPropertyMetadata
            {
                AffectsArrange = true,
                AffectsMeasure = true,
            },
            validateValueCallback: o => o == null || (int)o >= 1);

    public int? MaxRows
    {
        get { return (int?)GetValue(MaxRowsProperty); }
        set { SetValue(MaxRowsProperty, value); }
    }

    protected override Size MeasureOverride(Size constraint)
    {
        var children = InternalChildren
            .Cast<UIElement>()
            .Where(e => e.Visibility != Visibility.Collapsed)
            .ToList();
        if (children.Count == 0) return new Size();
        var rows = MaxRows.HasValue ?
            Math.Min(MaxRows.Value, children.Count) :
            children.Count;
        var columns = children.Count / rows +
            Math.Sign(children.Count % rows);
        var childConstraint = new Size
        {
            Width = constraint.Width / columns,
            Height = constraint.Height / rows,
        };
        foreach (UIElement child in children)
            child.Measure(childConstraint);
        return new Size
        {
            Height = rows * children
                .Cast<UIElement>()
                .Max(e => e.DesiredSize.Height),
            Width = columns * children
                .Cast<UIElement>()
                .Max(e => e.DesiredSize.Width),
        };
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        var children = InternalChildren
            .Cast<UIElement>()
            .Where(e => e.Visibility != Visibility.Collapsed)
            .ToList();
        if (children.Count == 0) return finalSize;
        var rows = MaxRows.HasValue ?
            Math.Min(MaxRows.Value, children.Count) : 
            children.Count;
        var columns = children.Count / rows +
            Math.Sign(children.Count % rows);
        var childSize = new Size
        {
            Width = finalSize.Width / columns,
            Height = finalSize.Height / rows
        };
        for (int i = 0; i < children.Count; i++)
        {
            var row = i % rows; //rows are numbered bottom-to-top
            var col = i / rows; //columns are numbered right-to-left
            var location = new Point
            {
                X = finalSize.Width - (col + 1) * childSize.Width,
                Y = finalSize.Height - (row + 1) * childSize.Height,
            };
            children[i].Arrange(new Rect(location, childSize));
        }
        return finalSize;
    }
}
Grx70
  • 10,041
  • 1
  • 40
  • 55
  • Works like a charm! – Pejman Aug 14 '17 at 13:02
  • I'm trying to learn the MeasureOverride. In this case, why does the MeasureOverride return the largest width and largest height among the children? Surely, the desired size of the panel is the entire area it needs to render all of them in arranged form....? – Tormod Mar 26 '21 at 13:13
  • 1
    @Tormod Good question. The rule of thumb is that `MeasureOverride` should return the desired size of the element. In this particular case my understanding of the original question was that the desired result was a sort of a grid with uniform cell size that could accommodate all of its contents. That's why my answer says "All slots are of the same size.". So in short this is the minimal size that would not clip any of the child elements. But in general this might not be the case. – Grx70 Mar 26 '21 at 18:52
  • 1
    @Tormod But to elaborate - AFAIK (and it's been a while) `MeasureOverride` does not need to observe the `constraint` parameter. I think it's just a hint for how much space is available for your control, but you can always say "yeah, that's not enough". Typically, as a resulting parameter of `ArrangeOverride` you'll get "well that's tough, deal with it" `finalSize` value, and you're expected to "squeeze" the contents to fit the size however you like, or all the space you need, but you should not expect the control to be rendered in its entirety. That's what I think happens with `ScrollPanel`. – Grx70 Mar 26 '21 at 19:02
  • Thank you for the clarification. You are actually telling me more than the topic I made for the question. If you have more to add here, it would be great: https://stackoverflow.com/questions/66818340/measureoverride-and-arrangeoverride-what-is-really-availablesize-desiredsize – Tormod Mar 27 '21 at 19:15