WPF:滚动Itemcontrol内容固定头(WPF: Scroll Itemcontrol Con

2019-07-19 16:39发布

是否有可能做这样的事情与WPF的ItemsControl的: 演示

我试图冻结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; }
          }

Answer 1:

我的解决方案使用共享组标题样式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>


Answer 2:

该解决方案是不是很大,这是黑客十岁上下,但它基本上会做你想要什么。 我所做的列表视图头看不见的,把一个文本块列表视图上面,和文本值设置为ListBox中第一个可见项的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;
                }
            }
        }

    }


Answer 3:

正如我刚才碰到了类似的问题和“黑客十岁上下的”解决方案并不适合我的需要和我一般不喜欢在生产环境中“黑客十岁上下”的东西,我开发了一个通用的解决方案,这里面我想与大家分享。 所附的类具有以下关键特征:

  • MVVM兼容
  • 没有代码隐藏
  • 与ListView中,GridView控件,ItemsControl的,即使是静态的XAML兼容! - 应该使用什么ScollViewer工作...
  • 使用附加属性来声明组项目

XAML用法(只是你内心的控件模板):

<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命名空间):

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

希望这可以帮助其他人遇到这个问题;)



文章来源: WPF: Scroll Itemcontrol Content Fixed Header