在 WPF 中折叠网格行
Collapse Grid Row in WPF
我创建了一个从 RowDefinition
扩展的自定义 WPF 元素,当元素的 Collapsed
属性 设置为 True
时,它应该折叠网格中的行。
它是通过在样式中使用转换器和数据触发器来将行高设置为0来实现的。它基于此SO Answer。
在下面的示例中,当网格分离器位于 window 的一半以上时,此方法非常有效。但是,当它不到一半时,行仍然折叠,但第一行不会展开。相反,原来行所在的位置只有一个白色间隙。这可以在下图中看到。
同样,如果在折叠的任何行上设置了 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>
上面的示例在技术上是错误的。
它本质上所做的是试图强制行高为 0,这不是您想要或应该做的 - 问题是 Tab 键将通过控件,即使高度为 0,讲述人将读取这些控件。本质上,这些控件仍然存在,并且完全可单击、正常运行和可访问,只是它们没有出现在 window 上,但它们仍然可以通过各种方式访问,并且可能会影响应用程序的工作。
其次(以及导致您描述的问题的事情,因为您没有描述上面的问题,尽管它们也是必不可少的并且不应被忽略),您有 GridSplitter
并且如前所述,它仍然可以正常工作即使你强制它的高度为 0(如上所述)。 GridSplitter
意味着在一天结束时,您不受布局的控制,而是用户。
应该做的是,您应该使用普通 RowDefinition
并将其高度设置为 Auto
,然后将行内容的 Visibility
设置为 Collapsed
- 当然你可以使用数据绑定和转换器。
编辑:进一步说明 - 在上面的代码中,您设置了名为 Collapsed
和 InvertCollapsed
的新属性。正因为这样命名,它们对折叠的行没有任何影响,所以它们也可以称为 Property1 和 Property2。它们以一种相当奇怪的方式在 DataTrigger
中使用 - 当它们的值被更改时,该值被转换为 Visibility
然后如果该转换值是 Collapsed
强制行高的设置器被称为 0。所以有人玩了很多布景,让他看起来像是在倒塌什么东西,但他并没有,他只是改变了高度,这是完全不同的事情。这就是问题的根源。我当然建议避免使用这种方法,但是如果您发现它对您的应用程序有好处,那么您需要做的最少的事情就是避免在设置 GridSplitter 的第二行使用这种方法,就好像您不这样做一样,您的请求变得不可能.
您只需要缓存可见行的高度即可。之后,您不再需要转换器或切换包含控件的可见性。
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
。这是为了确保当您将拆分器一直向上并切换可见性时,星形大小的行具有大小。只有这样它才能接管剩下的space.
更新
正如@Ivan 在他的 post 中提到的那样,折叠行中包含的控件仍可聚焦,允许用户在不应该访问它们时访问它们。
诚然,手动设置所有控件的可见性可能很痛苦,尤其是对于大型 XAMLs。因此,让我们添加一些自定义行为以将折叠的行与其控件同步。
- 问题
首先,运行 使用上面代码的示例,然后通过选中复选框折叠底行。现在,按一次 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's native caching 来存储高度值。产生具有自治属性的非常干净的代码,每个处理自己的 => 健壮的代码。例如,使用下面的代码,即使没有应用行为,您也无法在行折叠时移动 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);
}
}
我创建了一个从 RowDefinition
扩展的自定义 WPF 元素,当元素的 Collapsed
属性 设置为 True
时,它应该折叠网格中的行。
它是通过在样式中使用转换器和数据触发器来将行高设置为0来实现的。它基于此SO Answer。
在下面的示例中,当网格分离器位于 window 的一半以上时,此方法非常有效。但是,当它不到一半时,行仍然折叠,但第一行不会展开。相反,原来行所在的位置只有一个白色间隙。这可以在下图中看到。
同样,如果在折叠的任何行上设置了 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>
上面的示例在技术上是错误的。
它本质上所做的是试图强制行高为 0,这不是您想要或应该做的 - 问题是 Tab 键将通过控件,即使高度为 0,讲述人将读取这些控件。本质上,这些控件仍然存在,并且完全可单击、正常运行和可访问,只是它们没有出现在 window 上,但它们仍然可以通过各种方式访问,并且可能会影响应用程序的工作。
其次(以及导致您描述的问题的事情,因为您没有描述上面的问题,尽管它们也是必不可少的并且不应被忽略),您有 GridSplitter
并且如前所述,它仍然可以正常工作即使你强制它的高度为 0(如上所述)。 GridSplitter
意味着在一天结束时,您不受布局的控制,而是用户。
应该做的是,您应该使用普通 RowDefinition
并将其高度设置为 Auto
,然后将行内容的 Visibility
设置为 Collapsed
- 当然你可以使用数据绑定和转换器。
编辑:进一步说明 - 在上面的代码中,您设置了名为 Collapsed
和 InvertCollapsed
的新属性。正因为这样命名,它们对折叠的行没有任何影响,所以它们也可以称为 Property1 和 Property2。它们以一种相当奇怪的方式在 DataTrigger
中使用 - 当它们的值被更改时,该值被转换为 Visibility
然后如果该转换值是 Collapsed
强制行高的设置器被称为 0。所以有人玩了很多布景,让他看起来像是在倒塌什么东西,但他并没有,他只是改变了高度,这是完全不同的事情。这就是问题的根源。我当然建议避免使用这种方法,但是如果您发现它对您的应用程序有好处,那么您需要做的最少的事情就是避免在设置 GridSplitter 的第二行使用这种方法,就好像您不这样做一样,您的请求变得不可能.
您只需要缓存可见行的高度即可。之后,您不再需要转换器或切换包含控件的可见性。
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
。这是为了确保当您将拆分器一直向上并切换可见性时,星形大小的行具有大小。只有这样它才能接管剩下的space.
更新
正如@Ivan 在他的 post 中提到的那样,折叠行中包含的控件仍可聚焦,允许用户在不应该访问它们时访问它们。 诚然,手动设置所有控件的可见性可能很痛苦,尤其是对于大型 XAMLs。因此,让我们添加一些自定义行为以将折叠的行与其控件同步。
- 问题
首先,运行 使用上面代码的示例,然后通过选中复选框折叠底行。现在,按一次 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's native caching 来存储高度值。产生具有自治属性的非常干净的代码,每个处理自己的 => 健壮的代码。例如,使用下面的代码,即使没有应用行为,您也无法在行折叠时移动 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);
}
}