对于WinUI3的ListView控件,通过设置ItemsStackPanel的AreStickyGroupHeadersEnabled属性,可以控制ListView分组显示,鼠标滚动时,最顶部的组头总是显示出来,看如下的效果:
而WPF的ListBox/ListView控件则没有此功能,这里从stackoverflow: gridview - WPF: Scroll Itemcontrol Content Fixed Header - Stack Overflow 找到了答案。
首先定义附加属性:
public static class StickyScrollHeader
{
public static FrameworkElement GetAttachToControl(FrameworkElement obj)
{
return (FrameworkElement)obj.GetValue(AttachToControlProperty);
}
public static void SetAttachToControl(FrameworkElement obj, FrameworkElement value)
{
obj.SetValue(AttachToControlProperty, value);
}
private static ScrollViewer FindScrollViewer(FrameworkElement item)
{
FrameworkElement treeItem = item;
FrameworkElement directItem = item;
while (treeItem != null)
{
treeItem = VisualTreeHelper.GetParent(treeItem) as FrameworkElement;
if (treeItem is ScrollViewer)
{
return treeItem as ScrollViewer;
}
//else if (treeItem is ScrollContentPresenter)
//{
// return (treeItem as ScrollContentPresenter).ScrollOwner;
//}
}
while (directItem != null)
{
directItem = directItem.Parent as FrameworkElement;
if (directItem is ScrollViewer)
{
return directItem as ScrollViewer;
}
else if (directItem is ScrollContentPresenter)
{
return (directItem as ScrollContentPresenter).ScrollOwner;
}
}
return null;
}
private static ScrollContentPresenter FindScrollContentPresenter(FrameworkElement sv)
{
int childCount = VisualTreeHelper.GetChildrenCount(sv);
for (int i = 0; i < childCount; i++)
{
if (VisualTreeHelper.GetChild(sv, i) is FrameworkElement child && child is ScrollContentPresenter)
{
return child as ScrollContentPresenter;
}
}
for (int i = 0; i < childCount; i++)
{
if (FindScrollContentPresenter(VisualTreeHelper.GetChild(sv, i) as FrameworkElement) is FrameworkElement child && child is ScrollContentPresenter)
{
return child as ScrollContentPresenter;
}
}
return null;
}
public static readonly DependencyProperty AttachToControlProperty =
DependencyProperty.RegisterAttached("AttachToControl", typeof(FrameworkElement), typeof(StickyScrollHeader), new PropertyMetadata(null, (s, e) =>
{
try
{
if (!(s is FrameworkElement targetControl))
{ return; }
Canvas.SetZIndex(targetControl, 999);
ScrollViewer sv;
FrameworkElement parent;
//此段代码几乎不执行
if (e.OldValue is FrameworkElement oldParentControl)
{
ScrollViewer oldSv = FindScrollViewer(oldParentControl);
parent = oldParentControl;
oldSv.ScrollChanged -= Sv_ScrollChanged;
}
if (e.NewValue is FrameworkElement newParentControl)
{
sv = FindScrollViewer(newParentControl);
parent = newParentControl;
//注册滚动事件
sv.ScrollChanged -= Sv_ScrollChanged;
sv.ScrollChanged += Sv_ScrollChanged;
//注册卸载事件
sv.Unloaded -= Sv_Unloaded;
sv.Unloaded += Sv_Unloaded;
}
void Sv_ScrollChanged(object sender, ScrollChangedEventArgs sce)
{
if (!parent.IsVisible) { return; }
try
{
ScrollViewer isv = sender as ScrollViewer;
ScrollContentPresenter scp = FindScrollContentPresenter(isv);
var relativeTransform = parent.TransformToAncestor(scp);
Rect parentRenderRect = relativeTransform.TransformBounds(new Rect(new Point(0, 0), parent.RenderSize));
Rect intersectingRect = Rect.Intersect(new Rect(new Point(0, 0), scp.RenderSize), parentRenderRect);
if (intersectingRect != Rect.Empty)
{
TranslateTransform targetTransform = new TranslateTransform();
if (parentRenderRect.Top < 0)
{
double tempTop = (parentRenderRect.Top * -1);
if (tempTop + targetControl.RenderSize.Height < parent.RenderSize.Height)
{
targetTransform.Y = tempTop;
}
else if (tempTop < parent.RenderSize.Height)
{
targetTransform.Y = tempTop - (targetControl.RenderSize.Height - intersectingRect.Height);
}
}
else
{
targetTransform.Y = 0;
}
targetControl.RenderTransform = targetTransform;
}
}
catch { }
}
void Sv_Unloaded(object sender, RoutedEventArgs e)
{
sv.ScrollChanged -= Sv_ScrollChanged;
sv.Unloaded -= Sv_Unloaded;
}
}
catch { }
}));
}
然后定义样式(我把stackoverflow网站上的代码稍微修改了一下,做成了标准样式,这样方便资源引用):
<Style x:Key="StickyGroupHeaderStyle" TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ContentPresenter
local:StickyScrollHeader.AttachToControl="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,AncestorType=Grid}}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentStringFormat="{TemplateBinding ContentStringFormat}"
Name="PART_Header" />
<ItemsPresenter Grid.Row="1" Name="ItemsPresenter"></ItemsPresenter>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
最后在任意ListBox/ListView/GridView中类似如下的代码通过ContainerStyle引用静态资源即可:
<ListBox.GroupStyle>
<GroupStyle ContainerStyle="{StaticResource StickyGroupHeaderStyle}">
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Background="Beige" FontWeight="Bold" Text="{Binding Path=Name, StringFormat={}{0}}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
显示效果如下(滚动切换非常完美):
唯一遗憾的是此功能不支持带Expander的组头,若谁研究出来共享一下,不胜感激。
前两天,换了一种思考方式,那就是不使用Expander控件,而是在内部内置ToggleButton,以达到折叠/展开的效果,样式如下:
<Style x:Key="StickyGroupHeaderStyle_WithExpander" TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" x:Name="row0"/>
<RowDefinition Height="Auto" x:Name="row1"/>
</Grid.RowDefinitions>
<ToggleButton x:Name="expander"
local:StickyScrollHeader.AttachToControl="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Grid}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Path x:Name="arrow" Data="M 1,1.5 L 4.5,5 L 8,1.5"
HorizontalAlignment="Center"
Stroke="Black"
SnapsToDevicePixels="false"
StrokeThickness="2"
VerticalAlignment="Center"/>
<ContentPresenter
Grid.Column="1"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentStringFormat="{TemplateBinding
ContentStringFormat}"
Name="PART_Header" />
</Grid>
</ToggleButton>
<ItemsPresenter Grid.Row="1" Name="ItemsPresenter"></ItemsPresenter>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="true" SourceName="expander">
<Setter Property="Visibility" TargetName="ItemsPresenter"
Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
当然上面的样式主要是为了测试,做得并不美观,连折叠按钮的上下箭头切换都没有,运行效果如下:
堆叠样式:
Wrap样式
上面的样式在运行中,上下滚动感觉还挺完美,但在运行我的另外一个项目时,尤其是当滚动时分组眉头只遮挡了项的部分高度时,如果此时点击折叠按钮,则会导致后面的眉头发生偏移,然后尝试将样式中的折叠控制可见性修改为控制第2行的行高为0,或者同时起作用,但总是有点小问题,那个项目和上面截图的项目对比了一下,总结一点:项高度和组头高度一致,且每次滚动的距离为项高度的整数倍则不会有问题(如同上面截图的这个项目),否则会或多和少出点问题,可能是上面定义的那个附加属性类代码不够完美,虽然在非折叠功能中相当完美。
像下面的应用截图,如果不集成折叠功能,固定组头也是完美的(下面第1行组头就固定了),但如果集成了上面的那个带折叠功能的样式,在滚动中就会有一些问题: