我创建了一个自RowDefinition
扩展的自定义WPF元素,当该元素的Collapsed
属性设置为True
时,该元素应该折叠网格中的行。
通过使用一种样式的转换器和数据触发器将行的高度设置为0来实现。它基于此SO Answer。
在下面的示例中,当网格拆分器位于窗口的一半上方时,此方法非常有效。但是,当它不到一半时,行仍会折叠,但第一行不会扩展。取而代之的是,行过去只有一个空白。可以在下图中看到。
类似地,如果在任何折叠的行上设置了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 :(得分:5)
您需要做的就是缓存可见行的高度。之后,您将不再需要转换器或切换包含的控件的可见性。
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。因此,让我们添加一些自定义行为,以将折叠的行与其控件进行同步。
首先,使用上面的代码运行示例,然后通过选中复选框折叠最下面的行。现在,按一下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>
最后:
我们仍在提供灵活性:
对于每个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);
}
}
答案 1 :(得分:3)
以上示例在技术上是错误的。
其本质上是试图将行的高度强制为0,这不是您想要或不应该执行的操作-问题是即使高度为0,Tab键也将通过控件,并且讲述人将阅读这些控件。本质上,这些控件仍然存在,并且完全可以单击,功能和可访问,只是它们没有显示在窗口中,但是仍然可以通过各种方式访问它们,并且可能会影响应用程序的工作。
第二(导致问题的原因是您描述的问题,尽管它们虽然也是必不可少的,但也没有描述上述问题,因此不应忽略),您拥有GridSplitter
并且如上所述,即使您将其高度设置为0(如上所述)。 GridSplitter
意味着一天结束时您将不受用户的控制,而是用户。
相反,您应该使用普通的RowDefinition
并将其高度设置为Auto
,然后将行内容的Visibility
设置为{{1} }-当然,您可以使用数据绑定和转换器。
编辑:进一步说明-在上面的代码中,设置了名为Collapsed
和Collapsed
的新属性。只是因为它们的命名方式对折叠的行没有任何影响,所以它们也可以称为Property1和Property2。它们在InvertCollapsed
中以一种非常奇怪的方式使用-更改其值后,该值将转换为DataTrigger
,然后,如果转换后的值是Visibility
,则设置程序将强制行高达到被称为0。因此,某人演奏了许多风景以使其看起来像是正在塌陷,但他没有,他只是改变了高度,这是完全不同的事情。这就是问题的根源。我当然建议避免整个使用这种方法,但是如果您发现对您的应用程序有利,那么您要做的最小的事情就是避免在第二行设置GridSplitter的情况下使用该方法,就像您没有请求一样