19
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

グループ化したコレクションのヘッダーをスクロール時に常に表示する

Posted at

基本のおさらい

まずは基本的な部分の確認です。
1.CollectionViewSourceを用意
2.ItemsControlでItemsSourceに1で用意したCollectionViewSourceをバインド
3.スタイルでグループヘッダーの見た目を定義

具体的には
1.リソースにCollectionViewSourceを定義
PropertyGroupDescriptionでグループ化したいプロパティ名称を指定します。

<UserControl.Resources>
    <CollectionViewSource x:Key="StaffCollectionKey"
                          Source="{Binding Staffs}">
        <CollectionViewSource.GroupDescriptions>
            <PropertyGroupDescription PropertyName="Model.Department" />
        </CollectionViewSource.GroupDescriptions>
    </CollectionViewSource>
</UserControl.Resources>

2.コレクションをバインド

<ItemsControl ItemsSource="{Binding Source={StaticResource StaffCollectionKey}}">
  (略)
</ItemsControl>

3.ヘッダーのスタイルを定義

<ItemsControl.GroupStyle>
    <GroupStyle>
        <GroupStyle.HeaderTemplate>
            <DataTemplate>
                <Border Margin="1 0 1 4"
                        Background="DimGray">
                    <TextBlock Text="{Binding Name}"
                               Foreground="Snow"/>
                </Border>
            </DataTemplate>
        </GroupStyle.HeaderTemplate>
    </GroupStyle>
</ItemsControl.GroupStyle>

上記3ステップでグループ化表示ができます。(WPFステキ!)

キャプチャ3.PNG

本題

無事にグループ化表示できたわけですが、スクロールしたときにグループヘッダーも同時に移動してしまう欠点があります。

キャプチャ4.PNG
↑「1010 スタッフ3」の所属がわからない、の図

今回はこの欠点を克服するために、スクロール時に常にグループヘッダが表示されるようにカスタマイズを行います。

実装方法

基本的にはこの記事と同じように、Adornerを利用します。参照元では添付プロパティで実装してますが、ここではBehaviorとして実装します。また、ListBoxにしか対応できない(ItemsControl内にScrollViewerが存在すること前提になっている)ため、ListBox以外のItemsControlにも対応できるように修正しました。


    /// <summary>
    /// GroupItemのヘッダを固定表示するBehavior
    /// </summary>
    public class GroupHeaderFrozenBehavior : Behavior<ItemsControl>
    {
        private static readonly Dictionary<GroupItem, WeakReference<HeaderAdorner>> _CurrentGroupItem = new Dictionary<GroupItem, WeakReference<HeaderAdorner>>();

        #region HeaderTemplate依存関係プロパティ
        public DataTemplate HeaderTemplate
        {
            get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
            set { SetValue(HeaderTemplateProperty, value); }
        }

        // Using a DependencyProperty as the backing store for HeaderTemplate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HeaderTemplateProperty =
            DependencyProperty.Register("HeaderTemplate", typeof(DataTemplate), typeof(GroupHeaderFrozenBehavior), new PropertyMetadata(null)); 
        #endregion


        protected override void OnAttached()
        {
            base.OnAttached();

            AssociatedObject.Loaded += AssociatedObject_Loaded;
            AssociatedObject.Unloaded += AssociatedObject_Unloaded;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.Loaded -= AssociatedObject_Loaded;
            AssociatedObject.Unloaded -= AssociatedObject_Unloaded;
        }

        private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
        {
            SetScrollChangedEvent(true);
        }

        private void AssociatedObject_Unloaded(object sender, RoutedEventArgs e)
        {
            SetScrollChangedEvent(false);
        }

        private void SetScrollChangedEvent(bool add)
        {
            ScrollViewer scrollViewer;

            if(AssociatedObject is ListBox)
            {
                // リストボックス内のScrollViewerを探す
                scrollViewer = AssociatedObject.FindChild<ScrollViewer>();
            }
            else
            {
                // 親方向にScrollViewerを探す
                scrollViewer = AssociatedObject.FindParent<ScrollViewer>();
            }

            if (scrollViewer != null)
            {
                if(add)
                {
                    scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
                }
                else
                {
                    scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
                }
            }
        }

        private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            var scrollViewer = (ScrollViewer)sender;

            // ScrollViewerの描画サイズ
            var scrollViewerRectangle = new Rect(new Point(0, 0), scrollViewer.RenderSize);

            // ItemsControlのGroupItem
            foreach(var containerItem in AssociatedObject.ItemContainerGenerator.Items)
            {
                // GroupItemコントロールを取得
                var groupItemContainer = AssociatedObject.ItemContainerGenerator.ContainerFromItem(containerItem) as GroupItem;
                if(groupItemContainer==null)
                {
                    Debug.WriteLine("Failed: get groupItemContainer");
                    return;
                }

                // ScrollViewerを基準とした描画位置を計算
                var transform = groupItemContainer.TransformToAncestor(scrollViewer);
                var groupItemRect = transform.TransformBounds(new Rect(new Point(0, 0), groupItemContainer.RenderSize));

                // ScrollViewerとGroupItemの重なりを確認
                var intersectRect = Rect.Intersect(scrollViewerRectangle, groupItemRect);

                // ヘッダー固定表示用のAdornerを表示する必要があるかどうか
                var needDisplayAdorner = true;

                // ScrollViewerの描画エリア内にGroupItemが配置されている
                needDisplayAdorner &= intersectRect != Rect.Empty;
                
                // かつ、GroupItemの上端がScrollViewerの上端よりも上に存在
                needDisplayAdorner &= groupItemRect.Top <= 0;

                // GroupItemのAdornerLayerを取得
                var adornerLayer = AdornerLayer.GetAdornerLayer(groupItemContainer);
                if(adornerLayer==null)
                {
                    Debug.WriteLine("Failed: get adornerLayer of GroupItem");
                    return;
                }

                // Adornerの表示が必要な場合
                if(needDisplayAdorner)
                {
                    // すでにAdornerを作成している場合はその位置を更新するだけ
                    var headerAdorner = GetAdorner(groupItemContainer);
                    if (headerAdorner != null)
                    {
                        headerAdorner.UpdateLocation(groupItemRect.Top);
                        return;
                    }

                    // 未作成の場合は新規にAdornerを作成し、Dictionaryに加える
                    var adorner = new HeaderAdorner(groupItemContainer)
                    {
                        DataContext = containerItem,
                        HeaderTemplate = HeaderTemplate,
                        Top = Math.Abs(groupItemRect.Top)
                    };
                    adornerLayer.Add(adorner);

                    _CurrentGroupItem.Add(groupItemContainer, new WeakReference<HeaderAdorner>(adorner));
                }
                // Adornerの表示が不要な場合
                else
                {
                    // AdornerをAdornerLayerとDictionaryから除去
                    var adorner = GetAdorner(groupItemContainer);
                    if (adorner != null)
                    {
                        adornerLayer.Remove(adorner);
                        _CurrentGroupItem.Remove(groupItemContainer);
                    }
                }
            }

        }


        private HeaderAdorner GetAdorner(GroupItem container)
        {
            if (_CurrentGroupItem.ContainsKey(container))
            {
                HeaderAdorner adorner;
                if (_CurrentGroupItem[container].TryGetTarget(out adorner))
                {
                    return adorner;
                }
            }

            return null;
        }
    }

