NSOutlineView と item

多層構造のデータ表示の仕方がわからない、と言っていたが、試行錯誤の末、できた。

Person – Child – GChild という階層を持つデータがあったとき、適切に実装すると以下のように表示が可能だ。

ポイントは、 item をうまく使うこと。

具体的には

-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
{
    if(item){
        if([item isKindOfClass:[Child class]] ){
            Child *c = item;
            return [c.gchildren count];
        }
        if([item isKindOfClass:[Person class]] ){
            NSLog(@"this item is Person");
            return [[item children] count];
        }
        return 0;
    }else{
        return [self.objects count];
    }
}

などとする。

直感的には item = 行(インスタンス1つ)という理解でいいと思う。
トップ階層(item が定義できない)は、self.objects count などとしているわけですね。

参考

・ここでの質疑応答

item という概念がわからないというある人の問題提起に対して林さんが

私の場合ですが、実データを入れるクラスとOutlineのアイテムを別々のクラスにして、アイテムの方のクラスのプロパティに実データのクラスのインスタンスを入れるように実装したことがあります。アウトラインに表示する時にものによってそれに対応するデータのクラスが異なっていたので、アウトラインビューのデータソースのアイテムは表示のためのものと割り切りました。

と応えている。

これは考えてみれば当たり前の話で、NSOutlineView からしてみれば、どんなデータが要素になるかわからない。
だから、そこら辺はコーダーの方で処理してくれよ、という話になる。

・YouTube の動画

動画は流れを掴むのにちょうどいい。

