我的解决方案使用共享组标题样式的 TextBlock 覆盖。定位和正确的命中测试是棘手的部分,但我非常有信心这不会因布局或逻辑的微小变化而中断。
我不确定您是否想隐藏 ColumnHeader,但这很简单,除了所描述的内容之外不需要任何其他调整here.
背后代码:
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>