WPF ListBox滚动时固定组头

        对于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行组头就固定了),但如果集成了上面的那个带折叠功能的样式,在滚动中就会有一些问题:

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值