Вложенные области прокрутки

Я создаю элемент управления для WPF, и у меня есть вопрос к вам, гуру WPF.

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

В моем управлении у меня есть список, который я хочу расширить с помощью окна. У меня также есть другие элементы управления вокруг списка (кнопки, текст и т. д.).

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

Это создает вложенные области прокрутки: одну для списка и ScrollViewer, обертывающий весь элемент управления.

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

Я хочу, чтобы элемент управления прокручивался только в том случае, если содержимое не может стать меньше, иначе я не хочу прокручивать элемент управления; вместо этого я хочу прокрутить список внутри элемента управления.

Как я могу изменить поведение класса ScrollViewer по умолчанию? Я попытался наследоваться от класса ScrollViewer и переопределить классы MeasureOverride и ArrangeOverride, но не смог понять, как правильно измерить и упорядочить дочерний элемент. Похоже, что расположение должно каким-то образом влиять на ScrollContentPresenter, а не на фактический дочерний элемент содержимого.

Любая помощь/предложения будут высоко оценены.


person Community    schedule 13.10.2008    source источник
comment
Отличный вопрос. Мы сами столкнулись с очень похожей проблемой.   -  person cplotts    schedule 29.03.2010


Ответы (7)


Я создал класс для решения этой проблемы:

public class RestrictDesiredSize : Decorator
{
    Size lastArrangeSize = new Size(double.PositiveInfinity, double.PositiveInfinity);

    protected override Size MeasureOverride(Size constraint)
    {
        Debug.WriteLine("Measure: " + constraint);
        base.MeasureOverride(new Size(Math.Min(lastArrangeSize.Width, constraint.Width),
                                      Math.Min(lastArrangeSize.Height, constraint.Height)));
        return new Size(0, 0);
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        Debug.WriteLine("Arrange: " + arrangeSize);
        if (lastArrangeSize != arrangeSize) {
            lastArrangeSize = arrangeSize;
            base.MeasureOverride(arrangeSize);
        }
        return base.ArrangeOverride(arrangeSize);
    }
}

Он всегда будет возвращать желаемый размер (0,0), даже если содержащий элемент хочет быть больше. Использование:

<local:RestrictDesiredSize MinWidth="200" MinHeight="200">
     <ListBox />
</local>
person Daniel    schedule 15.10.2009
comment
Чистое, простое, элегантное решение - person Scott B; 09.03.2012

У вас возникает проблема, потому что Control в ScrollViewer имеют практически неограниченное доступное пространство. Поэтому ваш внутренний ListBox думает, что может избежать прокрутки, занимая всю высоту, необходимую для отображения всех его элементов. Конечно, в вашем случае это поведение имеет нежелательный побочный эффект слишком большого использования внешнего ScrollViewer.

Таким образом, цель состоит в том, чтобы заставить ListBox использовать видимую высоту внутри ScrollViewer, если ее достаточно, и определенную минимальную высоту в противном случае. Чтобы добиться этого, наиболее прямым способом является наследование от ScrollViewer и переопределение MeasureOverride() для передачи availableSize подходящего размера (то есть заданного availableSize, увеличенного до минимального размера вместо "обычной" бесконечности) в Visuals, найденные с помощью VisualChildrenCount и GetVisualChild(int).

person David Schmitt    schedule 20.10.2008
comment
+1 за объяснение относительно неограниченного доступного пространства. - person Ridcully; 16.09.2010

Я использовал решение Даниэля. Это прекрасно работает. Спасибо.

Затем я добавил в класс декоратора два логических свойства зависимостей: KeepWidth и KeepHeight. Таким образом, новая функция может быть подавлена ​​для одного измерения.

Это требует изменения в MeasureOverride:

protected override Size MeasureOverride(Size constraint)
{
    var innerWidth = Math.Min(this._lastArrangeSize.Width, constraint.Width);
    var innerHeight = Math.Min(this._lastArrangeSize.Height, constraint.Height);
    base.MeasureOverride(new Size(innerWidth, innerHeight));

    var outerWidth = KeepWidth ? Child.DesiredSize.Width : 0;
    var outerHeight = KeepHeight ? Child.DesiredSize.Height : 0;
    return new Size(outerWidth, outerHeight);
}
person Heiner    schedule 03.04.2012

