[Xamarin.Mac] アウトラインビューを表示してみました

2020.11.21

1 はじめに

CX事業本部の平内(SIN)です。

Xamarin.Macを使用すると、C#でネイティブなMacのアプリが作成可能です。 ここでは、私自身がXamarin.Macに入門して学習した事項を覚書として書かせて頂いています。

今回は、アウトラインビューを確認してみました。

アウトラインビュー(NSOutlineView)は、テーブルビュー(NSTableView)のサブクラスで、殆ど同じですが、拡張としてアイテムを階層的に持つことが出来ます。そして、三角形のクリックで、それが折り畳めるようになっています。

2 OutlineViewの配置

Interface Builderで、メインとなるビューOutline Viewを配置し、ウインドウいっぱいに表示されるように制約を追加します。

また、AssistantエディタでViewController.hを開いてOutletを接続します。 ( 接続するコントロールがNSOutlineViewになっていることに注意が必要です。)

テーブルビューと同じで、各カラムのタイトルや、デフォルトの幅は、プロパティで設定できます。

3 データクラス

クラスPersonは、超簡単な個人の情報で、名前(Name)と年齢(Age)だけを保持します。

public class Person {

    public string Name { get; set; } = "";
    public int Age { get; set; } = 0;

    public Person(string name, int age) {
        this.Name = name;
        this.Age = age;
    }
}

4 DataSourceとDelegate

OutlineViewにデータを表示するには、DataSourceとDelegateの仕組みが必要です。

(1) DataSource

NSOutlineViewDataSourceを継承したクラス(OutlineViewDataSource)を追加し、先のデータ(Person)のリストを保持します。

また、NSOutlineViewDataSourceGetChild/GetChildrenCount/ItemExpandableをオーバーライドします。

GetChildでは、childIndex番目の要素、GetChildrenCountは、要素のデータ数、そして、ItemExpandableは、子要素が0個以上かどうかを返します。 ただ、それぞれは、変数itemがnullの場合、自身についての情報ですが、そうでな場合、itemの要素を意味します。

このため、それぞれ、下記のようなコードで、統一して書いてみました。

var persons = (item == null) ? Persons : ((Person)item).Persons;
public class OutlineViewDataSource : NSOutlineViewDataSource {

        public List<Person> Persons = new List<Person>();

        public OutlineViewDataSource() {
        }

        public override NSObject GetChild(NSOutlineView outlineView, nint childIndex, NSObject item) {
            var persons = (item == null) ? Persons : ((Person)item).Persons;
            return persons[(int)childIndex];
        }

        public override nint GetChildrenCount(NSOutlineView outlineView, NSObject item) {
            var persons = (item == null) ? Persons : ((Person)item).Persons;
            return persons.Count;
        }

        public override bool ItemExpandable(NSOutlineView outlineView, NSObject item) {
            var persons = (item == null) ? Persons : ((Person)item).Persons;
            return (persons.Count > 0);
        }
    }

(2) Delegate

NSOutlineViewDelegateを継承したクラス(OutlineViewDelegate)を追加し、変数でDataSourceを定義しコンストラクタで初期化します。

また、NSOutlineViewDelegateGetViewをオーバーライドし、セルの生成と値の設定を行います。

GetViewは、1つのセルを描画する度に呼ばれますが、返すデータ(何行目かのデータ)は、itemで渡され、タイトルでカラムを識別しています。

public class OutlineViewDelegate : NSOutlineViewDelegate {

    private string identifier = "cell";
    private OutlineViewDataSource DataSource;

    public OutlineViewDelegate(OutlineViewDataSource dataSource) {
        this.DataSource = dataSource;
    }

    public override NSView GetView(NSOutlineView outlineView, NSTableColumn tableColumn, NSObject item) {
        NSTextField view = (NSTextField)outlineView.MakeView(identifier, this);
        if(view == null) {
            view = new NSTextField();
            view.Identifier = identifier;
            view.BackgroundColor = NSColor.Clear;
            view.Bordered = false;
            view.Selectable = false;
            view.Editable = false;
        }
        var person = item as Person;

        switch(tableColumn.Title) {
            case "Name":
                view.StringValue = person.Name;
                break;
            case "Age":
                view.StringValue = person.Age.ToString();
                break;
        }

        return view;
    }
}

5 OutlineViewの初期化

ViewControllerAwakeFromNibをオーバーライドし、DataSourceの初期化と、コントロールへの紐付けを行います。

public partial class ViewController : NSViewController {

    // ・・・略・・・

    public override void AwakeFromNib() {
        base.AwakeFromNib();

        var DataSource = new OutlineViewDataSource();

        var group1 = new Person("Group1", 0);
        group1.Persons.Add(new Person("Saito", 20));
        group1.Persons.Add(new Person("Suzuki", 18));
        group1.Persons.Add(new Person("Yamada", 14));
        DataSource.Persons.Add(group1);

        var group2 = new Person("Group2", 0);
        group2.Persons.Add(new Person("Yamamoto", 21));
        group2.Persons.Add(new Person("Kisida", 18));
        DataSource.Persons.Add(group2);

        var group3 = new Person("Group3", 0);
        group3.Persons.Add(new Person("Tanaka", 21));
        group3.Persons.Add(new Person("Sasaki", 22));
        DataSource.Persons.Add(group3);

        OutlineView.DataSource = DataSource;
        OutlineView.Delegate = new OutlineViewDelegate(DataSource);

    }
}

ここまでの作業で、表示は以下のようになりました。

グルーピング行

Nameのカラムが、階層構造になったことで、やや違和感が出でしまったので、カラムのタイトル及び、Ageの表示を少し変更してみました。

タイトルNameは、Group / Nameに変更しました。

また、カラムAgeの表示は、階層下に要素がある場合、その平均値にしてみました。

public class OutlineViewDelegate : NSOutlineViewDelegate {
        // ・・・略・・・
        var person = item as Person;

        switch(tableColumn.Title) {
            case "Group / Name":
                view.StringValue = person.Name;
                break;
            case "Age":
                if(person.Persons.Count > 0) {
                    var ave = person.Persons.Average(i => i.Age);
                    view.StringValue =$"ave : {ave}";
                } else {
                    view.StringValue = person.Age.ToString();
                }
                break;
        }
        return view;
    }
}

上記の変更で表示されるビューは、以下のようになってます。

6 最後に

今回は、アウトラインビューについて確認してみました。

2次元データの表示でありながら、階層が表現できる、ちょっと面白いビューだと思います。 なお、1カラムだけのOutlineViewを使用すれば、Windowsで言うTreeViewが、表現できるということでしょうか。