Полосы прокрутки 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 ответа

Такое поведение невозможно с WrapPanel не устанавливая явно Height/MinHeight для Vertical ориентация или Width/MinWidth для Horizontal ориентации. ScrollViewer будут отображаться только полосы прокрутки, когда FrameworkElement эта оболочка просмотра прокрутки не помещается в область просмотра.

Вы можете создать собственную панель переноса, которая рассчитывает ее минимальный размер на основе дочерних элементов.

Кроме того, вы можете реализовать 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

Это можно сделать, обернув свою обертку в окне просмотра прокрутки, но затем привязав высоту и ширину внутренней панели к высоте и ширине области просмотра окна просмотра прокрутки, чтобы она растягивалась и сжималась с остальной частью экрана. Я также добавил минимальную высоту и ширину к своему образцу, что гарантирует, что полосы прокрутки появятся, когда панель переноса будет сдвинута меньше, чем ее минимальные размеры.

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

Кажется, наблюдение за детьми является одной из важных задач для достижения желаемого. Так почему бы не создать собственную панель:

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>

Идея состоит в том, чтобы рассчитать макет вручную, а MeasureOverride а также ArrangeOverride будет вызываться автоматически при изменении потомков: добавлено, удалено, изменено размер и т. д.

Измерить логику просто: начни с (0,0) и измерить следующий дочерний размер, если он вписывается в текущий столбец - добавьте его, в противном случае начните и новый столбец путем смещения местоположения. В течение всего цикла измерения корректируйте полученный размер.

Единственная недостающая часть головоломки состоит в том, чтобы обеспечить в меру / организовать циклы ViewportHeight от родителя ScrollViewer, Это роль ColumnPanel.ViewportHeight,

Вот демоверсия (кнопка добавления фиолетового круга):

Другие вопросы по тегам