Contraer fila de cuadrícula en WPF

 C Programming >> Programación C >  >> Tags >> WPF
Contraer fila de cuadrícula en WPF

Todo lo que necesita es algo para almacenar en caché la(s) altura(s) de la fila visible. Después de eso, ya no necesitará convertidores ni alternar la visibilidad de los controles contenidos.

Fila plegable

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>

Deberías tener un MaxHeight en la fila colapsable (la tercera en nuestro ejemplo) o un MinHeight en la fila no plegable (la primera) adyacente al divisor. Esto para garantizar que la fila del tamaño de la estrella tenga un tamaño cuando coloca el divisor completamente hacia arriba y alterna la visibilidad. Solo entonces podrá ocupar el espacio restante.

ACTUALIZAR

Como mencionó @Ivan en su publicación, los controles que están contenidos en las filas colapsadas seguirán siendo enfocables, lo que permitirá a los usuarios acceder a ellos cuando no deberían. XAML grandes. Así que agreguemos un comportamiento personalizado para sincronizar las filas contraídas con sus controles.

  1. El problema

Primero, ejecute el ejemplo usando el código anterior, luego contraiga las filas inferiores marcando la casilla de verificación. Ahora, presione la tecla TAB una vez y use la tecla de FLECHA ARRIBA para mover el GridSplitter . Como puede ver, aunque el divisor no está visible, el usuario aún puede acceder a él.

  1. La solución

Agregar un nuevo archivo Extensions.cs para albergar el comportamiento.

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. Más pruebas

Cambie el XAML para agregar el comportamiento y algunos cuadros de texto (que también se pueden enfocar).

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

Al final:

  • La lógica está completamente oculta de XAML (limpio).
  • Seguimos ofreciendo flexibilidad:

    • Por cada CollapsibleRow podría vincular Collapsed a una variable diferente.

    • Las filas que no necesitan el comportamiento pueden usar la base RowDefinition (aplicar bajo demanda).

ACTUALIZACIÓN 2

Como @Ash señaló en los comentarios, puede usar el almacenamiento en caché nativo de WPF para almacenar los valores de altura. Dando como resultado un código muy limpio con propiedades autónomas, cada uno manejando su propio => código robusto. Por ejemplo, con el código siguiente no podrá mover el GridSplitter cuando las filas están contraídas, incluso sin que se aplique el comportamiento.

Por supuesto, los controles seguirían siendo accesibles, lo que permitiría al usuario activar eventos. Así que todavía necesitaríamos el comportamiento, pero el CoerceValueCallback proporciona un vínculo consistente entre el Collapsed y las diversas propiedades de dependencia de altura de nuestro 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);
    }
}

El ejemplo anterior es técnicamente incorrecto.

Lo que hace esencialmente es que intenta forzar que la altura de la fila sea 0, que no es lo que quiere o debe hacer; el problema es que la tecla de tabulación pasará por los controles incluso si la altura es 0, y Narrador leerá esos controles. Esencialmente, esos controles todavía existen y se puede hacer clic en ellos, son funcionales y accesibles, solo que no se presentan en la ventana, pero aún se puede acceder a ellos de varias maneras y pueden afectar el trabajo de la aplicación.

En segundo lugar (y lo que causa los problemas que describe, ya que no describió los problemas anteriores, aunque también son esenciales y no deben ignorarse), tiene GridSplitter y como se dijo, sigue siendo funcional incluso si fuerza su altura a 0 (como se explicó anteriormente). GridSplitter significa que al final del día no tienes el control del diseño, sino el usuario.

Lo que se debe hacer en su lugar es usar el RowDefinition simple y establezca su altura en Auto y luego establezca el Visibility del contenido de las filas a Collapsed - por supuesto, puede usar el enlace de datos y el convertidor.

EDITAR:aclaración adicional:en el código anterior, establece las nuevas propiedades llamadas Collapsed y InvertCollapsed . Solo porque se nombran así, no tienen ningún efecto en la fila que se contrae, también podrían llamarse Property1 y Property2. Se utilizan en el DataTrigger de una manera bastante extraña:cuando se cambia su valor, ese valor se convierte a Visibility y luego si ese valor convertido es Collapsed se llaman los setters que obligan a que la altura de la fila sea 0. Así que alguien jugó un montón de escenarios para que pareciera que está derrumbando algo, pero no lo hace, solo cambia la altura, lo cual es algo muy diferente de hacer. Y ahí es donde se originan los problemas. Ciertamente sugiero evitar todo este enfoque, pero si encuentra que es bueno para su aplicación, lo mínimo que debe hacer es evitar ese enfoque para la segunda fila donde GridSplitter está configurado como si no lo hiciera, su solicitud se vuelve imposible. .