在 ItemsControl 中设置项目的最小和最大高度
Set Min and Max height of items in ItemsControl
我正在使用 ItemsControl 来显示包含 1 - 10 个项目(通常是 2 - 4)的列表。我正在努力满足所有这些要求:
- 所有行的高度必须相同
- 如果可能,所有行的显示高度应最大为 300。
- 如果没有足够的空间以 300 高显示所有行,则以最大可能的高度显示。
- 如果最大可能高度小于 150,则以最大尺寸显示并使用滚动条
- 如果行没有填满页面,则必须在顶部垂直对齐
这是我目前拥有的:
<Window x:Class="TestGridRows.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:vm="clr-namespace:TestGridRows"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:MainViewModel}"
Height="570" Width="800">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Path=DataItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border MinHeight="150" MaxHeight="300" BorderBrush="DarkGray" BorderThickness="1" Margin="5">
<TextBlock Text="{Binding Path=TheNameToDisplay}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="1" IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Window>
这是 1 件商品目前的样子:
这就是它应该的样子:
2 或 3 项按预期显示:
对于 4 个以上的项目,滚动条显示正确,但所有项目的大小都为 150,而不是 300:
问题
当只有 1 个项目时,如何将内容对齐到顶部? (显然没有破坏其他功能)
额外问题:当有 4 个以上的项目时,如何让项目调整到最大高度而不是最小高度?
在WPF布局过程中,测量和排列会按顺序进行。在大多数转换中,如果 UIElement
具有可变大小,则结果将 return 最小要求。但是,如果任何布局对齐设置为Stretch
,UIElement
将尽可能沿该方向排列。在您的情况下,UniFormGrid
将始终 return 160(即 Border.MinHeight
+ Border.Margin.Top
+ Border.Margin.Bottom
)*测量结果中所需高度的项目数(它将存储在 DesiredSize.DesiredSize.Height
中)。但是它将 ItemsControl.ActualHeight
作为排列的高度,因为它有 Stretch
VerticalAlignment
。因此,如果 UniFormGrid.DesiredSize.Height
小于 ItemsControl.ActualHeight
,UniFormGrid
并且任何 child 具有 Stretch
VerticalAlignment
将垂直拉伸,直到遇到它MaxHeight
。这就是为什么您的 1 项测试结果居中。如果将UniFormGrid.VerticalAlignment
或Border.VerticalAlignment
更改为Top
,您将在ItemsContorl
的顶部获得一个160高度的项目。
这两个问题最简单的解决方法是根据最大行高和最小行高覆盖测量结果。我在下面编写了代码并做了一些基本测试,它似乎工作得很好。
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
public class MyScrollViewer : ScrollViewer
{
public double DesiredViewportHeight;
public MyScrollViewer() : base() { }
protected override Size MeasureOverride(Size constraint)
{
// record viewport's height for late calculation
DesiredViewportHeight = constraint.Height;
var result = base.MeasureOverride(constraint);
// make sure that `ComputedVerticalScrollBarVisibility` will get correct value
if (ComputedVerticalScrollBarVisibility == Visibility.Visible && ExtentHeight <= ViewportHeight)
result = base.MeasureOverride(constraint);
return result;
}
}
public class MyUniformGrid : UniformGrid
{
private MyScrollViewer hostSV;
private ItemsControl hostIC;
public MyUniFormGrid() : base() { }
public double MaxRowHeight { get; set; }
public double MinRowHeight { get; set; }
protected override Size MeasureOverride(Size constraint)
{
if (hostSV == null)
{
hostSV = VisualTreeHelperEx.GetAncestor<MyScrollViewer>(this);
hostSV.SizeChanged += (s, e) =>
{
if (e.HeightChanged)
{
// need to redo layout pass after the height of host had changed.
this.InvalidateMeasure();
}
};
}
if (hostIC == null)
hostIC = VisualTreeHelperEx.GetAncestor<ItemsControl>(this);
var viewportHeight = hostSV.DesiredViewportHeight;
var rows = hostIC.Items.Count;
var rowHeight = viewportHeight / rows;
double desiredHeight = 0;
// calculate the correct height
if (rowHeight > MaxRowHeight || rowHeight < MinRowHeight)
desiredHeight = MaxRowHeight * rows;
else
desiredHeight = viewportHeight;
var result = base.MeasureOverride(constraint);
return new Size(result.Width, desiredHeight);
}
}
public class VisualTreeHelperEx
{
public static T GetAncestor<T>(DependencyObject reference, int level = 1) where T : DependencyObject
{
if (level < 1)
throw new ArgumentOutOfRangeException(nameof(level));
return GetAncestorInternal<T>(reference, level);
}
private static T GetAncestorInternal<T>(DependencyObject reference, int level) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(reference);
if (parent == null)
return null;
if (parent is T && --level == 0)
return (T)parent;
return GetAncestorInternal<T>(parent, level);
}
}
}
Xaml
<Window x:Class="WpfApp1.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:WpfApp1"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Height="570" Width="800">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl>
<sys:String>aaa</sys:String>
<sys:String>aaa</sys:String>
<sys:String>aaa</sys:String>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="DarkGray" BorderThickness="1" Margin="5">
<TextBlock Text="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=ContentPresenter}}"
VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:MyUniformGrid Columns="1" MinRowHeight="150" MaxRowHeight="300" VerticalAlignment="Top"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</local:MyScrollViewer>
</Window>
我正在使用 ItemsControl 来显示包含 1 - 10 个项目(通常是 2 - 4)的列表。我正在努力满足所有这些要求:
- 所有行的高度必须相同
- 如果可能,所有行的显示高度应最大为 300。
- 如果没有足够的空间以 300 高显示所有行,则以最大可能的高度显示。
- 如果最大可能高度小于 150,则以最大尺寸显示并使用滚动条
- 如果行没有填满页面,则必须在顶部垂直对齐
这是我目前拥有的:
<Window x:Class="TestGridRows.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:vm="clr-namespace:TestGridRows"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:MainViewModel}"
Height="570" Width="800">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Path=DataItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border MinHeight="150" MaxHeight="300" BorderBrush="DarkGray" BorderThickness="1" Margin="5">
<TextBlock Text="{Binding Path=TheNameToDisplay}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="1" IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Window>
这是 1 件商品目前的样子:
这就是它应该的样子:
2 或 3 项按预期显示:
对于 4 个以上的项目,滚动条显示正确,但所有项目的大小都为 150,而不是 300:
问题
当只有 1 个项目时,如何将内容对齐到顶部? (显然没有破坏其他功能)
额外问题:当有 4 个以上的项目时,如何让项目调整到最大高度而不是最小高度?
在WPF布局过程中,测量和排列会按顺序进行。在大多数转换中,如果 UIElement
具有可变大小,则结果将 return 最小要求。但是,如果任何布局对齐设置为Stretch
,UIElement
将尽可能沿该方向排列。在您的情况下,UniFormGrid
将始终 return 160(即 Border.MinHeight
+ Border.Margin.Top
+ Border.Margin.Bottom
)*测量结果中所需高度的项目数(它将存储在 DesiredSize.DesiredSize.Height
中)。但是它将 ItemsControl.ActualHeight
作为排列的高度,因为它有 Stretch
VerticalAlignment
。因此,如果 UniFormGrid.DesiredSize.Height
小于 ItemsControl.ActualHeight
,UniFormGrid
并且任何 child 具有 Stretch
VerticalAlignment
将垂直拉伸,直到遇到它MaxHeight
。这就是为什么您的 1 项测试结果居中。如果将UniFormGrid.VerticalAlignment
或Border.VerticalAlignment
更改为Top
,您将在ItemsContorl
的顶部获得一个160高度的项目。
这两个问题最简单的解决方法是根据最大行高和最小行高覆盖测量结果。我在下面编写了代码并做了一些基本测试,它似乎工作得很好。
namespace WpfApp1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
public class MyScrollViewer : ScrollViewer
{
public double DesiredViewportHeight;
public MyScrollViewer() : base() { }
protected override Size MeasureOverride(Size constraint)
{
// record viewport's height for late calculation
DesiredViewportHeight = constraint.Height;
var result = base.MeasureOverride(constraint);
// make sure that `ComputedVerticalScrollBarVisibility` will get correct value
if (ComputedVerticalScrollBarVisibility == Visibility.Visible && ExtentHeight <= ViewportHeight)
result = base.MeasureOverride(constraint);
return result;
}
}
public class MyUniformGrid : UniformGrid
{
private MyScrollViewer hostSV;
private ItemsControl hostIC;
public MyUniFormGrid() : base() { }
public double MaxRowHeight { get; set; }
public double MinRowHeight { get; set; }
protected override Size MeasureOverride(Size constraint)
{
if (hostSV == null)
{
hostSV = VisualTreeHelperEx.GetAncestor<MyScrollViewer>(this);
hostSV.SizeChanged += (s, e) =>
{
if (e.HeightChanged)
{
// need to redo layout pass after the height of host had changed.
this.InvalidateMeasure();
}
};
}
if (hostIC == null)
hostIC = VisualTreeHelperEx.GetAncestor<ItemsControl>(this);
var viewportHeight = hostSV.DesiredViewportHeight;
var rows = hostIC.Items.Count;
var rowHeight = viewportHeight / rows;
double desiredHeight = 0;
// calculate the correct height
if (rowHeight > MaxRowHeight || rowHeight < MinRowHeight)
desiredHeight = MaxRowHeight * rows;
else
desiredHeight = viewportHeight;
var result = base.MeasureOverride(constraint);
return new Size(result.Width, desiredHeight);
}
}
public class VisualTreeHelperEx
{
public static T GetAncestor<T>(DependencyObject reference, int level = 1) where T : DependencyObject
{
if (level < 1)
throw new ArgumentOutOfRangeException(nameof(level));
return GetAncestorInternal<T>(reference, level);
}
private static T GetAncestorInternal<T>(DependencyObject reference, int level) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(reference);
if (parent == null)
return null;
if (parent is T && --level == 0)
return (T)parent;
return GetAncestorInternal<T>(parent, level);
}
}
}
Xaml
<Window x:Class="WpfApp1.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:WpfApp1"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Height="570" Width="800">
<local:MyScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl>
<sys:String>aaa</sys:String>
<sys:String>aaa</sys:String>
<sys:String>aaa</sys:String>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="DarkGray" BorderThickness="1" Margin="5">
<TextBlock Text="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=ContentPresenter}}"
VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:MyUniformGrid Columns="1" MinRowHeight="150" MaxRowHeight="300" VerticalAlignment="Top"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</local:MyScrollViewer>
</Window>