Хотя я бы не рекомендовал создавать пользовательский интерфейс, требующий внешних полос прокрутки, вы можете сделать это довольно легко:

<Window
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  >    
    <ScrollViewer HorizontalScrollBarVisibility="Auto" 
                  VerticalScrollBarVisibility="Auto">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <ListBox Grid.Row="0" Grid.RowSpan="3" Grid.Column="0" MinWidth="200"/>
            <Button Grid.Row="0" Grid.Column="1" Content="Button1"/>
            <Button Grid.Row="1" Grid.Column="1" Content="Button2"/>
            <Button Grid.Row="2" Grid.Column="1" Content="Button3"/>
        </Grid>
    </ScrollViewer>
</Window>

Я действительно не рекомендую это. WPF предоставляет исключительные системы компоновки, такие как Grid, и вы должны попытаться разрешить приложению изменять свой размер по мере необходимости. Возможно, вы можете установить MinWidth/MinHeight для самого окна, чтобы предотвратить изменение размера?

person Bob King    schedule 13.10.2008
comment
Сложность в том, что я хочу иметь возможность сжимать окно ниже минимального размера, не обрезая элементы управления. - person ; 13.10.2008
comment
Вы должны позволить окну уменьшиться ниже этого минимального размера? Чего вы пытаетесь достичь, позволяя это? - person Bob King; 13.10.2008
comment
Нет, не обязательно, но вся проблема исчезнет, ​​если я этого не сделаю. Я хочу уменьшить размер ниже минимального, чтобы обеспечить гибкие макеты. На самом деле я создаю приложение, содержащее оконные элементы управления, размер и положение которых можно изменять. - person ; 13.10.2008
comment
Тогда приведенный выше код должен работать для вас. Просто убедитесь, что ваши фрагменты находятся в столбцах правильного размера * и Auto, и убедитесь, что внешний ScrollViewer использует Auto для Horizontal/VerticalScrollbarVisibility. - person Bob King; 13.10.2008
comment
И вы можете захотеть оставить их как всегда видимыми, чтобы предотвратить скачок, когда один из них должен быть видимым внезапно. - person Bob King; 13.10.2008
comment
Ваше решение не работает. Добавьте несколько элементов в ListBox, и вы увидите, что список никогда не становится прокручиваемым; это всегда родительский ScrollViewer, который будет прокручиваться. Я думаю, что Джош Г (как и я) искал решение, при котором внешний ScrollViewer будет использоваться только в случае необходимости для MinWidth/MinHeight. - person Daniel; 15.10.2009

Создайте метод в коде программной части, который устанавливает MaxHeight ListBox в высоту любого элемента управления, содержащего его и другие элементы управления. Если в списке есть какие-либо элементы управления/поля/отступы над или под ним, вычтите их высоту из высоты контейнера, назначенной MaxHeight. Вызовите этот метод в основных окнах обработчиков событий «загрузка» и «изменение размера окна».

Это должно дать вам лучшее из обоих миров. Вы даете ListBox «фиксированный» размер, который заставит его прокручиваться, несмотря на то, что в главном окне есть собственная полоса прокрутки.

person richarfen    schedule 19.05.2014

для 2 ScrollViewer

   public class ScrollExt: ScrollViewer
{
    Size lastArrangeSize = new Size(double.PositiveInfinity, double.PositiveInfinity);

    public ScrollExt()
    {

    }
    protected override Size MeasureOverride(Size constraint)
    {
        base.MeasureOverride(new Size(Math.Min(lastArrangeSize.Width, constraint.Width),
                                      Math.Min(lastArrangeSize.Height, constraint.Height)));
        return new Size(0, 0);
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        if (lastArrangeSize != arrangeSize)
        {
            lastArrangeSize = arrangeSize;
            base.MeasureOverride(arrangeSize);
        }
        return base.ArrangeOverride(arrangeSize);
    }
}

