WrapPanel scrollbars

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

Resizing window:

How to show vertical and horizontal scrollbars when content doesn't fit?

Note: this should work for any content.


I tried to put it into ScrollViewer :

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

but then WrapPanel stops wrapping anything (always one column):

The problem here is that ScrollViewer gives (NaN, NaN) size to it child, so wrapping never occurs.

I tried to fix it by binding scroll viewer available height to max height of panel:

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

This will limit panel height (not NaN anymore), so wrapping now occurs. But because this also adjust the height of panel - the vertical scrollbar will never appears:

How to add vertical scrollbar?


In my case WrapPanel is vertical, means it will fill columns as much as it can and then wrap to a new column from left to right. Scrollbars are needed when children doesn't fit either vertically (when available space is less than children height) or horizontally.

The idea thought can be used for a standard (horizontal) WrapPanel : from left to right, creating new rows when full. Absolutely same problem will arise (just tried it).


You can do this by wrapping your wrappanel in a scrollviewer, but then binding the height and width of the inner panel to the Height and Width of the Viewport of the scrollviewer, so it stretches and contracts with the rest of the screen. I've also added minimum Height and Width to my sample, which ensures that the scrollbars appear once the wrap panel is pushed smaller than it's min dimensions

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

That sort of behavior is not possible with a WrapPanel without setting explicitly its Height / MinHeight for a Vertical orientation or Width / MinWidth for a Horizontal orientation. The ScrollViewer will only show the scrollbars when the FrameworkElement this scroll viewer wraps doesn't fit into the viewport.

You can create your own wrap panel that calculates its minimum size based on its children.

Alternatively, you can implement a Behavior<WrapPanel> or an attached property. This won't be as easy as just adding a couple of XAML tags, as you might expect.

We have solved this issue with an attached property. Let me give you an idea of what we did.

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

It is maybe not the easiest way, and there are a little bit more lines that just a few XAML tags, but it works flawlessly.

You have to be careful with the error handling though. I've just omitted all the checks and exception handling in the sample code.

The usage is simple:

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

It seems monitoring for children is one of important task to achieve wanted. So why not creating custom panel:

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

The usage (instead of WrapPanel ) is following:

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

The idea is to calculate layout manually, while MeasureOverride and ArrangeOverride will be automatically called whenever children are changed: added, deleted, resized, etc.

Measure logic is simple: start from (0,0) and measure next child size, if it fits into current column - add it, otherwise start and new column by offsetting location. During whole measurement cycle adjust the resulting size.

The only missing part of puzzle is to provide into measure/arrange cycles ViewportHeight from parent ScrollViewer . This is the role of ColumnPanel.ViewportHeight .

Here is the demo (button add purple circle):

链接地址: http://www.djcxy.com/p/63662.html

上一篇: 在socket.io中使用RedisStore的示例

下一篇: WrapPanel滚动条