うにてぃブログ

UnityやUnreal Engineの記事を書いていきます

【Unity】TreeView でソートを実装する

TreeView でソートをお手軽に実装するには MultiColumnHeader を利用する

MultiColumnHeader では ソート用のイベントとして sortingChanged が用意されており、これにイベントを登録することでどのカラムがソートされたかを判定できる

        internal TreeView(TreeViewState state, MultiColumnHeader multiColumnHeader) : base(state, multiColumnHeader)
        {
            multiColumnHeader.sortingChanged += OnSortingChanged;
            Reload();
        }

ソート処理全貌は以下のようになる

internal class SortableTreeView : TreeView
{
    internal SortableTreeView(TreeViewState state, MultiColumnHeader multiColumnHeader) : base(state, multiColumnHeader)
    {
        multiColumnHeader.sortingChanged += OnSortingChanged;        
        Reload();
    }
 
    /// <summary>
    /// ソート変更
    /// </summary>
    private void OnSortingChanged(MultiColumnHeader multiColumnHeader)
    {
        if (GetRows().Count <= 1)
            return;
        
        Sort(GetRows());
    }
 
    /// <summary>
    /// ソートを行う
    /// </summary>
    private void Sort(IList<TreeViewItem> rows)
    {
        // カラム別のソート順を取得する
        var sortedColumns = multiColumnHeader.state.sortedColumns;
        if (multiColumnHeader.sortedColumnIndex == -1 ||
            sortedColumns.Length == 0
           )
            return;
        
        // ソートするためにTreeViewItem をキャストする
        var cast = rootItem.children.Cast<TreeViewItem>();
        // 昇順ソートかどうかの判定
        var ascending = multiColumnHeader.IsSortedAscending(sortedColumns[0]);
        // 並べ替えするために 最初のソートカラムを基準にIOrderedEnumerable に変換する
        var items = InitialOrder(cast, ascending);
          
        // 2番以降のカラムをソートしていく
        for (var i = 1; i < sortedColumns.Length; i++)
        {
            // 昇順かどうか 
            ascending = multiColumnHeader.IsSortedAscending(sortedColumns[i]);
            // ThenBy で更にソートしていく
            switch (sortedColumns[i])
            {
                case 0:
                    items = ThenBy(items, i => i.displayName, ascending);
                    break;
                case 1:
                    items = ThenBy(items, i => i.id, ascending);
                    break;
            }
        }
        IOrderedEnumerable<T> ThenBy<T, TKey>(IOrderedEnumerable<T> source, Func<T, TKey> selector, bool ascending)
        {
            return @ascending ? source.ThenBy(selector) : source.ThenByDescending(selector);
        }
 
        // ソート後の TreeViewItem を再度子供に登録
        rootItem.children = items.Cast<TreeViewItem>().ToList();
        // Rows もソートされているためこちらにも並べ替えた結果を追加する
        rows.Clear();
        foreach (var item in rootItem.children)
        {
            rows.Add(item);
        }
        Repaint();
    }
   
    /// <summary>
    /// IOrderedEnumerable 変換
    /// カラムによって何をソート対象にするかを記述する
    /// </summary>
    private IOrderedEnumerable<DetailTreeViewItem> InitialOrder(IEnumerable<TreeViewItem> items, bool ascending)
    {
        switch (multiColumnHeader.state.sortedColumns[0])
        {
            case 1:
                return Order(items, i => i.id);
            default:
                return Order(items, i => i.displayName);
        }
        
        IOrderedEnumerable<T> Order<T, TKey>(IEnumerable<T> source, Func<T, TKey> selector)
        {
            return @ascending ? source.OrderBy(selector) : source.OrderByDescending(selector);
        }
    }
    
    protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
    {
        if (!root.hasChildren)
            return new List<TreeViewItem>();
 
        var rows = base.BuildRows(root);
        // コンパイル走った際にソートしている必要があるので初回生成時もソートする
        Sort(rows);
        return rows;
    }