WPF:滚动Itemcontrol内容固定标题

是否有可能使用WPF的ItemsControl:Demo来做这样的事情

我试图冻结GroupedItems而不是GridView列。

在这里输入图像描述

资源:

<Window.Resources>
        <CollectionViewSource x:Key="data" Source="{Binding}">
            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="Date"/>
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>
    </Window.Resources>

列表显示:

<ListView Grid.Column="0" ItemsSource="{Binding Source={StaticResource data}}">
    <ListView.View>
        <GridView>
            <GridView.Columns>
                <GridViewColumn Header="Col 1" DisplayMemberBinding="{Binding Col1}" Width="100"/>
                <GridViewColumn Header="Col 2" DisplayMemberBinding="{Binding Col2}" Width="100"/>
                <GridViewColumn Header="Col 3" DisplayMemberBinding="{Binding Col3}" Width="100"/>
            </GridView.Columns>
        </GridView>
    </ListView.View>
    <ListView.GroupStyle>
        <GroupStyle>
            <GroupStyle.ContainerStyle>
                <Style 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>
                                            <Grid Grid.Row="0">
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition Width="*"/>
                                        </Grid.ColumnDefinitions>
                                        <TextBlock Background="Beige" FontWeight="Bold" Text="{Binding Path=Name, StringFormat={}{0}}"/>
                                    </Grid>
                                    <DockPanel Grid.Row="1">
                                        <ItemsPresenter Grid.Row="2"></ItemsPresenter>
                                    </DockPanel>
                                        </Grid>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </GroupStyle.ContainerStyle>
        </GroupStyle>
    </ListView.GroupStyle>
</ListView>

后面的代码:

public MainWindow()
        {
            InitializeComponent();

            List<String> colList1 = new List<string>(){"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7"};
            List<String> colList2 = new List<string>(){"1", "2", "3", "4", "5", "6"};


            ObservableCollection<Data> dataCollection = new ObservableCollection<Data>();

            for (var a = 0; a<100; a++){
                Random rnd = new Random();
                int min = rnd.Next(5000);
                int rnd1 = rnd.Next(0, 6);
                int rnd2 = rnd.Next(0, 5);

                dataCollection .Add(
                    new Data(){
                    Date = DateTime.Now.AddMinutes(min).ToString("hh:MM tt"),
                    Col1= colList1[rnd2],
                    Col2= String.Format("Col2: {0}", "X"),
                    Col3= colList2[rnd2]
                    }
                );

            }
             this.DataContext = dataCollection;
          }

          public class Data
          {
            public string Date { get; set; }
            public string Col1{ get; set; }
            public string Col2{ get; set; }
            public string Col3{ get; set; }
          }

我的解决方案使用共享组标题样式的TextBlock叠加层。 定位和正确的HitTesting是棘手的部分,但我相当确信这不会因布局或逻辑的小改动而中断。

我不确定是否要隐藏ColumnHeader,但这很容易,不需要任何其他调整,而不需要进行其他调整。

