WrapPanel滚动条

时间:2017-07-25 12:53:42

标签: c# wpf layout wrappanel

简单的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:从左到右,在完整时创建新行。绝对会出现同样的问题(只是试了一下)。

3 个答案:

答案 0 :(得分:1)

您可以通过将包装带包装在滚动查看器中,然后将内部面板的高度和宽度绑定到滚动查看器的视口的高度和宽度来实现此目的,这样它就会拉伸并与屏幕的其余部分收缩。 我还为我的样本添加了最小高度和宽度,这确保一旦将面板推到小于其最小尺寸时滚动条就会出现

<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>

答案 1 :(得分:1)

如果WrapPanel没有明确指定Height / MinHeight Vertical方向或Width / {{1>,那么这种行为就无法实现对于MinWidth方向。当Horizontal此滚动查看器包装不适合视口时,ScrollViewer将仅显示滚动条。

您可以创建自己的包装面板,根据其子项计算其最小尺寸。

或者,您可以实施FrameworkElement或附加属性。如您所料,这不仅仅是添加几个XAML标签一样简单。

我们已使用附加属性解决了此问题。让我告诉你我们做了什么。

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
    }
}

答案 2 :(得分:0)

对儿童的监控似乎是实现通缉的重要任务之一。那么为什么不创建自定义面板:

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)开始并测量下一个子尺寸,如果它适合当前列 - 添加它,否则通过偏移位置开始和新列。在整个测量周期中,调整最终尺寸。

谜题中唯一缺失的部分是提供来自父ViewportHeight的衡量/安排周期ScrollViewer。这是ColumnPanel.ViewportHeight的作用。

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