Cocoa Break Logo Top Soft Develop  脱力空間 Logo
Apple Web Badge
made by mi

概要 翻訳 ソース リンク がらくた

概要 Examples ADC Samples 3rd Parties etc CBOriginals

OutlineView

以下は、Mac OS X 10.4.10 上の Xcode 2.5 で説明しています。

目次:
1 アプリケーション
2 ファイル構成
3 MainMenu.nib
4 DataSource クラス
4.1 インターフェイス
4.2 データソースとしてのメソッド
4.3 委任としてのメソッド
5 FileSystemItem クラス
5.1 インターフェイス
5.2 初期化
5.3 他のメソッド
6 まとめ

1 アプリケーション

アプリケ−ションを起動すると、次のようなウインドウが表示されます。

最初は、ルートディレクトリが「/」によって示されます。左向きの矢印は、その項目にサブ項目があることを示しています。矢印をクリックして項目を開くと、次のような表示に変わます。

このサンプルは、アウトラインビューを使って、コンピュータにつながっている記憶装置やサーバーのディレクトリをブラウズできます。Finder のリスト表示のように表示できます。

2 ファイル構成

プロジェクトを構成しているファイルを見てみましょう。プロジェクトファイルは次のようになっています。

DataSource.h と .m がありますが、ここで実装されているクラスは NSObject のサブクラスで、アウトラインビューに表示されるデータを供給するデータソースです。また FileSystemItem.h と .m がありますが、ここで実装されているクラスは NSObject のサブクラスで、DataSource クラスから呼び出され、ファイルマネージャとのやりとりを担当します。

AppIcon.icns という画像がありますが、これはアプリケーションのアイコンです。Info-OutlineView.plist 内でアプリケーションのアイコンファイルとして指定されています。MainMenu.nib には、ユーザーインターフェイス等の定義があります。

3 MainMenu.nib

MainMenu.nib は次のようになっています。

MainMenu.nib の MyWindow のなかに、OutlineView があります。これはスクロールビュー内に入れられているので注意してください。インスペクタパネルで見てわかるように、一度クリックしただけではスクロールビューが選択されます。アウトラインビューの部分をダブルクリックすると、アウトラインビューが選択されます。テーブルビューやアウトラインビューのような構造の複雑なインスタンスの各部を選択するには、nib ウインドウ自体をアウトラインモードで表示されるのも効率的です。nib ウインドウの右上のほうにあるボタンで切り替えます。また、DataSource クラスのインスタンスがありますが、これが NSOutlineView の dataSource、delegate(委任)に接続されています。これを設定することで、データの供給を要求するメソッドと、委任メソッドが自動的に送られます。。アウトラインビューでのすべての操作は、この2つの接続によって実行されています。

4 DataSource クラス

4.1 インターフェイス

まず、DataSource クラスについて調べてみます。その名のとおり、これは NSOutlineViewdataSource として働くように作られています。DataSource.h では、NSObject のサブクラスであること以外、インスタンス変数やメソッドが一切定義されていません。。

DataSource.h > @interface 宣言
@interface DataSource : NSObject @end

メソッドも宣言されていませんが、これは nib ファイル内でデータソースや委任に指定されたため、必要なときに各メソッドが呼び出されるからです。

4.2 データソースとしてのメソッド

DataSource.m では、メソッドが定義されています。定義されているのは、outlineView: numberOfChildrenOfItem:outlineView: isItemExpandable:outlineView: child:(int)index ofItem:outlineView: objectValueForTableColumn: byItem:outlineView: shouldEditTableColumn: item: です。最初の 4 つがデータソースとしてのメソッドで、最後の1つがアウトラインビューの委任メソッドです。

アウトラインビューのデータソースが満たすべき動作は、NSOutlineViewDataSource というプロコトルで宣言されています。このプロトコルには、ここで定義されている以外にもメソッドがありますが、最低限、ここで定義されている 4 つは実装しなければなりません。

まずある項目の子の数を知らせるメソッドとして、outlineView: numberOfChildrenOfItem: があります。

DataSource.m > outlineView: numberOfChildrenOfItem:
- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item { return (item == nil) ? 1 : [item numberOfChildren]; }

ここでは、itemnil であれば(つまり一番上の項目であるルートであれば)1 を、そうでなければ、itemnumberOfChildren メソッドを送っています。このメソッドは、FileSystemItem クラスで定義されています。このクラスについては、後で説明します。項目の子の項目の数を返すメソッドです。

次にある項目が開けるかどうかを返すメソッドとして、outlineView: isItemExpandable: があります。

DataSource.m > outlineView: isItemExpandable:
- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item { return (item == nil) ? YES : ([item numberOfChildren] != -1); }

