WPF:Scroll Itemcontrol内容固定标题

时间:2013-02-10 18:31:35

标签: wpf gridview scrollview itemscontrol

是否可以使用WPF的ItemsControl执行类似的操作:Demo

我正在尝试冻结GroupedItems而不是GridView列。

enter image description here

资源:

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

的ListView:

<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; }
          }

3 个答案:

答案 0 :(得分:3)

我的解决方案使用共享组头样式的TextBlock叠加层。定位和正确的HitTesting是一个棘手的部分,但我非常有信心这不会因为布局或逻辑上的微小变化而中断。

我不确定您是否要隐藏ColumnHeader,但这很容易,并且不需要进行除here所示的任何其他调整。

enter image description 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>

答案 1 :(得分:1)

这个解决方案不是很好,而且它很糟糕,但它基本上可以做你想要的。我使listview标头不可见,在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;
                }
            }
        }

    }

答案 2 :(得分:0)

因为我刚遇到了类似的问题,并且&#39; hack-ish&#39;解决方案不符合我的需求,我一般不喜欢&#39; hack-ish&#39;在生产环境中的东西,我开发了一个通用的解决方案,我想分享。 附加的类具有以下关键功能:

  • 兼容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>

Class(放在任何地方,必要时添加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 { }
        }));
}

希望这也有助于其他人遇到这个问题;)