WrapPanel 滚动条

4

简单的XAML代码:

<WrapPanel Orientation="Vertical">
    <Ellipse Width="100" Height="100" Fill="Red" />
    <Ellipse Width="100" Height="100" Fill="Yellow" />
    <Ellipse Width="100" Height="100" Fill="Green" />
</WrapPanel>

调整窗口大小:

如何在内容不适配时显示垂直和水平滚动条?

注意:这对任何内容都适用。


我尝试将其放入ScrollViewer中:

<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
    <WrapPanel Orientation="Vertical">
        ...
    </WrapPanel>
</ScrollViewer>

然而,WrapPanel 停止了对任何东西进行换行(始终只有一列):

问题在于 ScrollViewer 给它的子元素一个 (NaN, NaN) 的大小,所以永远不会发生自动换行。

我尝试通过将滚动视图可用高度绑定到面板最大高度来修复它:

<ScrollViewer ...>
    <WrapPanel MaxHeight="{Binding ViewportHeight, RelativeSource={RelativeSource AncestorType=ScrollViewer}}" ...>

这将限制面板高度(不再是 NaN ),因此现在会发生换行。但是,由于这也调整了面板的高度-垂直滚动条永远不会出现:

如何添加垂直滚动条?


在我的情况下, WrapPanel 是垂直的,这意味着它会尽可能填充列,然后从左到右换行到新列。当子元素不能垂直(可用空间小于子元素高度)或水平拟合时,需要滚动条。

所想到的想法可以用于标准(水平) WrapPanel :从左到右,创建新行时已满。绝对会出现相同的问题(刚刚尝试过)。


我认为你误解了这些控件的作用。例如,如果你正在滚动,为什么需要换行呢? - Ortund
为了尽可能减少滚动,建议设置@Ortund。但是当内容不适合于一行(自动换行也无济于事)时,应该可以滚动查看。 - Sinatr
尝试将WrapPanel放置在滚动视图器内的不同容器中... ScrollViewer > Grid/DockPanel/StackPanel > WrapPanel。看看这是否会使滚动视图器滚动WrapPanel的容器而不是WrapPanel本身。我建议在WrapPanel上使用DockPanel.Dock="Bottom"的DockPanel。 - Ortund
你知道你的包装面板内容的最小高度吗?还是应该动态计算? - dymanoid
3个回答

2
那种行为在没有明确设置Vertical方向的WrapPanelHeight/MinHeightHorizontal方向的Width/MinWidth时是不可能实现的。只有当这个滚动视图包裹的FrameworkElement超出了视口范围,ScrollViewer才会显示滚动条。
你可以创建自己的包裹面板,根据其子元素计算其最小尺寸。
或者,你可以实现一个Behavior<WrapPanel>或一个附加属性。这并不像你期望的那样简单,只需添加几个XAML标记。
我们已经通过附加属性解决了这个问题。让我给你一个我们所做的想法。
static class ScrollableWrapPanel
{
    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(ScrollableWrapPanel), new PropertyMetadata(false, IsEnabledChanged));

    // DP Get/Set static methods omitted

    static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var panel = (WrapPanel)d;
        if (!panel.IsInitialized)
        {
            panel.Initialized += PanelInitialized;        
        }
        // Add here the IsEnabled == false logic, if you wish
    }

    static void PanelInitialized(object sender, EventArgs e)
    {
        var panel = (WrapPanel)sender;
        // Monitor the Orientation property.
        // There is no OrientationChanged event, so we use the DP tools.
        DependencyPropertyDescriptor.FromProperty(
            WrapPanel.OrientationProperty,
            typeof(WrapPanel))
        .AddValueChanged(panel, OrientationChanged);

        panel.Unloaded += PanelUnloaded;

        // Sets up our custom behavior for the first time
        OrientationChanged(panel, EventArgs.Empty);
    }

    static void OrientationChanged(object sender, EventArgs e)
    {
        var panel = (WrapPanel)sender;
        if (panel.Orientation == Orientation.Vertical)
        {
            // We might have set it for the Horizontal orientation
            BindingOperations.ClearBinding(panel, WrapPanel.MinWidthProperty);

            // This multi-binding monitors the heights of the children
            // and returns the maximum height.
            var converter = new MaxValueConverter();
            var minHeightBiding = new MultiBinding { Converter = converter };
            foreach (var child in panel.Children.OfType<FrameworkElement>())
            {
                minHeightBiding.Bindings.Add(new Binding("ActualHeight") { Mode = BindingMode.OneWay, Source = child });
            }

            BindingOperations.SetBinding(panel, WrapPanel.MinHeightProperty, minHeightBiding);

            // We might have set it for the Horizontal orientation        
            BindingOperations.ClearBinding(panel, WrapPanel.WidthProperty);

            // We have to define the wrap panel's height for the vertical orientation
            var binding = new Binding("ViewportHeight")
            {
                RelativeSource = new RelativeSource { Mode = RelativeSourceMode.FindAncestor, AncestorType = typeof(ScrollViewer)}
            };

            BindingOperations.SetBinding(panel, WrapPanel.HeightProperty, binding);
        }
        else
        {
            // The "transposed" case for the horizontal wrap panel
        }
    }

    static void PanelUnloaded(object sender, RoutedEventArgs e)
    {
        var panel = (WrapPanel)sender;
        panel.Unloaded -= PanelUnloaded;

        // This is really important to prevent the memory leaks.
        DependencyPropertyDescriptor.FromProperty(WrapPanel.OrientationProperty, typeof(WrapPanel))
            .RemoveValueChanged(panel, OrientationChanged);
    }

    private class MaxValueConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return values.Cast<double>().Max();
        }

        // ConvertBack omitted
    }
}