利用するときは、下記のようにItemsControlにBehaviorを設定&GroupItemのHeaderTemplateを設定します。グループヘッダーはDataTemplateとしてリソース化しておくと管理しやすくなります。

<UserControl x:Class="ControlsShowcase.Views.GroupItemTabView"
             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" 
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
             xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
             xmlns:local="clr-namespace:ControlsShowcase.Views"
             xmlns:vm="clr-namespace:ControlsShowcase.ViewModels"
             xmlns:b="clr-namespace:ControlsShowcase.Behaviors"
             mc:Ignorable="d" 
             Foreground="Black"
             d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.Resources>
        <CollectionViewSource x:Key="StaffCollectionKey"
                              Source="{Binding Staffs}">
            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="Model.Department" />
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>

        <DataTemplate x:Key="GroupItemHeaderTemplateKey">
            <Border Margin="2"
                    Padding="4 2"
                    Background="DimGray">
                <TextBlock Text="{Binding Name}"
                           Foreground="Snow"/>
            </Border>

        </DataTemplate>
    </UserControl.Resources>
    
    <UserControl.DataContext>
        <vm:GroupItemTabViewModel />
    </UserControl.DataContext>
    
    <Grid>
        <ScrollViewer>
            <ItemsControl ItemsSource="{Binding Source={StaticResource StaffCollectionKey}}">
                <i:Interaction.Behaviors>
                    <b:GroupHeaderFrozenBehavior HeaderTemplate="{StaticResource GroupItemHeaderTemplateKey}" />
                </i:Interaction.Behaviors>
                
                <ItemsControl.GroupStyle>
                    <GroupStyle HeaderTemplate="{StaticResource GroupItemHeaderTemplateKey}">
                    </GroupStyle>
                </ItemsControl.GroupStyle>
                
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid Margin="4 1">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="60" />
                                <ColumnDefinition />
                            </Grid.ColumnDefinitions>

                            <TextBlock Text="{Binding Model.Id}" />

                            <TextBlock Grid.Column="1"
                                       Text="{Binding Model.Name}" />
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>

    </Grid>
</UserControl>

これでめでたくグループヘッダーの表示をスクロール時に固定することができました!

groupitem2.gif

ソースはこちらに上げています。

参考

19
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?