Свернуть сетку в WPF

Я создал пользовательский элемент WPF, расширенный с RowDefinition которые должны свернуть строки в сетке, когда Collapsed свойство элемента установлено в True,

Это делается с помощью конвертера и источника данных в стиле, чтобы установить высоту строки равной 0. Он основан на этом SO-ответе.

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

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

Мой вопрос заключается в том, что можно сделать по-другому, чтобы не имело значения размер строк или MinHeight / MaxHeight установлены, это просто в состоянии свернуть строки?


MCVE

MainWindow.xaml.cs

using System;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace RowCollapsibleMCVE
{
    public partial class MainWindow : INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private bool isCollapsed;

        public bool IsCollapsed
        {
            get => isCollapsed;
            set
            {
                isCollapsed = value;
                OnPropertyChanged();
            }
        }
    }

    public class CollapsibleRow : RowDefinition
    {
        #region Default Values
        private const bool COLLAPSED_DEFAULT = false;
        private const bool INVERT_COLLAPSED_DEFAULT = false;
        #endregion

        #region Dependency Properties
        public static readonly DependencyProperty CollapsedProperty =
            DependencyProperty.Register("Collapsed", typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(COLLAPSED_DEFAULT));

        public static readonly DependencyProperty InvertCollapsedProperty =
            DependencyProperty.Register("InvertCollapsed", typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(INVERT_COLLAPSED_DEFAULT));
        #endregion

        #region Properties
        public bool Collapsed {
            get => (bool)GetValue(CollapsedProperty);
            set => SetValue(CollapsedProperty, value);
        }

        public bool InvertCollapsed {
            get => (bool)GetValue(InvertCollapsedProperty);
            set => SetValue(InvertCollapsedProperty, value);
        }
        #endregion
    }

    public class BoolVisibilityConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            if (values.Length > 0 && values[0] is bool collapsed)
            {
                if (values.Length > 1 && values[1] is bool invert && invert)
                {
                    collapsed = !collapsed;
                }

                return collapsed ? Visibility.Collapsed : Visibility.Visible;
            }

            return Visibility.Collapsed;
        }

        public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }
}

MainWindow.xaml

<Window x:Class="RowCollapsibleMCVE.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:RowCollapsibleMCVE"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <Visibility x:Key="CollapsedVisibilityVal">Collapsed</Visibility>
        <local:BoolVisibilityConverter x:Key="BoolVisibilityConverter"/>

        <Style TargetType="{x:Type local:CollapsibleRow}">
            <Style.Triggers>
                <DataTrigger Value="{StaticResource CollapsedVisibilityVal}">
                    <DataTrigger.Binding>
                        <MultiBinding Converter="{StaticResource BoolVisibilityConverter}">
                            <Binding Path="Collapsed"
                                     RelativeSource="{RelativeSource Self}"/>
                            <Binding Path="InvertCollapsed"
                                     RelativeSource="{RelativeSource Self}"/>
                        </MultiBinding>
                    </DataTrigger.Binding>
                    <DataTrigger.Setters>
                        <Setter Property="MinHeight" Value="0"/>
                        <Setter Property="Height" Value="0"/>
                        <Setter Property="MaxHeight" Value="0"/>
                    </DataTrigger.Setters>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <CheckBox Content="Collapse Row"
                  IsChecked="{Binding IsCollapsed}"/>
        <Grid Row="1">
            <Grid.RowDefinitions>
                <local:CollapsibleRow Height="3*" />
                <local:CollapsibleRow Height="Auto" />
                <local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="*" /> <!-- Using [MaxHeight="300"] breaks this completely -->
            </Grid.RowDefinitions>
            <StackPanel Background="Red"/>

            <GridSplitter Grid.Row="1"
                          Height="10"
                          HorizontalAlignment="Stretch">
                <GridSplitter.Visibility>
                    <MultiBinding Converter="{StaticResource BoolVisibilityConverter}" >
                        <Binding Path="IsCollapsed"/>
                    </MultiBinding>
                </GridSplitter.Visibility>
            </GridSplitter>

            <StackPanel Background="Blue"
                        Grid.Row="2">
                <StackPanel.Visibility>
                    <MultiBinding Converter="{StaticResource BoolVisibilityConverter}" >
                        <Binding Path="IsCollapsed"/>
                    </MultiBinding>
                </StackPanel.Visibility>
            </StackPanel>
        </Grid>
    </Grid>
</Window>

2 ответа

Решение

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

CollapsibleRow

public class CollapsibleRow : RowDefinition
{
    #region Fields
    private GridLength cachedHeight;
    private double cachedMinHeight;
    #endregion

    #region Dependency Properties
    public static readonly DependencyProperty CollapsedProperty =
        DependencyProperty.Register("Collapsed", typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(false, OnCollapsedChanged));

