WPF - 如何在 WrapPanel 中居中所有项目?

Posted

技术标签:

【中文标题】WPF - 如何在 WrapPanel 中居中所有项目?【英文标题】:WPF - How can I center all items in a WrapPanel? 【发布时间】:2010-10-22 20:22:51 【问题描述】:

我使用WrapPanel 作为ItemsControlItemsPanel。现在,控件中的项目如下所示:

|1234567  |
|890      |

我希望它们像这样包装:

| 1234567 |
|   890   |

从概念上讲,布局过程应对齐每行项目,使其在WrapPanel 的边界内居中。

有人能解释一下这是怎么可能的吗?

【问题讨论】:

【参考方案1】:

内置的WrapPanel 不允许你对齐它的内容——只有它自己。这是一种允许您设置HorizontalContentAlignment的技术:

using System;
using System.Windows;
using System.Windows.Controls;

public class AlignableWrapPanel : Panel

    public HorizontalAlignment HorizontalContentAlignment
    
        get  return (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty); 
        set  SetValue(HorizontalContentAlignmentProperty, value); 
    

    public static readonly DependencyProperty HorizontalContentAlignmentProperty =
        DependencyProperty.Register("HorizontalContentAlignment", typeof(HorizontalAlignment), typeof(AlignableWrapPanel), new FrameworkPropertyMetadata(HorizontalAlignment.Left, FrameworkPropertyMetadataOptions.AffectsArrange));

    protected override Size MeasureOverride(Size constraint)
    
        Size curLineSize = new Size();
        Size panelSize = new Size();

        UIElementCollection children = base.InternalChildren;

        for (int i = 0; i < children.Count; i++)
        
            UIElement child = children[i] as UIElement;

            // Flow passes its own constraint to children
            child.Measure(constraint);
            Size sz = child.DesiredSize;

            if (curLineSize.Width + sz.Width > constraint.Width) //need to switch to another line
            
                panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
                panelSize.Height += curLineSize.Height;
                curLineSize = sz;

                if (sz.Width > constraint.Width) // if the element is wider then the constraint - give it a separate line                    
                
                    panelSize.Width = Math.Max(sz.Width, panelSize.Width);
                    panelSize.Height += sz.Height;
                    curLineSize = new Size();
                
            
            else //continue to accumulate a line
            
                curLineSize.Width += sz.Width;
                curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
            
        

        // the last line size, if any need to be added
        panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
        panelSize.Height += curLineSize.Height;

        return panelSize;
    

    protected override Size ArrangeOverride(Size arrangeBounds)
    
        int firstInLine = 0;
        Size curLineSize = new Size();
        double accumulatedHeight = 0;
        UIElementCollection children = this.InternalChildren;

        for (int i = 0; i < children.Count; i++)
        
            Size sz = children[i].DesiredSize;

            if (curLineSize.Width + sz.Width > arrangeBounds.Width) //need to switch to another line
            
                ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, i);

                accumulatedHeight += curLineSize.Height;
                curLineSize = sz;

                if (sz.Width > arrangeBounds.Width) //the element is wider then the constraint - give it a separate line                    
                
                    ArrangeLine(accumulatedHeight, sz, arrangeBounds.Width, i, ++i);
                    accumulatedHeight += sz.Height;
                    curLineSize = new Size();
                
                firstInLine = i;
            
            else //continue to accumulate a line
            
                curLineSize.Width += sz.Width;
                curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
            
        

        if (firstInLine < children.Count)
            ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, children.Count);

        return arrangeBounds;
    

    private void ArrangeLine(double y, Size lineSize, double boundsWidth, int start, int end)
    
        double x = 0;
        if (this.HorizontalContentAlignment == HorizontalAlignment.Center)
        
            x = (boundsWidth - lineSize.Width) / 2;
        
        else if (this.HorizontalContentAlignment == HorizontalAlignment.Right)
        
            x = (boundsWidth - lineSize.Width);
        

        UIElementCollection children = InternalChildren;
        for (int i = start; i < end; i++)
        
            UIElement child = children[i];
            child.Arrange(new Rect(x, y, child.DesiredSize.Width, lineSize.Height));
            x += child.DesiredSize.Width;
        
    

【讨论】:

好课,谢谢!如果孩子有一个,我已经对其进行了调整以尊重FrameworkElement.HorizontalAlignment 的值:gist.github.com/gmanny/7450651(仅更改了ArrangeLine 方法)。 对于拉伸对齐: if (i == end - 1 && Horizo​​ntalContentAlignment == Horizo​​ntalAlignment.Stretch) child.Arrange(new Rect(x, y, boundsWidth - x, lineSize.Height)); else child.Arrange(new Rect(x, y, child.DesiredSize.Width, lineSize.Height)); x += child.DesiredSize.Width; 完美的一个。谢谢 是否可以从中提取一个新方法,该方法在调用时会插入换行符?当我做了一些hacky代码来欺骗MeasureOverride认为它需要包装但我希望它在side方法中正常工作时,我只能让它这样做。 对不起。我是个十足的菜鸟。我如何使用它?【参考方案2】:

不幸的是,普通 WrapPanel 无法满足您的要求,因为它的目的是在包装之前水平(或垂直)填充所有可用空间。您可能必须设计自己的 WrapPanel 来处理这种情况。你可以从this sample that shows how to create your own WrapPanel开始。

【讨论】:

【参考方案3】:

VerticalContentAlignment 版本(用于垂直面板):

public class AlignableWrapPanel : Panel 
    public AlignableWrapPanel() 
        _orientation = Orientation.Horizontal;
    

    private static bool IsWidthHeightValid(object value) 
        var v = (double)value;
        return (double.IsNaN(v)) || (v >= 0.0d && !double.IsPositiveInfinity(v));
    

    public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double),
            typeof(AlignableWrapPanel), new FrameworkPropertyMetadata(double.NaN, FrameworkPropertyMetadataOptions.AffectsMeasure),
            IsWidthHeightValid);

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

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

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

    public static readonly DependencyProperty OrientationProperty = StackPanel.OrientationProperty.AddOwner(typeof(AlignableWrapPanel),
            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) 
        var p = (AlignableWrapPanel)d;
        p._orientation = (Orientation)e.NewValue;
    

    private Orientation _orientation;

    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); 
            private set  if (_orientation == Orientation.Horizontal) U = value; else V = value; 
        

        internal double Height 
            get  return (_orientation == Orientation.Horizontal ? V : U); 
            private 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)) continue;
                //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 
                ArrangeLine(finalSize, accumulatedV, curLineSize, firstInLine, i, useItemU, itemU);

                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 
                    ArrangeLine(finalSize, accumulatedV, sz, i, ++i, useItemU, itemU);

                    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) 
            ArrangeLine(finalSize, accumulatedV, curLineSize, firstInLine, children.Count, useItemU, itemU);
        

        return finalSize;
    

    private void ArrangeLine(Size finalSize, double v, UvSize line, int start, int end, bool useItemU, double itemU) 
        double u;
        var isHorizontal = Orientation == Orientation.Horizontal;

        if (_orientation == Orientation.Vertical) 
            switch (VerticalContentAlignment) 
                case VerticalAlignment.Center:
                    u = (finalSize.Height - line.U) / 2;
                    break;
                case VerticalAlignment.Bottom:
                    u = finalSize.Height - line.U;
                    break;
                default:
                    u = 0;
                    break;
            
         else 
            switch (HorizontalContentAlignment) 
                case HorizontalAlignment.Center:
                    u = (finalSize.Width - line.U) / 2;
                    break;
                case HorizontalAlignment.Right:
                    u = finalSize.Width - line.U;
                    break;
                default:
                    u = 0;
                    break;
            
        

        var children = InternalChildren;
        for (var i = start; i < end; i++) 
            var child = children[i];
            if (child == null) continue;
            var childSize = new UvSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
            var layoutSlotU = (useItemU ? itemU : childSize.U);
            child.Arrange(new Rect(
                    isHorizontal ? u : v,
                    isHorizontal ? v : u,
                    isHorizontal ? layoutSlotU : line.V,
                    isHorizontal ? line.V : layoutSlotU));
            u += layoutSlotU;
        
    

    public static readonly DependencyProperty HorizontalContentAlignmentProperty = DependencyProperty.Register(nameof(HorizontalContentAlignment), typeof(HorizontalAlignment),
            typeof(AlignableWrapPanel), new FrameworkPropertyMetadata(HorizontalAlignment.Left, FrameworkPropertyMetadataOptions.AffectsArrange));

    public HorizontalAlignment HorizontalContentAlignment 
        get  return (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty); 
        set  SetValue(HorizontalContentAlignmentProperty, value); 
    

    public static readonly DependencyProperty VerticalContentAlignmentProperty = DependencyProperty.Register(nameof(VerticalContentAlignment), typeof(VerticalAlignment),
            typeof(AlignableWrapPanel), new FrameworkPropertyMetadata(VerticalAlignment.Top, FrameworkPropertyMetadataOptions.AffectsArrange));

    public VerticalAlignment VerticalContentAlignment 
        get  return (VerticalAlignment)GetValue(VerticalContentAlignmentProperty); 
        set  SetValue(VerticalContentAlignmentProperty, value); 
    