31:53 TableColumn の identifier の設定。
36:30 NSOutluneView と管理クラスの結び付け(datasource 経由

あたりは「百聞は一見にしかず」でしょう。

・OsiriX/Horos/HorliX における NSOutlineView

調査中。

・NSTreeView を使う

この記事で NSTreeView を使っていた。
手が空いたら試す。

・公式解説

 

CoreData と NSOutlineView

CoreData と NSTableView に関して、いくつか書いたが、これらは練習。

NSTableView では entity 同士が relationship で繋がれたような階層的なデータは扱えないので、NSOutlineView というものが用意されている。

NSTableView の時と同様、Cell Based と View Based があり、挙動が違うようなんだが、イマイチ理解してない。
どちらが作成しやすいかといえば Cell Based 。

なお、デフォは View Based になっているようなので、NSOutlineView を貼り付けたら、右インスペクタを操作して Cell Based に設定しておく。

NSOutlineView

いろいろ調べたんだが、芯食った解説(特に Objective-C & MacOS 環境)に巡り合わなかったので、いきなり CoreDtata との連携を考えるのではなく NSOutlineView 単独で適当なデータを表示させる方向でいく。ここは慎重に。

で、作成。

クラス Person と NSMutableArray<Person *> *people を用意して *people を表示させたい。

階層がある場合には、セルクリックで展開できる。

まずは、ポイントを解説。

階層があるので、セルを表示するのに row と column では役不足。
そこで item という概念が登場する。
実態としては、この場合は *people を反映したなにかなんだが、多分、最初はなんのことかわからないと思う。

先に進もう。

NSOutlineDatasource プロトコルを適用した NSOutlineView を管理するクラスを作成する。
プロトコルを満たすために最低4つのメソッドを実装する必要がある。
なお、データを表示するだけなら、NSOutlineViewDatasource プロトコルを満たすだけでいい。delegate までは必要ない

4つのメソッドは

-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item;

-(BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item;

-(id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item;

-(id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item;

で、例えば、最初の outlineView: numberOfChildrenOfItem: は

-(NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item{
    //return !item ? [self.people count] :[[item children] count];
    if(item){
        return [[item children] count];
    }else{
        return [self.people count];
    }
}

となる。

DataSource と item

まだ、公式ドキュメントで確認などはしていないのだが、動作から考えるに

・DataSource としてリンクさせたクラスのコレクションがテーブルで表示させる範囲

・コレクションに含まれるクラスが item

なのかなと推測しているし、例外は経験したことがない。

上の場合で言えば、NSMutableArray<Person *> *people 自体は要素ではないから、item ではない。

推測する、とぼかしたのは、コレクションすべてを試したわけではないから。
今のところ、取り扱いが簡単な NSMutableArray で試してうまく表示できたが、他の、例えば NSDictionary などは一切試していない。というか試したが、見事にコケた。書き方が悪いのか、それとも、仕様上そうなってないのかは不明。
チャレンジされた方がいたら、教えて欲しい。

また、「コレクションに含まれるクラスが item」とは書いたが、他のクラスを Person の子要素に含んでいても問題なく動く。

異なるクラスを 親-子-孫 の関係にした場合

「他のクラスを Person の子要素に含んでいても問題なく動く」とは書いたが、Person の子要素として Child クラスを設定し、さらにその子要素として GChild を設定した場合は、ダメっぽい。

        NSString *className = NSStringFromClass([item class]);
        NSLog(@"ClassName is: %@", className);

を適当な位置に入れたが、返されるのは Person のみ。
しかし、NSStringFromClass([item class]) という書き方は便利ですね。item のところにクラスがよくわからないインスタンスに置き換えれば、そのクラス名を返してくれます。

おそらく DataSorce =  NSMutableArray<Person *> *people としたため、item のクラスとしては Person 以外取りようがないからだと思う。

しかし、DataSorce =  NSMutableArray<NSObject *> *object と一般的にすると item は任意のクラスを取りうる。しかし、このやり方で3階層以上を実装するのは頭こんがらがりそう。

まあ、大雑把に表示はできたんですが。→自己解決。別記事にします。

NSOutlineView と CoreData を結びつける

結びつけること自体は、それほど難しくないと思う。

ただ、 One To Many の関係にある relationship の Many の側はことごとく NSSet になってしまうので、いまいち使いづらい。
いい方法ないだろうか?(→ NSSet 公式ページ

クラスを生成した「後に」属性値を追加する

普通にできるっぽい。
.xcdatamodeled に attr を追加。該当するクラスに @property …. を追加する。

これができないと、ちょっとした変更をするたびに、クラスを生成する必要があるので、当然か。

実用的には、クラスと relationship をとりあえず決めておいて、細部は後で編集する、という使い方でいいかもしれない。

 

(続く)


気が早いかもしれないが、CoreData でデータも作成しておく。

いつものように Person – Child という親子関係の Entity を作成し、永続化させると sqlite には以下のように保存される。


対応するコードは以下の通り。


    Child *child = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
                                                    inManagedObjectContext:context];
    child.name = @"taro";
    
    Child *child2 = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
                                                   inManagedObjectContext:context];
    child2.name = @"kana";
     
    Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                    inManagedObjectContext:context];
     
    person.name = @"akiba";
     
 
    NSSet *childs = [NSSet setWithObjects:child, child2, nil];
     //[person addChildrenObject:child];
    [person addChildren:childs];

Child のインスタンスを一度に複数登録する場合には、NSSet を使うので、そのように変更した。

 


アップル公式の参考情報

Xcode で NSOutlineView を使った際に右インスペクタに表示される参照情報。
気になったのでまとめた。

Delegate and Data Sources

NSOutlineViewDataSource

Navigating Hierarchical Data Using Outline and Split Views
サンプル(↓)もあるのだが、立派すぎて初学者には参考にならないかな。

akiba

CoreData と NSTableView

CoreData に関する記事はいくつか書いてきたんだが、実用的には NSTableView とリンクして使いたい。

なのだが、cocoa & Objective-C 環境で作成されたサンプルがないんだな、これが(笑)。

UITableView になってしまうが iOS & Swift なら結構あるんですけどね。
まあ、ないと iPhone アプリでデータのテーブル表示できないでしょうから、当然でしょうけど。

NSTableView 単独になってしまうけど、かろうじて見つけたのが、これ

おそるおそる現環境(MacOS Ventura, Xcode 14)でビルド。

おお、修正なしで一発動作。よしよし。

解説記事も充実している(態度豹変)。

 

このサンプルをベースに CoreData のデータと NSTableView のセルを結びつける。

結果的には、まあまあできた。

ポイントは、NSTableDataSource プロトコルと NSTableViewDelegate プロトコルに必須の二つのメソッドを実装するところでしょうか。

こういうとき「動く」サンプルは便利だと思うのだが、サンプルを少々訂正するだけでいいし、だから、その意味を掴みやすい。

ワイはこんな風に実装した。


// NSTableViewDataSource Protocol Method

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
    //return self.numbers.count;
    return self.personname.count;
}

// NSTableViewDelegate Protocol Method

-(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
    NSString *identifier = tableColumn.identifier;
    NSTableCellView *cell = [tableView makeViewWithIdentifier:identifier owner:self];
    
    if ([identifier isEqualToString:@"personname"]) {
        //cell.textField.stringValue = [self.numbers objectAtIndex:row];
        cell.textField.stringValue = [self.personname objectAtIndex:row];
    } else {
        cell.textField.stringValue = [self.personage objectAtIndex:row];
    }
    
    return cell;
}

元コードではセル値の供給元になっている numbers は NSArray として定義されていたが、これだと可変値は扱いにくい。データを追加・削除しやすいように NSMutableArray に変更した。

これはなかなか勉強になった。

最終的には、sqlite のデータが書き変わったら、テーブルのセル値も追従して変えてくれるようにしたいんだが、現状だとちょっと難しいかな。(よくわからない人は「NSTableView update」あたりで検索かけましょう。ただ、これ、オブジェクト間通信を使えばいいような? 設計的に美しくないし、可読性落ちるけどさ→やはり通知でできましたとさ)

 

 

cocoa Core Graphics 関係

MacOS の新しい画像ライブラリは Metal だが、2次元に限定されてしまうが、使いやすいのは Core Graphics だろう。

なのだが、情報が乏しいのなんのって。

iOS 系のサンプルや公式ドキュメントを参考に MacOS 向けの簡単なサンプルコードを書いてみた。

簡単な描画サンプル


- (void)drawRect:(CGRect)dirtyRect {
    
    //[super drawRect:dirtyRect];//不要のようだ
     NSGraphicsContext* nsgc = [NSGraphicsContext currentContext];
     CGContextRef context = [nsgc CGContext];
    
     CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
    double col[4],col2[4];
     col[0] = 1.0;col[1] = 1.0;col[2] = 0.5;col[3] = 1.0;
    col2[0] = 1.0;col2[1] = 0.0;col2[2] = 0.0;col2[3] = 0.8;
     CGColorRef color = CGColorCreate(space,col);
    CGColorRef color2 = CGColorCreate(space,col2);
     //透明レイヤー開始
     //CGContextBeginTransparencyLayer(context, nil);

     //パスの描画を開始
     CGContextBeginPath(context);
     float startAngle = -M_PI/2;
     float endAngle = startAngle + (M_PI * 2.0) * 0.5;
     CGContextMoveToPoint(context, 100, 100);
     CGContextAddArc(context, 100, 100, 50.0f, startAngle, endAngle, clockwise);
     CGContextClosePath(context);
     CGContextSetFillColorWithColor(context, color);
     CGContextDrawPath(context, kCGPathFill);
    
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, 0, 0);
    //CGContextSetStrokeColor(context, col2);
    CGContextSetLineWidth(context,2.0);
    CGContextAddLineToPoint(context, 100, 100);
    CGContextAddLineToPoint(context, 150, 50);
    CGContextDrawPath(context, kCGPathStroke);

     //透明レイヤー終了
     //CGContextEndTransparencyLayer(context);
    
    CGColorRelease(color);

}

Metal に比べると実に簡単。
点を打ってラインで繋ぎ・・・というふうに本当に直感的。
これでアイキャッチのような図形が描画できる。

なお、cocoa 系の Core Graphics の座標系は以下の通り。

clockwise は 1 と定義してます。

Core Graphics が Quartz???

ところで、Core Graphics は Quartz の枠組みを踏襲しているようだが、その実装は CPU レンダリング主体、少なくとも OpenGL 絡みは使っていないはずで、今後も安心して使えると思うのだが、そこらへん、曖昧に説明しているサイトがあったりして「は?」となっている。

追記:ソースコードはこちらの CoreGraphics 特集?に収載されてます。

Core Graphics + Metal

Core Graphics のお手軽さと Metal の速さを組み合わせて両方を活かして使いたいという欲求はあるようで、以下のような記事があった。

Combine the power of CoreGraphics and Metal by sharing resource memory 

サンプルが動画になっている。

見事。