Свернуть сетку в WPF - PullRequest
       39

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

0 голосов
/ 03 января 2019

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

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

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

Picture shows under half, the bottom row doesn't disappear, but over half it does

Аналогичным образом, если 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 ]

0 голосов
/ 06 января 2019

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

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.Как видите, даже если сплиттер не виден, пользователь может получить к нему доступ.

Исправление

Добавление нового файла 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
    }
}
Дополнительные тесты

Измените 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 голосов
/ 05 января 2019

Образец выше технически неверен.

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

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

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

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

...