代码背后:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace WpfApplication1
{
    public partial class FreezingGroupHeader : UserControl
    {
        private double _listviewHeaderHeight;
        private double _listviewSideMargin;

        public FreezingGroupHeader()
        {
            InitializeComponent();

            List<String> colList1 = new List<string>() { "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7" };
            List<String> colList2 = new List<string>() { "1", "2", "3", "4", "5", "6" };

            ObservableCollection<Data> dataCollection = new ObservableCollection<Data>();

            Random rnd = new Random();

            for (var a = 0; a < 100; a++)
            {
                int min = rnd.Next(5000);
                int rnd1 = rnd.Next(0, 6);
                int rnd2 = rnd.Next(0, 5);

                dataCollection.Add(
                    new Data()
                    {
                        Date = DateTime.Now.AddMinutes(min).ToString("hh:MM tt"),
                        Col1 = colList1[rnd2],
                        Col2 = String.Format("Col2: {0}", "X"),
                        Col3 = colList2[rnd2]
                    }
                );
            }
            this.DataContext = dataCollection;

            this.Loaded += OnLoaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            // Position frozen header
            GetListViewMargins(this.listview1);

            Thickness margin = this.frozenGroupHeader.Margin;
            margin.Top = _listviewHeaderHeight;
            margin.Right = SystemParameters.VerticalScrollBarWidth + _listviewSideMargin;
            margin.Left = _listviewSideMargin;

            this.frozenGroupHeader.Margin = margin;

            UpdateFrozenGroupHeader();
        }

        private void listview1_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            UpdateFrozenGroupHeader();
        }

        /// <summary>
        /// Sets text and visibility of frozen header
        /// </summary>
        private void UpdateFrozenGroupHeader()
        {
            if (listview1.HasItems)
            {
                // Text of frozenGroupHeader
                GroupItem group = GetFirstVisibleGroupItem(this.listview1);
                if (group != null)
                {
                    object data = group.Content;
                    this.frozenGroupHeader.Text = data.GetType().GetProperty("Name").GetValue(data, null) as string;  // slight hack
                }
                this.frozenGroupHeader.Visibility = Visibility.Visible;
            }
            else
                this.frozenGroupHeader.Visibility = Visibility.Collapsed;
        }

        /// <summary>
        /// Sets values that will be used in the positioning of the frozen header
        /// </summary>
        private void GetListViewMargins(ListView listview)
        {
            if (listview.HasItems)
            {
                object o = listview.Items[0];
                ListViewItem firstItem = (ListViewItem)listview.ItemContainerGenerator.ContainerFromItem(o);
                if (firstItem != null)
                {
                    GroupItem group = FindUpVisualTree<GroupItem>(firstItem);
                    Point p = group.TranslatePoint(new Point(0, 0), listview);
                    _listviewHeaderHeight = p.Y; // height of columnheader
                    _listviewSideMargin = p.X; // listview borders
                }
            }
        }

        /// <summary>
        /// Gets the first visible GroupItem in the listview
        /// </summary>
        private GroupItem GetFirstVisibleGroupItem(ListView listview)
        {
            HitTestResult hitTest = VisualTreeHelper.HitTest(listview, new Point(5, _listviewHeaderHeight + 5));
            GroupItem group = FindUpVisualTree<GroupItem>(hitTest.VisualHit);
            return group;
        }


        /// <summary>
        /// walk up the visual tree to find object of type T, starting from initial object
        /// http://www.codeproject.com/Tips/75816/Walk-up-the-Visual-Tree
        /// </summary>
        private static T FindUpVisualTree<T>(DependencyObject initial) where T : DependencyObject
        {
            DependencyObject current = initial;

            while (current != null && current.GetType() != typeof(T))
            {
                current = VisualTreeHelper.GetParent(current);
            }
            return current as T;
        }

        public class Data
        {
            public string Date { get; set; }
            public string Col1 { get; set; }
            public string Col2 { get; set; }
            public string Col3 { get; set; }
        }
    }
}

XAML:

