自定义 WPF WrapPanel 通过重写 OnVisualChildrenChanged 方法无法按预期工作

3
我正在Windows上开发一个应用程序,我想要一个Panel,可以对其子元素进行如下布局:
https://istack.dev59.com/fg87q.webp 在WPF Panel中,最接近的是垂直方向上的WrapPanel,其布局如下:
https://istack.dev59.com/0GJ78.webp 到目前为止,我的解决方案是创建一个继承自WrapPanel的派生类,并将它及其每个子元素旋转180度,以实现我的目标。
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);
    }
}

问题在于重写的OnVisualChildrenChanged中的addedChild.RenderTransform = new RotateTransform(180) 部分似乎没有起作用。欢迎提供任何解决方案(即使与我的方法无关)。
更新:
我重新检查了代码,并意识到我在其他地方更改了RenderTransform,这阻止了它的工作。因此,我的问题已经解决了。但是,如果您有更加简洁的解决方案,例如使用MeasureOverride/ArrangeOverride,我会很感激。

我刚刚将你的代码粘贴到一个新的项目中,它运行得很好... - Grx70
@Grx70,谢谢您的评论。请参考我在帖子中添加的更新部分。 - Pejman
1
可以从头开始创建自定义面板来覆盖MeasureOverrideArrangeOverride方法(这也许是可行的甚至是建议的),但要做到这一点,您必须更具体地了解您的需求。例如,如果只有8个项目,哪个“瓷砖”是空的?并且所有瓷砖的大小都相同吗? - Grx70
@Grx70 我希望的项目顺序与我在第一张图片中描述的完全相同。所以,如果只有8个项目,第9个瓷砖将会是空的。实际上,我希望能够设置行和列的数量,并且是的,所有的瓷砖都是相同大小的。实际上,现在我在考虑也许一个按照我希望的项目顺序排列的统一网格可以达到目的。 - Pejman
需要通过UI来完成这个吗?难道你不能简单地反转绑定的集合/视图模型吗? - bic
@bic 不仅仅是顺序问题。我希望我的项目从底部开始堆叠,并且我想能够设置行限制。一旦达到限制,我希望项目从现有列的左侧重新堆叠到顶部。 - Pejman
1个回答

2
根据您的要求,以下是我认为可以满足您需求的自定义面板。它按照从底部到顶部、从右到左的方式排列子元素,在一列中堆叠最多达到MaxRows个元素(如果是null,则为所有元素)。所有插槽的大小相同。它还考虑了元素的Visibility值,即如果一个项目是Hidden,它会留下一个空插槽;如果它是Collapsed,则会跳过该元素,下一个元素会“跳”到它的位置。
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;
    }
}

运行得非常好! - Pejman
我正在尝试学习 MeasureOverride。在这种情况下,为什么 MeasureOverride 返回子元素中最大的宽度和最大的高度?当然,面板的期望大小是渲染所有子元素的整个区域,以便以排列的形式呈现它们......? - Tormod
1
@Tormod 很好的问题。经验法则是MeasureOverride应该返回元素的期望大小。在这种特殊情况下,我理解原始问题的期望结果是一种具有统一单元格大小的网格,可以容纳其所有内容。这就是为什么我的答案说“所有插槽的大小都相同。”。因此,简而言之,这是不会剪切任何子元素的最小大小。但是一般情况下可能不是这样的。 - Grx70
1
@Tormod 但是要详细说明一下 - 据我所知(已经有一段时间了),MeasureOverride 不需要观察 constraint 参数。我认为它只是一个提示,告诉你控件有多少可用空间,但你总可以说“是的,这不够”。通常,作为 ArrangeOverride 的结果参数,你会得到“好吧,那就难办了”的 finalSize 值,你需要“挤压”内容以适应任何你喜欢的大小或所有需要的空间,但你不应该期望控件完全呈现。我认为这就是 ScrollPanel 发生的事情。 - Grx70
谢谢您的澄清。您实际上告诉了我比我为这个问题制作的主题更多的内容。如果您有更多要补充的话,在这里添加就好了:https://dev59.com/jcDqa4cB1Zd3GeqPVA6N - Tormod

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接