C# WPF中的折叠网格行
我已经创建了一个从C# WPF中的折叠网格行,c#,wpf,xaml,datatrigger,wpf-grid,C#,Wpf,Xaml,Datatrigger,Wpf Grid,我已经创建了一个从RowDefinition扩展而来的自定义WPF元素,当元素的Collapsed属性设置为True时,该元素应该在网格中折叠行 它通过在样式中使用转换器和datatrigger将行的高度设置为0来实现。基于此 在下面的示例中,当栅格拆分器位于窗口上方的一半以上时,此功能非常有效。但是,当距离不足一半时,行仍会折叠,但第一行不会展开。取而代之的是,在过去的行之间只有一个白色的间隙。这可以在下图中看到 类似地,如果在任何折叠的行上设置了MinHeight或MaxHeight,则它
RowDefinition
扩展而来的自定义WPF元素,当元素的Collapsed
属性设置为True
时,该元素应该在网格中折叠行
它通过在样式中使用转换器和datatrigger将行的高度设置为0来实现。基于此
在下面的示例中,当栅格拆分器位于窗口上方的一半以上时,此功能非常有效。但是,当距离不足一半时,行仍会折叠,但第一行不会展开。取而代之的是,在过去的行之间只有一个白色的间隙。这可以在下图中看到
类似地,如果在任何折叠的行上设置了MinHeight
或MaxHeight
,则它将不再折叠该行。我试图通过在数据触发器中为这些属性添加setter来解决这个问题,但没有解决
我的问题是,有什么不同的方法可以使行的大小无关紧要,或者如果设置了MinHeight
/MaxHeight
,就可以折叠行
MCVE MainWindow.xaml.cs MainWindow.xaml
崩溃
上述示例在技术上是错误的
它实际上是试图强制行的高度为0,这不是您想要或应该做的-问题是,即使高度为0,tab键也会穿过控件,并且“讲述人”将读取这些控件。从本质上讲,这些控件仍然存在,并且完全可单击、功能齐全且可访问,只是它们没有显示在窗口上,但它们仍然可以以各种方式访问,并且可能会影响应用程序的工作
第二(导致您描述的问题的原因是您没有描述上面的问题,尽管它们也很重要,不应该被忽略),您有GridSplitter
,并且如上所述,即使您强制其高度为0(如上所述),它仍然可以正常工作GridSplitter
意味着在一天结束时,您不受布局的控制,而受用户的控制
相反,您应该使用普通的行定义
,将其高度设置为自动
,然后将行内容的可见性
设置为折叠
——当然,您可以使用数据绑定和转换器
编辑:进一步澄清-在上面的代码中,您设置了名为
Collapsed
和InvertCollapsed
的新属性。因为它们的命名方式对折叠的行没有任何影响,所以它们也可以称为Property1和Property2。它们以一种非常奇怪的方式在DataTrigger
中使用-当它们的值更改时,该值将转换为Visibility
,然后如果转换后的值是折叠的
,则调用强制行高为0的设置器。所以有人玩了很多的布景,让它看起来像是在坍塌什么东西,但他没有,他只是改变了高度,这是完全不同的事情。这就是问题的根源。我当然建议完全避免这种方法,但如果您发现它对您的应用程序有好处,那么您需要做的最简单的事情就是在设置GridSplitter的第二行中避免这种方法,因为如果您不这样做,那么您的请求就变得不可能了。您所需要的只是缓存可见行的高度。之后,您不再需要转换器或切换包含控件的可见性
可折叠行
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>
Hi Ivan,通常当我使用这个时,组件可见性被设置为使用相同的绑定bool和convertor折叠,我将更新问题以显示这一点。至于自动建议,这并不理想,因为在我当前的程序中,我正在寻找一种方式,当屏幕初始化时,底部部分占据大约四分之一的可用空间,然后用户可以操纵/折叠它的大小。因此,我为什么要使用问题中的MCVE嗨,我试图解释这根本不像在其他情况下那样是折叠的,您可以检查是否添加控件并按照建议使用tab键。关键的部分是将GridSplitter可见性设置为Collapsed,而不是它的行,这将使它适用于您,即使您在应用程序的其余部分使用相同的代码。然而,我所描述的由您的方法引起的问题将仍然存在,但如果您对这些问题没有意见,那么您可以使用它们。@Dan我更新了进一步的澄清,因为我发现您不清楚,然后我猜其他人也不清楚,答案是正确的。@Ivan Well spotted+1。然而,对于大型XAML来说,手动设置所有控件的可见性可能会非常痛苦。我添加了行为来处理控件的绘图焦点。@Funk这就是数据绑定的目的,这样您就可以用一行o来更改所有内容
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
}
<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>
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
}
}
<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>
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);
}
}