<UserControl x:Class="WpfApplication1.FreezingGroupHeader"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
           >

    <UserControl.Resources>
        <CollectionViewSource x:Key="data" Source="{Binding}">
            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="Date"/>
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>

        <Style x:Key="GroupHeaderStyle1" TargetType="{x:Type TextBlock}">
            <Setter Property="Background" Value="Beige" />
            <Setter Property="Foreground" Value="Black" />
            <Setter Property="FontWeight" Value="Bold" />
        </Style>
    </UserControl.Resources>

    <Grid>
        <ListView x:Name="listview1" Grid.Column="0" ItemsSource="{Binding Source={StaticResource data}}" ScrollViewer.ScrollChanged="listview1_ScrollChanged" >
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Header="Col 1" DisplayMemberBinding="{Binding Col1}" Width="100"/>
                        <GridViewColumn Header="Col 2" DisplayMemberBinding="{Binding Col2}" Width="100"/>
                        <GridViewColumn Header="Col 3" DisplayMemberBinding="{Binding Col3}" Width="100"/>
                    </GridView.Columns>
                </GridView>
            </ListView.View>
            <ListView.GroupStyle>
                <GroupStyle>
                    <GroupStyle.ContainerStyle>
                        <Style 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>
                                            <Grid Grid.Row="0">
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="*"/>
                                                </Grid.ColumnDefinitions>
                                                <TextBlock Style="{StaticResource GroupHeaderStyle1}" Text="{Binding Name, StringFormat={}{0}}"  />
                                            </Grid>
                                            <DockPanel Grid.Row="1">
                                                <ItemsPresenter Grid.Row="2"></ItemsPresenter>
                                            </DockPanel>
                                        </Grid>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </GroupStyle.ContainerStyle>
                </GroupStyle>
            </ListView.GroupStyle>
        </ListView>
        <TextBlock x:Name="frozenGroupHeader" Style="{StaticResource GroupHeaderStyle1}" VerticalAlignment="Top"/>
    </Grid>
</UserControl>

这个解决方案不是很好,它是黑客,但它基本上会做你想做的。 我使listview标题不可见,将一个文本块放在列表视图上方,并将该文本值设置为列表框中第一个可见项目的groupitem。 哈克,但这是我想出的最好的。

public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();

        List<String> colList1 = new List<string>() { "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7" };
        List<String> colList2 = new List<string>() { "1", "2", "3", "4", "5", "6" };


        ObservableCollection<Data> dataCollection = new ObservableCollection<Data>();

        for (var a = 0; a < 100; a++)
        {
            Random rnd = new Random();
            int min = rnd.Next(5000);
            int rnd1 = rnd.Next(0, 6);
            int rnd2 = rnd.Next(0, 5);

            dataCollection.Add(
                new Data()
                {
                    Date = DateTime.Now.AddMinutes(min).ToString("hh:MM tt"),
                    Col1 = colList1[rnd2],
                    Col2 = String.Format("Col2: {0}", "X"),
                    Col3 = colList2[rnd2]
                }
            );

        }
        this.DataContext = dataCollection;
    }

    public class Data
    {
        public string Date { get; set; }
        public string Col1 { get; set; }
        public string Col2 { get; set; }
        public string Col3 { get; set; }
    }

    private void grid_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (grid.Items.Count > 0)
        {
            HitTestResult hitTest = VisualTreeHelper.HitTest(grid, new Point(5, 5));
            System.Windows.Controls.ListViewItem item = GetListViewItemFromEvent(null, hitTest.VisualHit) as System.Windows.Controls.ListViewItem;
            if (item != null)
                Head.Text = ((Data)item.Content).Date;
        }

    }

    System.Windows.Controls.ListViewItem GetListViewItemFromEvent(object sender, object originalSource)
    {
        DependencyObject depObj = originalSource as DependencyObject;
        if (depObj != null)
        {
            // go up the visual hierarchy until we find the list view item the click came from  
            // the click might have been on the grid or column headers so we need to cater for this  
            DependencyObject current = depObj;
            while (current != null && current != grid)
            {
                System.Windows.Controls.ListViewItem ListViewItem = current as System.Windows.Controls.ListViewItem;
                if (ListViewItem != null)
                {
                    return ListViewItem;
                }
                current = VisualTreeHelper.GetParent(current);
            }
        }

        return null;
    }

}

XAML:

<Window x:Class="header.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="auto" SizeToContent="Width">
<Window.Resources>
    <CollectionViewSource x:Key="data" Source="{Binding}">
        <CollectionViewSource.GroupDescriptions>
            <PropertyGroupDescription PropertyName="Date"/>
        </CollectionViewSource.GroupDescriptions>
    </CollectionViewSource>

    <Style x:Key="myHeaderStyle" TargetType="{x:Type GridViewColumnHeader}">
        <Setter Property="Visibility" Value="Collapsed" />
    </Style>