【讨论】:

由于某种原因,此解决方案有效,但接受的答案无效。谢谢你。【参考方案4】:

简单的答案是您无法将 WrapPanel 的内容居中对齐。您可以将面板本身居中对齐,但最后一行仍将在面板内左对齐。

改变建议:

对行和列使用网格。如果您不是动态地将项目添加到集合中,这可能会很好地工作。

创建您自己的 WrapPanel 版本,按照您需要的方式工作。 This MSDN document 描述了面板的工作原理,并包含有关创建自定义面板的部分。它还有一个指向示例自定义面板的链接。

【讨论】:

【参考方案5】:

这是 Silverlight 版本

特别感谢@DTig

using System.Windows.Controls;
using System.Windows;
using Telerik.Windows;
using System;
using System.Linq;

public class AlignableWrapPanel : Panel

    public HorizontalAlignment HorizontalContentAlignment
    
        get  return (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty); 
        set  SetValue(HorizontalContentAlignmentProperty, value); 
    

    public static readonly DependencyProperty HorizontalContentAlignmentProperty =
        DependencyProperty.Register("HorizontalContentAlignment", typeof(HorizontalAlignment), typeof(AlignableWrapPanel), new FrameworkPropertyMetadata(HorizontalAlignment.Left, FrameworkPropertyMetadataOptions.AffectsArrange));

    protected override Size MeasureOverride(Size constraint)
    
        Size curLineSize = new Size();
        Size panelSize = new Size();

        UIElementCollection children = base.Children;

        for (int i = 0; i < children.Count; i++)
        
            UIElement child = children[i] as UIElement;

            // Flow passes its own constraint to children
            child.Measure(constraint);
            Size sz = child.DesiredSize;

            if (curLineSize.Width + sz.Width > constraint.Width) //need to switch to another line
            
                panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
                panelSize.Height += curLineSize.Height;
                curLineSize = sz;

                if (sz.Width > constraint.Width) // if the element is wider then the constraint - give it a separate line                    
                
                    panelSize.Width = Math.Max(sz.Width, panelSize.Width);
                    panelSize.Height += sz.Height;
                    curLineSize = new Size();
                
            
            else //continue to accumulate a line
            
                curLineSize.Width += sz.Width;
                curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
            
        

        // the last line size, if any need to be added
        panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
        panelSize.Height += curLineSize.Height;

        return panelSize;
    

    protected override Size ArrangeOverride(Size arrangeBounds)
    
        int firstInLine = 0;
        Size curLineSize = new Size();
        double accumulatedHeight = 0;
        UIElementCollection children = this.Children;

        for (int i = 0; i < children.Count; i++)
        
            Size sz = children[i].DesiredSize;

            if (curLineSize.Width + sz.Width > arrangeBounds.Width) //need to switch to another line
            
                ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, i);

                accumulatedHeight += curLineSize.Height;
                curLineSize = sz;

                if (sz.Width > arrangeBounds.Width) //the element is wider then the constraint - give it a separate line                    
                
                    ArrangeLine(accumulatedHeight, sz, arrangeBounds.Width, i, ++i);
                    accumulatedHeight += sz.Height;
                    curLineSize = new Size();
                
                firstInLine = i;
            
            else //continue to accumulate a line
            
                curLineSize.Width += sz.Width;
                curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
            
        

        if (firstInLine < children.Count)
            ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, children.Count);

        return arrangeBounds;
    

    private void ArrangeLine(double y, Size lineSize, double boundsWidth, int start, int end)
    
        double x = 0;
        if (this.HorizontalContentAlignment == HorizontalAlignment.Center)
        
            x = (boundsWidth - lineSize.Width) / 2;
        
        else if (this.HorizontalContentAlignment == HorizontalAlignment.Right)
        
            x = (boundsWidth - lineSize.Width);
        

        UIElementCollection children = Children;
        for (int i = start; i < end; i++)
        
            UIElement child = children[i];
            var rect = new System.Windows.Rect(x, y, child.DesiredSize.Width, lineSize.Height);
            child.Arrange(rect);
            x += child.DesiredSize.Width;
        
    

【讨论】:

【参考方案6】:

我需要一个可以拉伸其内容的 WrapPanel,因此基于https://***.com/a/7747002/121122 和一些摆弄我想出了这个:

public class AlignableWrapPanel : Panel

    public HorizontalAlignment HorizontalContentAlignment
    
        get => (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty);
        set => SetValue(HorizontalContentAlignmentProperty, value);
    

    public static readonly DependencyProperty HorizontalContentAlignmentProperty = DependencyProperty.Register(
        nameof(HorizontalContentAlignment),
        typeof(HorizontalAlignment),
        typeof(AlignableWrapPanel),
        new FrameworkPropertyMetadata(HorizontalAlignment.Left, FrameworkPropertyMetadataOptions.AffectsArrange));

    protected override Size MeasureOverride(Size constraint)
    
        var curLineSize = new Size();
        var panelSize = new Size();

        var children = InternalChildren;

        for (var i = 0; i < children.Count; i++)
        
            var child = children[i];

            // flow passes its own constraint to children
            child.Measure(constraint);
            var sz = child.DesiredSize;

            if (curLineSize.Width + sz.Width > constraint.Width) // need to switch to another line
            
                panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
                panelSize.Height += curLineSize.Height;
                curLineSize = sz;

                if (sz.Width > constraint.Width) // if the element is wider then the constraint - give it a separate line                    
                
                    panelSize.Width = Math.Max(sz.Width, panelSize.Width);
                    panelSize.Height += sz.Height;
                    curLineSize = new Size();
                
            
            else // continue to add to the line
            
                curLineSize.Width += sz.Width;
                curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
            
        

        // the last line size, if any need to be added
        panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
        panelSize.Height += curLineSize.Height;

        return panelSize;
    

    protected override Size ArrangeOverride(Size arrangeBounds)
    
        var firstInLine = 0;
        var curLineSize = new Size();
        var accumulatedHeight = 0D;
        var children = InternalChildren;

        for (var i = 0; i < children.Count; i++)
        
            var desiredSize = children[i].DesiredSize;

            if (curLineSize.Width + desiredSize.Width > arrangeBounds.Width) // need to switch to another line
            
                ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, i);

                accumulatedHeight += curLineSize.Height;
                curLineSize = desiredSize;

                if(
                    desiredSize.Width > arrangeBounds.Width || 
                    children[i] is FrameworkElement element && element.HorizontalAlignment == HorizontalAlignment.Stretch)
                
                    // the element is wider then the constraint or it stretches - give it a separate line
                    ArrangeLine(accumulatedHeight, desiredSize, arrangeBounds.Width, i, ++i);
                    accumulatedHeight += desiredSize.Height;
                    curLineSize = new Size();
                
                firstInLine = i;
            
            else // continue to add to the line
            
                curLineSize.Width += desiredSize.Width;
                curLineSize.Height = Math.Max(desiredSize.Height, curLineSize.Height);
            
        

        if (firstInLine < children.Count)
        
            ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, children.Count);
        

        return arrangeBounds;
    

    private void ArrangeLine(double y, Size lineSize, double boundsWidth, int start, int end)
    
        var children = InternalChildren;

        var x = 0D;
        if (HorizontalContentAlignment == HorizontalAlignment.Center)
        
            x = (boundsWidth - lineSize.Width) / 2;
        
        else if (HorizontalContentAlignment == HorizontalAlignment.Right)
        
            x = boundsWidth - lineSize.Width;
        

        var stretchChildren = new List<UIElement>();
        for (var i = start; i < end; i++)
        
            var child = children[i];
            if (child is FrameworkElement element && element.HorizontalAlignment == HorizontalAlignment.Stretch)
            
                stretchChildren.Add(child);
            
        

        var spaceAvailableForStretchChildren = boundsWidth - lineSize.Width;
        var spaceAvailablePerStretchChild = 0D;
        if (stretchChildren.Any())
        
            x = 0; // all available space will be filled so start at 0
            spaceAvailablePerStretchChild = spaceAvailableForStretchChildren / stretchChildren.Count;
            spaceAvailablePerStretchChild = spaceAvailablePerStretchChild >= 0 ? spaceAvailablePerStretchChild : 0;
        

        for (var i = start; i < end; i++)
        
            var child = children[i];
            double itemWidth;
            if(stretchChildren.Contains(child))
            
                itemWidth = child.DesiredSize.Width + spaceAvailablePerStretchChild;
            
            else
            
                itemWidth = child.DesiredSize.Width;
            

            child.Arrange(new Rect(x, y, itemWidth, lineSize.Height));
            x += itemWidth;
        
    

【讨论】:

以上是关于WPF - 如何在 WrapPanel 中居中所有项目?的主要内容,如果未能解决你的问题,请参考以下文章

WPF: WrapPanel 容器的数据绑定(动态生成控件遍历)

WPF - 如何在 ListView 中居中列数据?

WPF WrapPanel与Grid布局

使用 WrapPanel 和 ScrollViewer 在 WPF 中提供多列列表框

WPF wrappanel:管理水平和垂直行为

WPF:带有 WrapPanel 的 ListBox,垂直滚动问题