也许这不是最简单的方法,需要写比几个XAML标记更多的代码行数,但它可以无缝运行。

但是在错误处理方面要小心。在样例代码中,我省略了所有检查和异常处理。

使用方法很简单:

<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
    <WrapPanel Orientation="Vertical" local:ScrollableWrapPanel.IsEnabled="True">
    <!-- Content -->
    </WrapPanel>
</ScrollViewer

使用多绑定和多转换器的想法很有趣。这种解决方案存在一个问题 - 您没有监视Children更改(如果添加了新的大孩子),不确定是否可以轻松修复。 - Sinatr
@Sinatr,这绝对是可能的。请查看此答案。您需要观察LayoutUpdated事件。顺便说一下,在您的问题中,您没有提到您需要WrapPanel监视其子项计数。 - dymanoid
你认为我会在生产中使用红色、黄色和绿色的圆圈吗?=D 但你说得对,我有问题没有正确地提问。在问题中很容易错过“注释”,在对其他答案的评论中,我提到了MVVM(可以翻译为“许多非常非常多的绑定”)。 - Sinatr

1
你可以通过在wrappanel中包裹scrollviewer来实现此操作,但需要将内部面板的高度和宽度绑定到scrollviewer视口的高度和宽度,以便其随着屏幕的展开和收缩而伸缩。 我还在示例中添加了最小高度和宽度,这样可以确保在wrappanel被压缩到最小尺寸以下时出现滚动条。
<ScrollViewer x:Name="sv" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
    <WrapPanel MinWidth="200" Width="{Binding ElementName=sv, Path=ViewportWidth}" MinHeight="200" Height="{Binding ElementName=sv, Path=ViewportHeight}">
        <Ellipse Fill="Red" Height="200" Width="200"/>
        <Ellipse Fill="Yellow" Height="200" Width="200"/>
        <Ellipse Fill="Green" Height="200" Width="200"/>
    </WrapPanel>
</ScrollViewer>

谢谢,但我不知道MinWidthMinHeight的值。在MVVM世界中,这将是使用数据模板时的难点。除非制作一个自定义的WrapPanel来监视子元素的大小..或者使用一些事件..嗯..是否有更简单的方法? - Sinatr
通过这个解决方案,只有在窗口的高度或宽度小于WrapPanel的最小高度和宽度时,滚动条才会出现,即使在此之前您已经无法看到WrapPanel的内容。 - Daniel Marques
您是否认为,包装面板中的所有项都具有不同的高度和宽度,还是更可能它们的大小都相同(根据数据模板)? - Dave England

1

似乎对于孩子的监控是实现所需目标的重要任务之一。那为什么不创建自定义面板:

public class ColumnPanel : Panel
{
    public double ViewportHeight
    {
        get { return (double)GetValue(ViewportHeightProperty); }
        set { SetValue(ViewportHeightProperty, value); }
    }
    public static readonly DependencyProperty ViewportHeightProperty =
        DependencyProperty.Register("ViewportHeight", typeof(double), typeof(ColumnPanel),
            new FrameworkPropertyMetadata(double.PositiveInfinity, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));

    protected override Size MeasureOverride(Size constraint)
    {
        var location = new Point(0, 0);
        var size = new Size(0, 0);
        foreach (UIElement child in Children)
        {
            child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
            if (location.Y != 0 && ViewportHeight < location.Y + child.DesiredSize.Height)
            {
                location.X = size.Width;
                location.Y = 0;
            }
            if (size.Width < location.X + child.DesiredSize.Width)
                size.Width = location.X + child.DesiredSize.Width;
            if (size.Height < location.Y + child.DesiredSize.Height)
                size.Height = location.Y + child.DesiredSize.Height;
            location.Offset(0, child.DesiredSize.Height);
        }
        return size;
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        var location = new Point(0, 0);
        var size = new Size(0, 0);
        foreach (UIElement child in Children)
        {
            if (location.Y != 0 && ViewportHeight < location.Y + child.DesiredSize.Height)
            {
                location.X = size.Width;
                location.Y = 0;
            }
            child.Arrange(new Rect(location, child.DesiredSize));
            if (size.Width < location.X + child.DesiredSize.Width)
                size.Width = location.X + child.DesiredSize.Width;
            if (size.Height < location.Y + child.DesiredSize.Height)
                size.Height = location.Y + child.DesiredSize.Height;
            location.Offset(0, child.DesiredSize.Height);
        }
        return size;
    }
}

使用(而不是 WrapPanel)如下:

<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
    <local:ColumnPanel ViewportHeight="{Binding ViewportHeight, RelativeSource={RelativeSource AncestorType=ScrollViewer}}" ... >
        ...
    </local:ColumnPanel>
</ScrollViewer>

这个想法是手动计算布局,当子元素改变时(添加、删除、调整大小等),MeasureOverrideArrangeOverride将自动调用。

测量逻辑很简单:从(0,0)开始,测量下一个子元素的大小,如果它适合当前列,则添加它,否则通过偏移位置开始新列。在整个测量周期中调整结果大小。

唯一缺失的拼图部分是在测量/排列周期中提供来自父级ScrollViewerViewportHeight。这是ColumnPanel.ViewportHeight的作用。

这里是演示(添加紫色圆形按钮):


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