</Window.Resources>
<Grid>
    <StackPanel>
    <TextBlock Name="Head" Grid.Row="0"/>
            <ListView Name="grid" Grid.Column="0" Grid.Row="1" ItemsSource="{Binding Source={StaticResource data}}" Height="300" ScrollViewer.ScrollChanged="grid_ScrollChanged">
                <ListView.View>
                    <GridView ColumnHeaderContainerStyle="{StaticResource myHeaderStyle}">
                        <GridView.Columns>
                            <GridViewColumn DisplayMemberBinding="{Binding Col1}" Width="100"/>
                            <GridViewColumn DisplayMemberBinding="{Binding Col2}" Width="100"/>
                            <GridViewColumn DisplayMemberBinding="{Binding Col3}" Width="100"/>
                        </GridView.Columns>
                    </GridView>
                </ListView.View>
                <ListView.GroupStyle>
                    <GroupStyle>
                        <GroupStyle.ContainerStyle>
                            <Style 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>
                                                <Grid Grid.Row="0">
                                                    <Grid.ColumnDefinitions>
                                                        <ColumnDefinition Width="*"/>
                                                    </Grid.ColumnDefinitions>
                                                    <TextBlock Background="Beige" FontWeight="Bold" Text="{Binding Path=Name, StringFormat={}{0}}"/>
                                                </Grid>
                                                <DockPanel Grid.Row="1">
                                                    <ItemsPresenter Grid.Row="2"></ItemsPresenter>
                                                </DockPanel>
                                            </Grid>
                                        </ControlTemplate>
                                    </Setter.Value>
                                </Setter>
                            </Style>
                        </GroupStyle.ContainerStyle>
                    </GroupStyle>
                </ListView.GroupStyle>
            </ListView>
    </StackPanel>
</Grid>

编辑:修复ScrollChanged事件。

private void grid_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (grid.Items.Count > 0)
        {
            Point point = new Point(5, 5);
            foreach(Data lvItem in grid.Items)
            {
                HitTestResult hitTest = VisualTreeHelper.HitTest(grid, point);
                ListViewItem item = GetListViewItemFromEvent(null, hitTest.VisualHit) as System.Windows.Controls.ListViewItem;
                if (item != null)
                {
                    Data value = ((Data)item.Content);
                    Head.Text = ((Data)item.Content).Date;
                    break;
                }
                else
                {
                    point.X += 5;
                    point.Y += 5;
                }
            }
        }

    }

当我遇到类似的问题,'hack-ish'解决方案不适合我的需求,我通常不喜欢生产环境中的'hack-ish'的东西,我开发了一个通用的解决方案,我想分享。 附加的类具有以下关键特征:

  • MVVM兼容
  • 没有代码隐藏
  • 与ListView,GridView,ItemsControl兼容,甚至是静态的xaml! - 应该使用ScollViewer来处理任何事物...
  • 使用附加属性来声明组项目
  • xaml使用(只是你的内部ControlTemplate):

    <ControlTemplate TargetType="{x:Type GroupItem}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition  Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Grid Grid.Row="0" local:StickyScrollHeader.AttachToControl="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Grid}}">
                <TextBlock Background="Beige" FontWeight="Bold" Text="{Binding Path=Name, StringFormat={}{0}}"/>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
            </Grid>
            <DockPanel Grid.Row="1">
                <ItemsPresenter Grid.Row="2"></ItemsPresenter>
            </DockPanel>
        </Grid>
    </ControlTemplate>
    

    类(放在任何地方,如果需要添加xaml-namespace):

    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;
                    }
    
                    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 { }
                    }
                }
                catch { }
            }));
    }
    

    希望这也能帮助其他人解决这个问题;)

    链接地址: http://www.djcxy.com/p/69157.html

    上一篇: WPF: Scroll Itemcontrol Content Fixed Header

    下一篇: Using Anchors with Equal Height Columns Hides Content