    private static void OnCollapsedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if(d is CollapsibleRow row && e.NewValue is bool collapsed)
        {
            if(collapsed)
            {
                if(row.MinHeight != 0)
                {
                    row.cachedMinHeight = row.MinHeight;
                    row.MinHeight = 0;
                }
                row.cachedHeight = row.Height;
            }
            else if(row.cachedMinHeight != 0)
            {
                row.MinHeight = row.cachedMinHeight;
            }
            row.Height = collapsed ? new GridLength(0) : row.cachedHeight;
        }
    }
    #endregion

    #region Properties
    public bool Collapsed
    {
        get => (bool)GetValue(CollapsedProperty);
        set => SetValue(CollapsedProperty, value);
    }
    #endregion
}

XAML

<Window x:Class="RowCollapsibleMCVE.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:RowCollapsibleMCVE"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <CheckBox Content="Collapse Row"
                  IsChecked="{Binding IsCollapsed}"/>
        <Grid Row="1">
            <Grid.RowDefinitions>
                <local:CollapsibleRow Height="3*" MinHeight="0.0001"/>
                <local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="Auto" />
                <local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="*" /> <!-- Using [MinHeight="50" MaxHeight="100"] behaves as expected -->
            </Grid.RowDefinitions>
            <StackPanel Background="Red"/>
            <GridSplitter Grid.Row="1" Height="10" HorizontalAlignment="Stretch" />
            <StackPanel Background="Blue" Grid.Row="2" />
        </Grid>
    </Grid>
</Window>

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


ОБНОВИТЬ

Как упомянул @Ivan в своем посте, элементы управления, содержащиеся в свернутых строках, будут по-прежнему доступны для фокусировки, что позволит пользователям получать к ним доступ, когда они не должны этого делать. По общему признанию, это может быть трудно установить видимость для всех элементов управления вручную, особенно для больших XAML. Итак, давайте добавим некоторое пользовательское поведение для синхронизации свернутых строк с их элементами управления.

  1. Эта проблема

Сначала запустите пример, используя приведенный выше код, затем сверните нижние строки, установив флажок. Теперь нажмите клавишу TAB один раз и используйте клавишу со стрелкой вверх, чтобы переместить GridSplitter, Как видите, даже если сплиттер не виден, пользователь все равно может получить к нему доступ.

  1. Исправление

Добавить новый файл Extensions.cs принять поведение.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using RowCollapsibleMCVE;

namespace Extensions
{
    [ValueConversion(typeof(bool), typeof(bool))]
    public class BooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return !(bool)value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return Binding.DoNothing;
        }
    }

    public class GridHelper : DependencyObject
    {
        #region Attached Property

        public static readonly DependencyProperty SyncCollapsibleRowsProperty =
            DependencyProperty.RegisterAttached(
                "SyncCollapsibleRows",
                typeof(Boolean),
                typeof(GridHelper),
                new FrameworkPropertyMetadata(false,
                    FrameworkPropertyMetadataOptions.AffectsRender,
                    new PropertyChangedCallback(OnSyncWithCollapsibleRows)
                ));

        public static void SetSyncCollapsibleRows(UIElement element, Boolean value)
        {
            element.SetValue(SyncCollapsibleRowsProperty, value);
        }

        private static void OnSyncWithCollapsibleRows(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is Grid grid)
            {
                grid.Loaded += (o,ev) => SetBindingForControlsInCollapsibleRows((Grid)o);
            }
        }

        #endregion

        #region Logic

        private static IEnumerable<UIElement> GetChildrenFromPanels(IEnumerable<UIElement> elements)
        {
            Queue<UIElement> queue = new Queue<UIElement>(elements);
            while (queue.Any())
            {
                var uiElement = queue.Dequeue();
                if (uiElement is Panel panel)
                {
                    foreach (UIElement child in panel.Children) queue.Enqueue(child);
                }
                else
                {
                    yield return uiElement;
                }
            }
        }

        private static IEnumerable<UIElement> ElementsInRow(Grid grid, int iRow)
        {
            var rowRootElements = grid.Children.OfType<UIElement>().Where(c => Grid.GetRow(c) == iRow);

            if (rowRootElements.Any(e => e is Panel))
            {
                return GetChildrenFromPanels(rowRootElements);
            }
            else
            {
                return rowRootElements;
            }
        }

        private static BooleanConverter MyBooleanConverter = new BooleanConverter();

        private static void SyncUIElementWithRow(UIElement uiElement, CollapsibleRow row)
        {
            BindingOperations.SetBinding(uiElement, UIElement.FocusableProperty, new Binding
            {
                Path = new PropertyPath(CollapsibleRow.CollapsedProperty),
                Source = row,
                Converter = MyBooleanConverter
            });
        }

        private static void SetBindingForControlsInCollapsibleRows(Grid grid)
        {
            for (int i = 0; i < grid.RowDefinitions.Count; i++)
            {
                if (grid.RowDefinitions[i] is CollapsibleRow row)
                {
                    ElementsInRow(grid, i).ToList().ForEach(uiElement => SyncUIElementWithRow(uiElement, row));
                }
            }
        }

        #endregion
    }
}
  1. Больше тестирования

Измените XAML, чтобы добавить поведение и некоторые текстовые поля (которые также можно фокусировать).