код:

  <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
        <Grid >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <TextBlock Background="Beige" Width="600" Text="Example"/>
            <Grid Grid.Column="1" x:Name="grid">
                    <Grid Grid.Column="1" Margin="25" Background="Green">
                    <local:ScrollExt HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
                        <Grid Width="10000" Margin="25" Background="Red" />
                    </local:ScrollExt>
                    </Grid>
                </Grid>
        </Grid>
    </ScrollViewer>
person Dmitry    schedule 16.06.2017

В итоге я объединил ответ Дэниелса и ответ Хайнера. Я решил опубликовать все решение, чтобы людям было легче принять его, если это необходимо. Вот мой класс декоратора:

public class RestrictDesiredSizeDecorator : Decorator
{
    public static readonly DependencyProperty KeepWidth;
    public static readonly DependencyProperty KeepHeight;

    #region Dependency property setters and getters
    public static void SetKeepWidth(UIElement element, bool value)
    {
        element.SetValue(KeepWidth, value);
    }

    public static bool GetKeepWidth(UIElement element)
    {
        return (bool)element.GetValue(KeepWidth);
    }

    public static void SetKeepHeight(UIElement element, bool value)
    {
        element.SetValue(KeepHeight, value);
    }

    public static bool GetKeepHeight(UIElement element)
    {
        return (bool)element.GetValue(KeepHeight);
    }
    #endregion

    private Size _lastArrangeSize = new Size(double.PositiveInfinity, double.PositiveInfinity);

    static RestrictDesiredSizeDecorator()
    {
        KeepWidth = DependencyProperty.RegisterAttached(
            nameof(KeepWidth),
            typeof(bool),
            typeof(RestrictDesiredSizeDecorator));

        KeepHeight = DependencyProperty.RegisterAttached(
            nameof(KeepHeight),
            typeof(bool),
            typeof(RestrictDesiredSizeDecorator));
    }

    protected override Size MeasureOverride(Size constraint)
    {
        Debug.WriteLine("Measure: " + constraint);

        var keepWidth = GetValue(KeepWidth) as bool? ?? false;
        var keepHeight = GetValue(KeepHeight) as bool? ?? false;

        var innerWidth = keepWidth ? constraint.Width : Math.Min(this._lastArrangeSize.Width, constraint.Width);
        var innerHeight = keepHeight ? constraint.Height : Math.Min(this._lastArrangeSize.Height, constraint.Height);
        base.MeasureOverride(new Size(innerWidth, innerHeight));

        var outerWidth = keepWidth ? Child.DesiredSize.Width : 0;
        var outerHeight = keepHeight ? Child.DesiredSize.Height : 0;

        return new Size(outerWidth, outerHeight);
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        Debug.WriteLine("Arrange: " + arrangeSize);

        if (_lastArrangeSize != arrangeSize)
        {
            _lastArrangeSize = arrangeSize;
            base.MeasureOverride(arrangeSize);
        }

        return base.ArrangeOverride(arrangeSize);
    }
}

и вот как я использую это в xaml:

<ScrollViewer>
    <StackPanel Orientation="Vertical">
        <Whatever />

        <decorators:RestrictDesiredSizeDecorator MinWidth="100" KeepHeight="True">
            <TextBox
                Text="{Binding Comment, UpdateSourceTrigger=PropertyChanged}"
                Height="Auto"
                MaxHeight="360"
                VerticalScrollBarVisibility="Auto"
                HorizontalScrollBarVisibility="Auto"
                AcceptsReturn="True"
                AcceptsTab="True"
                TextWrapping="WrapWithOverflow"
                />
        </decorators:RestrictDesiredSizeDecorator>

        <Whatever />
    </StackPanel>
</ScrollViewer

Вышеупомянутое создает текстовое поле, которое будет расти по вертикали (пока не достигнет MaxHeight), но будет соответствовать ширине родителя без увеличения внешнего ScrollViewer. Изменение размера окна/ScrollViewer до ширины менее 100 заставит внешний ScrollViewer отображать горизонтальные полосы прокрутки. Также можно использовать другие элементы управления с внутренними элементами ScrollViewer, в том числе сложные сетки.

person Glaucus    schedule 26.10.2018