これは上のメソッドとほとんど同じで、子の数が -1 でなければ YES を返すようになっています。

次に、ある項目の子のうち特定の番号のものを返すメソッドとして、outlineView: child: ofItem: があります。

DataSource.m > outlineView: child: ofItem:
- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item { return (item == nil) ? [FileSystemItem rootItem] : [item childAtIndex:index]; }

ここでは、項目がルート項目なら、FileSystemItemrootItem メソッド、そうでなければ、childAtIndex: index: メソッドを送って、返ってきたものをそのまま返しています。

最後に、指定された項目のデータを返すメソッドとして、outlineView: objectValueForTableColumn: byItem: があります。

DataSource.m > outlineView: objectValueForTableColumn: byItem:
- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item { return (item == nil) ? @"/" : (id)[item relativePath]; }

ここでは、ルート項目であれば、「/」という文字列を、そうでなければ、FileSystemItem クラスの relativePath メッセージを送り、その結果を返しています。

4.3 委任としてのメソッド

DataSource はアウトラインビューの委任としても設定されています。アウトラインビューの委任メソッドは、たくさんあります。項目を閉じたり開いたり、選択されたり、セルを表示するときや、列の移動やリサイズが行われたとき、列が編集されるとき、テーブルコラムがマウス操作されたときなどにメッセージが送られます。

このサンプルでは、セルが編集できるかどうかを指定する、次の委任メソッドを実装しています。

DataSource.m > outlineView: shouldEditTableColumn: item:
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item { return NO; }

これはつねに NO を返します。すなわち、このアウトラインビューのセルは一切編集できません。

5 FileSystemItem クラス

5.1 インターフェイス

FileSystemItem は、NSObject のサブクラスとして、以下のように定義されています。