<Window x:Class="RowCollapsibleMCVE.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:RowCollapsibleMCVE"
        xmlns:ext="clr-namespace:Extensions"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <CheckBox Content="Collapse Row" IsChecked="{Binding IsCollapsed}"/>
        <!-- Set the desired behavior through an Attached Property -->
        <Grid ext:GridHelper.SyncCollapsibleRows="True" Row="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="3*" MinHeight="0.0001" />
                <local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="Auto" />
                <local:CollapsibleRow Collapsed="{Binding IsCollapsed}" Height="*" />
            </Grid.RowDefinitions>
            <StackPanel Background="Red">
                <TextBox Width="100" Margin="40" />
            </StackPanel>
            <GridSplitter Grid.Row="1" Height="10" HorizontalAlignment="Stretch" />
            <StackPanel Grid.Row="2" Background="Blue">
                <TextBox Width="100" Margin="40" />
            </StackPanel>
        </Grid>
    </Grid>
</Window>

В конце:

  • Логика полностью скрыта от XAML (чисто).
  • Мы по-прежнему предоставляем гибкость:

    • Для каждого CollapsibleRow вы могли бы связать Collapsed в другую переменную.

    • Ряды, которые не нуждаются в поведении, могут использовать базу RowDefinition (применяется по запросу).


ОБНОВЛЕНИЕ 2

Как отметил @Ash в комментариях, вы можете использовать встроенное кэширование WPF для хранения значений высоты. В результате получается очень чистый код с автономными свойствами, каждый из которых обрабатывает свой собственный => надежный код. Например, используя приведенный ниже код, вы не сможете переместить GridSplitter когда строки свернуты, даже без применения поведения.

Конечно, элементы управления будут по-прежнему доступны, что позволит пользователю запускать события. Таким образом, нам все еще нужно поведение, но CoerceValueCallback обеспечивает последовательную связь между Collapsed и различные свойства зависимости от высоты нашего CollapsibleRow,

public class CollapsibleRow : RowDefinition
{
    public static readonly DependencyProperty CollapsedProperty;

    public bool Collapsed
    {
        get => (bool)GetValue(CollapsedProperty);
        set => SetValue(CollapsedProperty, value);
    }

    static CollapsibleRow()
    {
        CollapsedProperty = DependencyProperty.Register("Collapsed",
            typeof(bool), typeof(CollapsibleRow), new PropertyMetadata(false, OnCollapsedChanged));

        RowDefinition.HeightProperty.OverrideMetadata(typeof(CollapsibleRow),
            new FrameworkPropertyMetadata(new GridLength(1, GridUnitType.Star), null, CoerceHeight));

        RowDefinition.MinHeightProperty.OverrideMetadata(typeof(CollapsibleRow),
            new FrameworkPropertyMetadata(0.0, null, CoerceHeight));

        RowDefinition.MaxHeightProperty.OverrideMetadata(typeof(CollapsibleRow),
            new FrameworkPropertyMetadata(double.PositiveInfinity, null, CoerceHeight));
    }

    private static object CoerceHeight(DependencyObject d, object baseValue)
    {
        return (((CollapsibleRow)d).Collapsed) ? (baseValue is GridLength ? new GridLength(0) : 0.0 as object) : baseValue;
    }

    private static void OnCollapsedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.CoerceValue(RowDefinition.HeightProperty);
        d.CoerceValue(RowDefinition.MinHeightProperty);
        d.CoerceValue(RowDefinition.MaxHeightProperty);
    }
}

Пример выше технически неверен.

По сути, он пытается установить высоту строки равной 0, а это не то, что вы хотите или должны делать - проблема в том, что клавиша табуляции будет проходить через элементы управления, даже если высота равна 0, и Narrator будет читать эти элементы управления. По сути, эти элементы управления все еще существуют и являются полностью интерактивными, функциональными и доступными, только если они не представлены в окне, но к ним можно по-прежнему обращаться различными способами и они могут повлиять на работу приложения.

Во-вторых (и то, что вызывает проблемы, которые вы описываете, поскольку вы не описали проблемы выше, хотя они также важны и не должны игнорироваться), у вас есть GridSplitter и, как сказано, он остается работоспособным, даже если вы принудительно установите его высоту в 0 (как описано выше). GridSplitter означает, что в конце дня вы управляете не макетом, а пользователем.

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

РЕДАКТИРОВАТЬ: дальнейшие разъяснения - в приведенном выше коде вы устанавливаете новые свойства под названием Collapsed а также InvertCollapsed, Просто потому, что они названы так, что они не оказывают никакого влияния на сворачиваемую строку, они могут также называться Property1 и Property2. Они используются в DataTrigger довольно странным образом - когда их значение изменяется, это значение преобразуется в Visibility а затем, если это преобразованное значение Collapsed вызывают сеттеры, которые заставляют высоту строки быть 0. Итак, кто-то разыграл много декораций, чтобы было похоже, что он что-то рушится, но он этого не делает, он только меняет высоту, что совсем другое дело. И вот откуда возникают проблемы. Я, конечно, советую избегать всего этого подхода, но если вы обнаружите, что он подходит для вашего приложения, минимальное, что вам нужно сделать, - это избегать этого подхода для второй строки, где GridSplitter настроен так, как если бы вы этого не делали, ваш запрос становится невозможным.,

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