FileSystemItem.h > @interface 宣言
@interface FileSystemItem : NSObject { NSString *relativePath; // 相対パス FileSystemItem *parent; // 親項目 NSMutableArray *children; // 子項目 } + (FileSystemItem *)rootItem; - (int)numberOfChildren; // 葉ノードに対しては -1 を返す - (FileSystemItem *)childAtIndex:(int)n; // 葉ノードに対する呼び出しを無効とする - (NSString *)fullPath; - (NSString *)relativePath; @end

このクラスは、アウトラインビューで使われる項目1つ1つを表します。インスタンス変数は 3 つあります。relativePath はパス文字列を保存します。parent は親となる項目を保存します。children は子のリストを保存しておく可変配列です。また、次の静的変数とマクロが定義されています。

FileSystem.m > 定数とマクロ
static FileSystemItem *rootItem = nil; // ルート項目定数 #define IsALeafNode ((id)-1) // 葉ノードを示す

5.2 初期化

初期化メソッドは、ヘッダに宣言されておらず、内部的に利用されます。これは、次のようになります。

FileSystemItem.m > initWithPath: parent:
- (id)initWithPath:(NSString *)path parent:(FileSystemItem *)obj { if (self = [super init]) { // スーパークラスの初期化 relativePath = [[path lastPathComponent] copy]; // 渡されたパスの最後の成分をコピー parent = obj; // 親項目を設定 } return self; }

初期化は、NSString で与えられるパス文字列と、親のオブジェクトによって行なわれます。インスタンス変数の relativePath には、パス文字列の最後の成分がコピーされます。これはコピーメソッドの暗黙の了解として保持されていることに注意してください。そして、parent に親オブジェクトを格納しています。今度は単に参照を格納しているだけで、保持 (retain) はしていないことに注意してください。あとで見るように、子配列の中の子は配列によって保持されるので、親が子を保持するものの、子は親を保持しないようにして、保持の循環が発生するのを防いでいます。ここで children は初期化されていませんが、これは子のオブジェクトが与えられたときに保存され、維持されることになります。また、上で説明した保持オブジェクトの 解放を dealloc メソッドで行っています。

FileSystemItem.m > dealloc
- (void)dealloc { if (children != IsALeafNode) [children release]; // 子項目があれば解放 [relativePath release]; // 相対パスを解放 [super dealloc]; // スーパークラスの実装 }

5.3 他のメソッド

このクラスには、クラスメソッドとして、rootItem が定義されています。

FileSystemItem.m > rootItem
+ (FileSystemItem *)rootItem { if (rootItem == nil) // まだ作られていないなら rootItem = [[FileSystemItem alloc] initWithPath:@"/" parent:nil]; // ルート項目を作成 return rootItem; // 今回、または以前に作成されたルート項目を返す }

これは、単純に静的変数である rootItem を返します。rootItemnil だった(まだ一度も参照されていない)場合、rootItem を初期化しています。これは、アプリケーション内で単一のインスタンスであることが保証されている必要があるオブジェクト(シングルトン)を作成するときに使われるパターンです。AppKit における共有オブジェクトも、クラスオブジェクトによって返され、たいていこのような形で実装されていると思われます。

さて、このクラスが所持しているパス名は最後の成分だけなので、そのため、パスを扱うメソッドが2種類定義されています。

FileSystemItem.m > relativePath・fullPath
- (NSString *)relativePath { return relativePath; } - (NSString *)fullPath { return parent ? [[parent fullPath] stringByAppendingPathComponent:relativePath] : relativePath; }

relativePath は単純に自分の持っているパス名の成分を返します。通常、これはファイル名やディレクトリ名です。fullPath は、3 項 if 演算子で親があるかどうかを調べ、あるなら親に自分のパス名の前にパスを加えてもらいます。ないなら、自身の相対パスを返します。これは、ルート項目に到達するまで再帰的に呼び出されることになるので、結果としてルート項目からのパス、つまり絶対パス(または完全パス)が返ることになります。

次に、子の項目を返す children ですが、このメソッドが呼び出されたとき、ファイルマネージャーを利用して、子の項目が作られます。

FileSystemItem.m > children
- (NSArray *)children { if (children == NULL) { NSFileManager *fileManager = [NSFileManager defaultManager]; // ファイルマネージャーを取得 NSString *fullPath = [self fullPath]; // 自身の完全パスを計算させて設定 BOOL isDir, valid = [fileManager fileExistsAtPath:fullPath isDirectory:&isDir]; // 1 if (valid && isDir) { // 存在してディレクトリなら NSArray *array = [fileManager directoryContentsAtPath:fullPath]; // 内容項目を取得 int cnt, numChildren = [array count]; // 内容項目の数 children = [[NSMutableArray alloc] initWithCapacity:numChildren]; // 保持用変更可能配列を作成 for (cnt = 0; cnt < numChildren; cnt++) { // 内容項目数だけ繰り返し [children addObject:[[FileSystemItem alloc] initWithPath:[array objectAtIndex:cnt] parent:self]]; // 子項目用オブジェクトを作成し、それを配列に追加する } } else { // 存在しないなら children = IsALeafNode; // 子項目なし=葉ノードにする } } return children; }

子の項目があった場合、それを返します。そうでなければ、デフォルトのファイルマネージャーを取得して、自分の完全パスを指定して、それがディレクトリであるかどうかを問い合わせています。1 で問い合わせが行われています。戻り値は存在するかどうかで、ディレクトリかどうかは isDir のなかに間接的に(変数のアドレスを渡し、メソッド終了後にそこに値が入れられる)返されます。

存在して、かつディレクトリなら、ファイルマネージャーを利用して、ディレクトリ内の項目を NSArray として取得したあと、NSMutableArray にそれらのファイルに対応する FileSystemItem クラスを作成して格納しています。これで、子のリストが作成されます。ディレクトリでない場合、children には IsALeafNode(つまり -1)が格納される。これで children を調べるだけで、葉ノードかどうかがわかります。

子の項目に関するメソッドは、上記の children のほかに、次の2つのメソッドが定義されています。

FileSystemItem.m > childAtIndex:・numberOfChildren
- (FileSystemItem *)childAtIndex:(int)n { return [[self children] objectAtIndex:n]; } - (int)numberOfChildren { id tmp = [self children]; return (tmp == IsALeafNode) ? (-1) : [tmp count]; }

childAtIndex は、特定の番号位置にある項目を返します。このメソッドは、NSMutableArray であるインスタンス変数 children に、objectAtIndex: メッセージを送って結果を返しているだけです。

numberOfChildren も同様に子があれば、children に対して、NSMutableArraycount メッセージを送っているだけです。

6 まとめ

Cocoa に慣れない人にとっては、最初はテーブルビューやアウトラインビューにどのようにデータを表示するか疑問に思うかもしれません。テーブルビュー内で、そのようなメソッドが定義されていたほうがいいように思いますが、このようにデータの供給を切り離すことで、それを切り替えたり、さまざまな可能性に対応しやすくなっています。また、このおかげでバインディング等に対する新しい技術の導入も、それほど違和感なく行えます。

各メソッドの説明をしましたが、どう動くかを知りたい人は、データソースメソッドや委任メソッドなどにブレークポイントを設定するなどして、デバッガで動かしてアウトラインビューを操作してみてください。どういうタイミングで呼ばれているのかがわかるはずです。


管理人:神吉 秀典 E-mail:puer@ba.wakwak.com