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

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

概要 Examples ADC Samples 3rd Parties etc CBOriginals

SimpleBrowser

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

目次:
1 README.rtf の日本語訳
2 アプリケーション
3 ファイル構成
4 MainMenu.nib
5 AppController クラス
5.1 インターフェイス宣言
5.2 初期化
5.3 内容データ
6 FSNodeInfo クラス
6.1 インターフェイス宣言
6.2 作成と割り当て解除
6.3 子項目
6.4 各種情報の取得
7 AppController クラスふたたび
7.1 ブラウザの委任メソッド
7.2 クリック時の動作
8 FSBrowserCell クラス
8.1 継承とインスタンス変数
8.2 ノードからの属性付き文字列作成
8.3 継承メソッド
9 まとめ

1 README.rtf の日本語訳

SimpleBrowser は、Cocoa の NSBrowser クラスの使い方を示す小さなアプリケーションです。このアプリケーションは、表示メカニズムとして NSBrowser を使った、ファイルシステムブラウザを実装します。ブラウザは、ファイルシステムを移動させ、書類やアプリケーションを起動させたり開きます。このサンプルコードは、以下のことを実演します。

主なソースファイルと、それらについて何が興味深いかを以下に説明します。

AppController.m

AppController オブジェクトは fsBrowser オブジェクトの委任です。ブラウザの委任は、browser:willDisplayCell:atRow:column: と、browser:numberOfRowsInColumn: または browser:createRowsForColumn:inMatrix: のどちらかのメソッドを実装しなければなりません。この AppController は、browser:numberOfRowsInColumn: を実装し、したがって fsBrowser はそのデータを遅延読み込み (lazily load) します。ブラウザセル内のセルの読み込みにはディスクを操作することが含まれているので、ブラウザが遅延であるのは適切であるように思われます。

AppController は、ブラウザのシングル/ダブルクリックアクションを、それぞれ browserSingleClick:browserDoubleClick: に設定します。シングルクリックを受けとったとき、ブラウザは現在の選択範囲についての情報を表示します。ファイルに対するダブルクリックはそのファイルを開き、一方、アプリケーションに対するダブルクリックはプログラムの起動をもたらします。

FSBrowserCell.m

FSNodeInfo オブジェクトによって与えられたアイコンとファイル名の両方を表示する NSBrowserCell のサブクラスです。

FSNodeInfo.m

単一のファイルシステムノードについての情報、それがファイルまたはディレクトリのどちらかを維持管理します。特に、FSNodeInfo は、ファイルの絶対パス、最後のパス成分、読み込みアクセス権 (read accessiblity) などを返すことができます。

2 アプリケーション

アプリケーションを起動すると次のようになります。

図では、Xcode.app を選択した状態を示しています。Finder と似ていますが、ファイルシステムから情報を得ているため、パッケージの中身が丸見えになっています。標準のブラウザ機能を備えていることを確認してみてください。ダブルクリックすると Xcode が起動します。矢印キーで上下左右に動けます。シフトを押せば複数選択できます。ドラッグができないことに注意してください。

メニューには「Debugging(デバッグ)」という項目があり、その「Reload Browser Data(ブラウザのデータを再読み込み)」を選択すると、ブラウザのデータが再読み込みされます。このとき、ブラウザの選択範囲が最初の状態に戻ります。

3 ファイル構成

Xcode でプロジェクトを開くと、以下のようになっています。

Classes に、AppController.h と .m があります。このクラスは MainMenu.nib 内でインスタンス化されていて、NSBrowser インスタンスの委任にされています。アプリケーションの委任にはされていません。このクラスがアプリケーションの主要な動作を行います。

FSBrowserCell.h と .m は、NSBrowserCell のサブクラスを定義します。ブラウザのセルの独自の表示を担当します。

FSNodeInfo.h と .m は、は、ブラウザ内の項目情報を扱う NSObject のサブクラスを定義しています。

Resources 内には、Images というサブグループがあり、そのなかには画像が入れられています。MainMenu.nib は、このプロジェクトの主要 nib ファイルです。

4 MainMenu.nib

MainMenu.nib を開くと、以下のようになっています。

AppController クラスがインスタンス化されています。これを選択してアウトレットを表示させると、fsBrowser がウインドウ内のブラウザに、nodeIconWell が右上の画像ビューに、nodeInspector が右下のテキストフィールドにそれぞれ接続されています。

逆にブラウザを選択してみると、delegate(委任)が AppController に接続されています。ウインドウ内のどの項目もアクションはありません。ウインドウの initalFirstResponder がブラウザになっていることに気をつけてください。

メニュー内の項目は基本的に一次レスポンダに接続されていますが、1 つだけ「Debugging(デバッグ)」>「Reload Browser Data(ブラウザのデータを再読み込み)」は、AppController インスタンスの reloadData: に接続されています。

5 AppController クラス

5.1 インターフェイス宣言

AppController.h のインターフェイス宣言は以下のようになっています。

AppController.h
@interface AppController : NSObject { @private IBOutlet NSBrowser *fsBrowser; // ウインドウ内のブラウザ IBOutlet NSImageView *nodeIconWell; // 選択された項目のアイコンを示す画像ウェル IBOutlet NSTextField *nodeInspector; // 選択された項目の属性を表示するテキストフィールド } // 列ゼロ、したがってデータすべての再読み込みを強制 - (IBAction)reloadData:(id)sender; // ブラウザからここへとブラウザによって送られるメソッド - (IBAction)browserSingleClick:(id)sender; - (IBAction)browserDoubleClick:(id)sender; @end

インタンス変数は MainMenu.nib 内で接続されていたアウトレットだけです。メソッドのうち上のものは、メニュー項目に接続されていました。下の 2 つのメソッドは、次の初期化の説明で出てきます。

5.2 初期化

さて、AppController クラスは、MainMenu.nib でインスタンス化されているので、アプリケーションが起動し、nib ファイルが読み込まれた時にインスタンスが作られることになります。このとき、awakeFromNib が送られます。AppController ではこのメソッドを使って必要な初期化作業を行っています。このメソッドは以下のように実装されています。

AppController.m > awakeFromNib
- (void)awakeFromNib { // ブラウザが独自のブラウザセルを使うようにする [fsBrowser setCellClass: [FSBrowserCell class]]; // クリックされた時、ブラウザにメッセージを送るように告げる [fsBrowser setTarget: self]; [fsBrowser setAction: @selector(browserSingleClick:)]; [fsBrowser setDoubleAction: @selector(browserDoubleClick:)]; // 表示される列の数を設定(デフォルトの最大表示列は 1) [fsBrowser setMaxVisibleColumns: MAX_VISIBLE_COLUMNS]; [fsBrowser setMinColumnWidth: NSWidth([fsBrowser bounds])/(float)MAX_VISIBLE_COLUMNS]; // 最初に読み込まれたデータでブラウザを準備する [self reloadData: nil]; }

まず、ブラウザが独自のセルクラスを使うように設定しています。このクラスについては、後で説明します。

次に、クリックされた時のアクションを設定します。nib ファイルでブラウザにはターゲットもアクションも設定されていませんでした。これらはブラウザがシングルクリックされた時に送られるアクションメッセージです。さらにダブルクリック時のアクションも設定しています。

次に表示される最大列数を設定しています。MAX_VISIBLE_COLUMNS は、実装ファイルの最初で定義されている定数です。これはあくまで最大列数です。サンプルを起動すると最初に表示されているのが 3 列であることに注意してください。サイズ変更コントロールを引っぱって横に広げると、ブラウザが 4 列になります。それ以降は、どれほど引っぱっても 4 列のままです。さらに列の最小幅を設定しています。

最後に、ブラウザにデータの再読み込みをさせています。このクラスの reloadData: を呼び出しています。このメソッドは、以下のように実装されています。

AppController.m > reloadData:
- (IBAction)reloadData:(id)sender { [fsBrowser loadColumnZero]; }

このメソッドは、メニュー項目からも呼び出せるようにアクションメソッドの形になっています。NSBrowser のインスタンスメソッドを呼び出しているだけです。これは以前に読み込まれたデータを読み込み解除(作られたリソースなどを解放)して、新たにデータを読み込みます。

5.3 内容データ

さて、これでアプリケーションの初期化作業は終了し、あとはユーザーの操作を待っている状態になります。Cocoa に慣れていない方は、不思議に思ったかもしれません。起動した状態で、ウインドウ内のブラウザには、ディスク内容を示すデータが表示されています。このデータはどこで設定されていたのでしょう。

Application Kit 内でデータを表示するクラスのうち、多量のデータを表示するテーブルビューやアウトラインビュー、そしてブラウザなどのクラスは、データを明示的に設定しません。そのかわり、必要な時にデータを提供する別オブジェクトを用意するようになっています。テーブルビューでは、これはデータソースと呼ばれ、NSTableDataSource 簡易ブロトコルを実装しているオブジェクトが設定されます。ブラウザの場合、これは委任が担当します。

ブラウザの委任は、データを提供し、枝 (branch)(下にさらに項目がある)または葉 (leaf) (終端)ノード、有効または無効として各項目を設定します。スクロールのようなイベントを受けとったり、編集可能な場合の変更の検証も行えます。ブウラザの委任は、browser:willDisplayCell:atRow:column: と、browser:numberOfRowsInColumn: または browser:createRowsForColumn:inMatrix: のどちらかを実装しなければなりません。つまり、最低でも 2 つのメソッドは必ず実装していなければならないわけです。後の 2 つは両方実装できません。

browser:willDisplayCell:atRow:column: は、特定の行と列を表示される前に呼ばれます。このメソッド内でセルの文列値データを設定したり、それが枝か葉かなどを設定します。必須メソッドの後の 2 つのうち、browser:numberOfRowsInColumn: は特定の列内の行の数を返すものです。これを実装したブラウザの委任は受動的と呼ばれ、データ自体は、先の browser:willDisplayCell:atRow:column: で設定することになります。browser:createRowsForColumn:inMatrix: の方を実装したブラウザの委任は能動的と呼ばれます。このメソッドでは、列を表示するためのマトリックスが与えられるので、その列のなかに表示されるセルを配置します。その結果のマトリックスをブラウザがそのまま表示します。これによって、細かい表示の調整を行えるので、能動的と言われます。逆に受動的な委任は表示の詳細はブラウザクラスにまかせることになります。能動的な委任の場合、表示をすべて作ってしまうので、べつに browser:willDisplayCell:atRow:column: 内で何かを行う必要はありません。ただし、たとえ何もしなくても、このメソッドは実装しないと例外が発生してしまいます。

AppController は、browser:numberOfRowsInColumn: を実装しているので、受動的ということになります。よって列内の表示は基本的にブウラザにまかせることになります。実際、Finder 風の通常のリスト表示になっています。ただし、独自のセルを使っているため、セル内にアイコンが表示されるなど、単なる文字列ではありません。通常のセルを使っていれば、単に文字列値を表示したりという風になります。能動的なセルでは列内のマトリックス自体を修正可能なので、もっと違う形の表示も可能になるわけです。

さて、データの読み込みがどのように行われているかがわかったので、ここで委任メソッドを調べればいいのですが、このサンプルではファイル階層のデータを表示するようになっていて、そのためのノードクラスを定義しています。これを理解してからでないと、委任メソッドを見ても何のことがわからないので、まずこのデータを表すクラスを調べてみます。

6 FSNodeInfo クラス

6.1 インターフェイス宣言

このサンプルにおけるデータは、ファイルシステム上の項目です。ひとつひとつの項目は、FNNodeInfo クラスで表されています。まずこのクラスのデータ構造を見てみましょう。FSNodeInfo.h の一部を以下に示します。

FSNodeInfo.h > インターフェイスの一部
@interface FSNodeInfo : NSObject { @private NSString *relativePath; // 親に相対的なパス FSNodeInfo *parentNode; // これを収容するディレクトリ、保持・解放循環を避けるため保持されない } + (FSNodeInfo *)nodeWithParent:(FSNodeInfo*)parent atRelativePath:(NSString *)path; - (id)initWithParent:(FSNodeInfo*)parent atRelativePath:(NSString*)path; - (void)dealloc;

このクラスは 2 つのインスタンス変数を持っています。parentNode は親のノードで、このクラスのオブジェクトです。これにより、下から上へと参照の連鎖があることになります。相方向リストではなく、子の集まりがないことに注意してください。relativePath は相対パスですが、すぐ上の親からなのでたいていは名前だけになるでしょう。

このクラスはデータ構造でいう木構造を表しています。知らない方はデータ構造を説明している本またはサイトを見てください。Cocoa では直接の木構造コレクションはありませんが、AppKit サンプルの DragNDropOutlineView に単純な木構造クラスがあります。この実装はちょっと難があるのでそのまま使い続けるのはすすめられませんが…。あとは、Core Foundation に CFTree があるので、これが使えます。DragNDropOutlineView の実装では、インスタンス変数として子の配列を保持しています。このサンプルでは、それがありませんが、これはファイルシステムを調べれば、子がわかるので、その部分が省かれています。

6.2 作成と割り当て解除

作成メソッドは、自動解放オブジェクトを返すクラスメソッドと、初期化メソッドの 2 つが宣言されています。クラスメソッドのほうは、以下のように実装されています。

FSNodeInfo.m > +nodeWithParent:
+ (FSNodeInfo*)nodeWithParent:(FSNodeInfo*)parent atRelativePath:(NSString *)path { return [[[FSNodeInfo alloc] initWithParent:parent atRelativePath:path] autorelease]; }

見てわかるとおり、初期化メソッドが呼ばれて、返されたオブジェクトを自動解放して返しているだけです。実質的には、initWithParent: atRelativePath: がすべての初期化を担当していることになります。このメソッドは、以下のようになっています。

FSNodeInfo.m > initWithParent:
- (id)initWithParent:(FSNodeInfo*)parent atRelativePath:(NSString*)path { self = [super init]; // スーパークラスの初期化 if (self==nil) return nil; // 失敗なら nil を返す parentNode = parent; // 引数で渡された親を設定 relativePath = [path retain]; // 引数で渡されたパスを設定 return self; }

単に引数をインスタンス変数に設定しているだけです。parentNode は保持されず、relativePath は保持されていることに注意してください。したがって、dealloc が保持したものを解放するために定義されています。

FSNodeInfo.m > dealloc
- (void)dealloc { // parentNode は、保持しないので解放されない [relativePath release]; relativePath = nil; parentNode = nil; [super dealloc]; }

これで、このクラスがどういうデータを持っているかがわかったと思います。これだけでは意味がありません。このクラスは、他に、子ノードの配列を返すメソッドや、現在のインスタンスが表しているファイルシステム上の項目に対する情報を取得するメソッドを宣言し、定義しています。

6.3 子項目

まず子ノードの配列を返すメソッドを見てみます。

FSNodeInfo.m > subNodes
- (NSArray *)subNodes { NSString *subNodePath = nil; NSEnumerator *subNodePaths = [[[NSFileManager defaultManager] directoryContentsAtPath: [self absolutePath]] objectEnumerator]; // 共有ファイルマネージャーを取得し、パス項目の列挙子を取得 NSMutableArray *subNodes = [NSMutableArray array]; // 空の配列を準備 while ((subNodePath=[subNodePaths nextObject])) { // 項目があれば1つずつ処理 FSNodeInfo *node = [FSNodeInfo nodeWithParent:self atRelativePath: subNodePath]; // 自身を親にして、子項目を作成 [subNodes addObject: node]; // 配列に入れる } return subNodes; }

まず [NSFileManager defaultManager] で、共有ファイルマネージャーを取得しています。これはアプリケーションで用意されていて、いつでも取得できます。このメソッドは、まだ作成されていなければそれを作り、すでに作っていれば、それをそのまま返すので、プログラマが心配することはありません。そして、directoryContentsAtPath: により、引数パスにあるディレクトリの内容を取得しています。このクラスが所持しているのは相対パスなので、[self absolutePath] でまず自身の絶対パスを作っていることに注意してください。これは NSArray を返します。そこで、objectEnumerator を呼び出しています。このオブジェクトは、項目をひとつずつ取り出して繰り返しを行うときに使います。 そして項目ひとつずつ、自分を親にしてノードを作成し、それを配列に入れていきます。最後までくれば [subNodePaths nextObject]nil を返すので、subNodePathnil が代入され、while 文が終わります。

このクラスには、visibleSubNodes というメソッドもあります。これは、上と同様ですが、項目のノードを作成するときに、隠しファイルかどうかを調べて、隠しファイルは含めない点が違います。

6.4 各種情報の取得

このクラスは、情報取得のためのメソッドも宣言しています。fsType は、ディレクトリかどうかを示す文字列を返します。

FSNodeInfo.m > fsType
- (NSString*)fsType { if ([self isDirectory]) return @"Directory"; else return @"Non-Directory"; }

ここで使われている isDirectory は次のようになっています。

FSNodeInfo.m > isDirectory
- (BOOL)isDirectory { BOOL isDir = NO; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:[self absolutePath] isDirectory:&isDir]; return (exists && isDir); }

共有ファイルマネージャーを取得し、absolutePath で絶対パスを取得して、ファイルマネージャーにそこにあるファイルがディレクトリかどうかを問い合わせています。BOOLexists はファイルが存在しなければ NO が返るので、最初にこれとディレクトリかどうかを && 演算しています。よってこのメソッドは、ディレクトリであれば YES、ディレクトリでないか、または存在しなければ NO が返ることになります。

直前のメソッドや、子ノードの所でも、absolutePath が使われていました。このクラスは、インスタンス変数として相対パスだけを保持しているので、直接絶対パスが使えません。絶対パスはそのつど計算することになります。

FSNodeInfo.m > absolutePath
- (NSString*)absolutePath { NSString *result = relativePath; if(parentNode!=nil) { NSString *parentAbsPath = [parentNode absolutePath]; if ([parentAbsPath isEqualToString: @"/"]) parentAbsPath = @""; result = [NSString stringWithFormat: @"%@/%@", parentAbsPath, relativePath]; } return result; }

親ノードがなければ、自分の保持しているパスをそのまま返し、親があればその absolutePath を呼び出して、戻ってきた値をチェックして、「/」なら「」にしてます。そして親の絶対パスと自分のパスを結合しています。

isLink は、リンク項目かどうかを返します。

FSNodeInfo.m > isLink
- (BOOL)isLink { NSDictionary *fileAttributes = [[NSFileManager defaultManager] fileAttributesAtPath:[self absolutePath] traverseLink:NO]; return [[fileAttributes objectForKey:NSFileType] isEqualToString:NSFileTypeSymbolicLink]; }

fileAttributesAtPath: traverseLink: は、指定したパスにあるファイルの属性ディクショナリを返すメソッドです。traverseLink: 引数は、リンクを追跡するかどうかで、YES ならリンク対象の項目のデータが返ってきます。ここでは、NO ですが、これだとリンクファイルそのものの属性が返ってきます。そして返されたディクショナリから、ファイルタイプの値をとりだし、それがシンボリックリンクファイルかどうかを調べています。返されたディクショナリには、作成日やサイズなど他にも多量の情報が入っています。

isReadable は、ファイルが読み出し可能かどうかを返します。

FSNodeInfo.m > isReadable
- (BOOL)isReadable { return [[NSFileManager defaultManager] isReadableFileAtPath: [self absolutePath]]; }

単に共有ファイルマネージャーに問い合わせているだけです。

isVisible は、ファイルが不可視項目かどうかを返します。

FSNodeInfo.m > isVisible
- (BOOL)isVisible { NSString *lastPathComponent = [self lastPathComponent]; return ([lastPathComponent length] ? ([lastPathComponent characterAtIndex:0]!='.') : NO); }

これは単に名前の先頭が「.」で始まるかどうかを調べているだけです。

lastPathComponent は、最後のパス成分を返します。

FSNodeInfo.m > lastPathComponent
- (NSString*)lastPathComponent { return [relativePath lastPathComponent]; }

単に、インスタンス変数の相対パスの最後のパス成分を調べているだけです。

最後に、iconImageOfSize: は、この項目のアイコンを指定サイズで返します。

FSNodeInfo.m > iconImageOfSize:
- (NSImage*)iconImageOfSize:(NSSize)size { NSString *path = [self absolutePath]; NSImage *nodeImage = nil; nodeImage = [[NSWorkspace sharedWorkspace] iconForFile:path]; // アイコン取得 if (!nodeImage) { // ファイルそのものにはアイコンがない、拡張子をためす nodeImage = [[NSWorkspace sharedWorkspace] iconForFileType:[path pathExtension]]; // タイプに対するアイコン取得 } [nodeImage setSize: size]; // 画像のサイズを設定 if ([self isLink]) { // リンクファイルなら NSImage *arrowImage = [NSImage imageNamed: @"FSIconImage-LinkArrow"]; // リンク矢印取得 NSImage *nodeImageWithArrow = [[[NSImage alloc] initWithSize: size] autorelease]; // 新しい画像を作るため [arrowImage setScalesWhenResized: YES]; // サイズ変更時拡大縮小 [arrowImage setSize: size]; // 指定サイズにする [nodeImageWithArrow lockFocus]; // 画像内に描画 [nodeImage compositeToPoint:NSZeroPoint operation:NSCompositeCopy]; [arrowImage compositeToPoint:NSZeroPoint operation:NSCompositeSourceOver]; [nodeImageWithArrow unlockFocus]; nodeImage = nodeImageWithArrow; // 画像を入れ替え } if (nodeImage==nil) { //それでも画像がないなら nodeImage = [NSImage imageNamed:@"FSIconImage-Default"]; } return nodeImage; }

まずファイルのアイコンを取得します。NSWorkspace は、アプリケーションを起動したり、マウントを解除したりといったさまざまな雑用を行うクラスです。ファイルマネージャーの時と同様に、[NSWorkspace sharedWorkspace] で共有オブジェクトを取得して、それに対してメソッドを呼び出します。まず、それに対してファイルのアイコンを要求します。アイコンがないなら、そのタイプに対するアイコンを要求します。次に自身がリンクなら、リンクを表す矢印をアイコンの上に合成しています。途中、新しい空の画像を作成し、それに対して lockFocus し、そのなかに描画している所に注意してください。このように lockFocusunlockFocus の間に描画メソッドを入れれば、その画像に描画できます。

これで、FSNodeInfo クラスのすべてのメソッドを調べました。

7 AppController クラスふたたび

先ほどは、awakeFromNib メソッドと、reloadData: だけを見ました。残りのメソッドについて調べていきます。

7.1 ブラウザの委任メソッド

まず、browser:numberOfRowsInColumn: を見てみます。このメソッドは受動的な委任が実装するものです。列における行の数を返します。

AppController.m > browser:numberOfRowsInColumn:
- (int)browser:(NSBrowser *)sender numberOfRowsInColumn:(int)column { NSString *fsNodePath = nil; FSNodeInfo *fsNodeInfo = nil; // ブラウザの選択範囲によって表現される絶対パスを取得し、パスに対する FSNodeInfo を作成 // 列は(遅延的に)読み込まれた列を表すので、fsNodePath は最後に選択されたセルに対するパスとなる fsNodePath = [self fsPathToColumn: column]; fsNodeInfo = [FSNodeInfo nodeWithParent: nil atRelativePath: fsNodePath]; return [[fsNodeInfo visibleSubNodes] count]; }

まず、自身で定義している fsPathToColumn: を呼び出しています。このメソッドは以下のようになります。

AppController.m > fsPathToColumn:
- (NSString*)fsPathToColumn:(int)column { NSString *path = nil; if(column==0) path = [NSString stringWithFormat: @"/"]; else path = [fsBrowser pathToColumn: column]; return path; }

これは渡された列が 0(一番上または左)ならば、「/」を返し、そうでなければブラウザにその列までのパスを問い合わせて返しています。ここでいうパスはファイルシステムパスではなく、ブラウザのパスであることに注意してください。このクラスは、ブラウザパスをうまく利用してファイルシステムと一致させています。

browser:numberOfRowsInColumn: では、この戻り値のブラウザパスを相対パスとして、クラスメソッドを使って、新しい FSNodeInfo を自動解放オブジェクトとして作成しています。それから、そのオブジェクトに隠しファイルでない子項目の配列を作らせ、その数を返しています。

受動的な委任では、このメソッドで数を返すことによって、ブラウザが自動的に返された数を表示するためのマトリックスを作成して、表示が必要な項目に対して表示を要求することになります。能動的な委任では、列のマトリックスは委任が準備します。

各項目の表示に対して browser:willDisplayCell:atRow:column: が呼ばれます。

AppController.m > browser:willDisplayCell:atRow:column:
- (void)browser:(NSBrowser *)sender willDisplayCell:(id)cell atRow:(int)row column:(int)column { NSString *containingDirPath = nil; FSNodeInfo *containingDirNode = nil; FSNodeInfo *displayedCellNode = nil; NSArray *directoryContents = nil; // ブラウザの選択範囲で表現される絶対パスを取得し、そのパスに対する FSNodeInfo を作成 // (row, column) は表示されるセルを表現しているので、 // containingDirPath はそれを収容するディレクトリに対するパス containingDirPath = [self fsPathToColumn: column]; containingDirNode = [FSNodeInfo nodeWithParent: nil atRelativePath: containingDirPath]; // 表示されるセルに対する FSNodeInfo を取得できるよう、親に可視ノードのリストを要求 // セルが自身を表示する方法を決定できるよう、それからセルに FSNodeInfo を与える directoryContents = [containingDirNode visibleSubNodes]; displayedCellNode = [directoryContents objectAtIndex: row]; [cell setAttributedStringValueFromFSNodeInfo: displayedCellNode]; }

まず、そのセル項目の上位ディレクトリのパスを取得し、それを使って FSNodeInfo を得ています。そして、可視項目のリストを得て、その内容から番号でノードを取り出しています。最後に、セルのクラスとして設定したサンプル独自の FSBrowserCell のメソッド setAttributedStringValueFromFSNodeInfo: を呼び出して、その項目ノードから属性付き文字列値を作成させています。これはアイコン画像と属性付き文字列を生成させるためのものです。このクラスについては最後に説明します。

7.2 クリック時の動作

awakeFromNib で、このクラスは自身のインタンスをターゲットとして、シングルクリック、ダブルクリックのアクションを設定しました。ブラウサの項目がシングル・ダブルクリックされた時に、これらのメソッドが呼び出されることになります。まずシングルクリックです。

AppController.m > browserSingleClick:
- (IBAction)browserSingleClick:(id)browser { // 選択範囲を判定し、UI の右側にアイコンとインスペクタ情報を表示 NSImage *inspectorImage = nil; NSAttributedString *attributedString = nil; if ([[browser selectedCells] count]==1) { // 1項目だけ選択 NSString *nodePath = [browser path]; FSNodeInfo *fsNode = [FSNodeInfo nodeWithParent: nil atRelativePath: nodePath]; attributedString = [self attributedInspectorStringForFSNode: fsNode]; inspectorImage = [fsNode iconImageOfSize: NSMakeSize(128,128)]; } else if ([[browser selectedCells] count]>1) { // 複数選択 attributedString = [[NSAttributedString alloc] initWithString: @"Multiple Selection"]; } else { // 選択なし attributedString = [[NSAttributedString alloc] initWithString: @"No Selection"]; } [nodeInspector setAttributedStringValue: attributedString]; [nodeIconWell setImage: inspectorImage]; }

まずブラウザに選択されているセルの個数を問い合わせて、1 項目選択、複数選択、選択なしで処理を分けています。直接のメソッドではなく、NSArray を返してもらい、それに個数を問い合わせていることに注意してください。

1 項目選択の場合、まずブラウザに現在の選択範囲のパスを問い合わせます。次にそのパスを使って、FSNodeInfo を作成します。それからインスペクタに表示する文字列を作るメソッドをそのノードで呼び出しています。これは以下のようになります。

AppController.m > attributedInspectorStringForFSNode:
- (NSAttributedString*)attributedInspectorStringForFSNode:(FSNodeInfo*)fsnode { NSMutableAttributedString *attrString = [[[NSMutableAttributedString alloc] initWithString:@"Name: " attributes:[self boldFontAttributes]] autorelease]; [attrString appendAttributedString: [[[NSAttributedString alloc] initWithString:[NSString stringWithFormat: @"%@\n", [fsnode lastPathComponent]] attributes:[self normalFontAttributes]] autorelease]]; [attrString appendAttributedString: [[[NSAttributedString alloc] initWithString:@"Type: " attributes:[self boldFontAttributes]] autorelease]]; [attrString appendAttributedString: [[[NSAttributedString alloc] initWithString:[NSString stringWithFormat: @"%@\n", [fsnode fsType]] attributes:[self normalFontAttributes]] autorelease]]; return attrString; }

各行が長いですが、行われていることは単純です。まず最初に「Name:」とういうタイトルで属性付き文字列を作成します。この時、boldFontAttributes というこのクラスで定義されてるメソッドが呼ばれています。

AppController.m > boldFontAttributes
- (NSDictionary*)boldFontAttributes { return [NSDictionary dictionaryWithObject: [NSFont boldSystemFontOfSize:[NSFont systemFontSize]] forKey:NSFontAttributeName]; }

これはシステムフォントサイズの太字フォントをフォント名属性としてもつ、1 項目だけの属性ディクショナリを作り、それを返しています。これにより「Name:」は太字となります。これと対応するメソッドは normalFontAttributes です。

AppController.m > normalFontAttributes
- (NSDictionary*)normalFontAttributes { return [NSDictionary dictionaryWithObject: [NSFont systemFontOfSize:[NSFont systemFontSize]] forKey:NSFontAttributeName]; }

これはシステムフォントをフォント名属性としてもつ、1 項目だけの属性ディクショナリを作って返します。これら 2 つのメソッドの意味がわかれば、attributedInspectorStringForFSNode: の残りも簡単に理解できます。単にフォントを変えながら、名前として最後のパス成分、「Type:」というタイトル、ファイルタイプを属性付き文字列に追加しているだけです。改行を追加するために、stringWithFormat: で改行が最後にくる書式文字列中にわざわざ文字列を入れているのに注意してください。書式文字列中の「%@」はオブジェクトを表します。

属性付き文字列を作成したら、次は FSNodeInfo クラスのメソッド iconImageOfSize: を使って、指定サイズのアイコン画像を作成させます。

複数選択の場合、@"Multiple Selection"、選択なしの場合、@"No Selection" という文字列を作成しているだけです。ここで、先ほどの属性付き文字列を作成するメソッド内では、autorelease されていたのに対して、この 2 つがされていないことに注意してください。これはこのサンプルのバグです。このままだと、複数選択、選択なしが起こるたびに、メモリ漏れが発生することになってしまいます。ビルドして実行を何度も行いたい人は、この 2 つを autorelease してやってください。このようにサンプルといえども完全でなく、特にメモリ関連のバグがときおり残っています。また、通常不必要なことを説明のためにあえてやっている場合もあるので、コピーして使うときは、十分注意してください。

つぎは、ダブルクリック時の動作を見てみます。

AppController.m > browserDoubleClick:
- (IBAction)browserDoubleClick:(id)browser { // ファイルを開き、単一クリックルーチンを呼び出すことで情報表示 NSString *nodePath = [browser path]; [self browserSingleClick: browser]; [[NSWorkspace sharedWorkspace] openFile: nodePath]; }

ブラウザから現在の選択範囲のパスを取得し、シングルクリック時のルーチンを呼び出し、それに情報を表示させます。それから NSWorkspace を使って、そこにあるファイルを開かせています。ディレクトタの場合、単にそれが Finder で表示されます。

これで AppController クラスについては、完全に理解できたはずです。最後に、ブラウザのセルとして設定されクラスを調べます。

8 FSBrowserCell クラス

8.1 継承とインスタンス変数

このクラスのインターフェイス宣言の一部は以下のようになります。

FSBrowserCell.h > 継承とインスタンス変数
@interface FSBrowserCell : NSBrowserCell { @private NSImage *iconImage; }

NSBrowserCell のサブクラスであり、アイコン画像である NSImage を保持していることがわかります。@private は、このクラス以外で使われたくないインスタンス変数に使います。Objective-C 言語において、このほかに、@public@protected があり、これらは、他から使えるもの、そのクラスとサブクラスでのみ使えるもの、を意味します。デフォルト(無指定)は @protected になっています。オブジェクト指向のカプセル化の観点からすると、@public は使うべきではなく、@private にしてアクセサを実装し、サブクラスもアクセサ経由にするというのが良いようですが、サブクラス内で変数が触れないのも不便なことが多いので、結果的に @protected が多用されることになります。

8.2 ノードからの属性付き文字列作成

AppController において、セルの各項目を表示する前に呼ばれる委任メソッド browser:willDisplayCell:atRow:column: において、このクラスの setAttributedStringValueFromFSNodeInfo: が呼び出されていました。まず、これから見ていきます。

FSBrowserCell.m > setAttributedStringValueFromFSNodeInfo:
- (void)setAttributedStringValueFromFSNodeInfo:(FSNodeInfo*)node { // 特定の FSNodeInfo が与えられ、表示プロパティをセットアップ NSString *stringValue = [node lastPathComponent]; // テキスト部分をセットアップする。FSNodeInfo はファイルの // さまざまなプロパティにもとづいて文字列を書式付ける(下線、太字など)ことになる [self setAttributedStringValue: [[[NSAttributedString alloc] initWithString:stringValue attributes:[FSBrowserCell stringAttributesForNode:node]] autorelease]]; // 画像部分を設定。FSNodeInfo は、与えられたファイル・ディレクトリに対して // 使われる正しいアイコンを探す方法がわかっている [self setIconImage: [node iconImageOfSize:NSMakeSize(ICON_SIZE,ICON_SIZE)]]; // ファイルにアクセスできないなら、ユーザーがそれを選択できないことを確実にする [self setEnabled: [node isReadable]]; // セルが子を持っているかどうかを知っていることを確実にする [self setLeaf:![node isDirectory]]; }

まず与えられたノードの最後のパス成分から文字列値を設定しています。次に、継承している NSCell のメソッド setAttributedStringValue: で属性付き文字列を設定しています。stringAttributesForNode: はこのクラスで定義されているもので、

FSBrowserCell.m > +stringAttributesForNode:
+ (NSDictionary*)stringAttributesForNode:(FSNodeInfo*)node { NSMutableDictionary *attrs = [NSMutableDictionary dictionary]; [attrs setObject: [NSFont systemFontOfSize:[NSFont systemFontSize]] forKey:NSFontAttributeName]; // 単におもしろいから、リンクであるテキストに下線を引く if ([node isLink]) [attrs setObject:[NSNumber numberWithInt:NSSingleUnderlineStyle] forKey:NSUnderlineStyleAttributeName]; return attrs; }

まず、空の変更可能ディクショナリを作成し、それにフォント名を設定します。それだけではつまらないので、わざわざリンクの場合は下線をつけています。これは必要なこと、というより、さらに例を示すためです。

さて、setAttributedStringValueFromFSNodeInfo: に戻ると、属性付き文字列を設定した後で、アイコンを設定しています。setIconImage: もこのクラスで定義されているメソッドで、

FSBrowserCell.m > +stringAttributesForNode:
- (void)setIconImage: (NSImage *)image { [iconImage autorelease]; iconImage = [image copy]; // 画像が望むサイズで表示されることを確実にする [iconImage setSize: NSMakeSize(ICON_SIZE,ICON_SIZE)]; }

まず元にあった画像を自動解放にして、それからコピーしています。その後で、画像サイズを設定しています。

さて、setAttributedStringValueFromFSNodeInfo: で、アイコン画像をインスタンス変数に収めた後は、読みとり可能な場合だけ有効になるようにしています。それから、ノードの葉を設定しています。これはノードクラスで初期化時に子が設定されず、子がなかった場合、それを設定することも行われていないため、これを呼び出すことで同時にそれも設定されるようにしています。これで、このメソッドは終わりです。基本的に表示が行われる前に表示に必要なデータを準備しているメソッドであることがわかったと思います。

8.3 継承メソッド

他のメソッドは、基本的に継承メソッドです。全体的な説明については『Cocoa のためのコントロールとセルプログラミングトピック』の「NSCell のサブクラスの作成」その他の記事を見てください。ここで触れられているdrawInteriorWithFrame:inView: がこのクラスで実装されています。これがブラウザのセルの内部を実際に描いています。

FSBrowserCell.m > +stringAttributesForNode:
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { if (iconImage != nil) { // アイコン画像があるなら NSSize imageSize = [iconImage size]; NSRect imageFrame, highlightRect, textFrame; // セルを2つの部分(画像部分(左)とテキスト部分)に分割 NSDivideRect(cellFrame, &imageFrame, &textFrame, ICON_INSET_HORIZ + ICON_TEXT_SPACING + imageSize.width, NSMinXEdge); imageFrame.origin.x += ICON_INSET_HORIZ; // 先頭からアイコンまでの空間 imageFrame.size = imageSize; // フレームサイズをアイコンサイズに // 合成の時、オンラインのドキュメントが「画像は対象座標に関係なく、 // 基準座標系の方向を持っていることになっています」と述べているように、 // 反転されたコントロール内にあるかもしれないことを考慮して、画像フレームの一番上を調整する if ([controlView isFlipped]) imageFrame.origin.y += ceil((textFrame.size.height + imageFrame.size.height) / 2); else imageFrame.origin.y += ceil((textFrame.size.height - imageFrame.size.height) / 2); // 現在の状態に従って、強調することになる色を設定する if ([self isHighlighted]) { // 強調されるべきなら // NSBrowserCell は、一番右の列以外のすべてのセルを少し薄くするので // [NSColor selectedControlColor] のかわりに highlightColorInView を使う // highlightColorInView からの戻り値は、適切なものを返すことになる [[self highlightColorInView: controlView] set]; } else { // 強調されなくていいなら [[NSColor controlBackgroundColor] set]; } // 強調を描画、しかし、下の [super drawInteriorWithFrame:...] に対する呼び出しで // とらえられないことになる部分だけ。同じ部分を2回描画する必要はない。 highlightRect = NSMakeRect(NSMinX(cellFrame), NSMinY(cellFrame), NSWidth(cellFrame) - NSWidth(textFrame), NSHeight(cellFrame)); NSRectFill(highlightRect); // 画像を合成 [iconImage compositeToPoint:imageFrame.origin operation:NSCompositeSourceOver]; // テキスト部分はどうするかわかっているので、NSBrowser が行ってくれる // わざわざそれが行ってくれるものを再発明する必要はない [super drawInteriorWithFrame:textFrame inView:controlView]; } else { // アイコン画像がないなら // アイコンを見つけられない場合、少なくとも何かを描く。もっと賢いことをしたいかもしれない [super drawInteriorWithFrame:cellFrame inView:controlView]; } }

ちょっとややこしそうですが、順を追えば簡単に理解できます。最初に画像サイズをローカル変数 imageSize に入れています。次に渡されたセルのフレームを2つに分割しています。先頭からアイコンまでの空間+アイコンサイズ+アイコンからテキストまでのサイズをもつ長方形と、そこから最大xまでの2つに分割しています。それから、左側の長方形のサイズをアイコンサイズに変えています。この長方形はアイコンを 描くときに使うので、アイコンサイズぴったりにしています。

つぎに、コントロールが反転されているかどうか判らないため、それにしたがって調整を行っています。反転の場合は、アイコンを描き始める場所まで原点を動かしています。この時、テキスト部分との高さを足したり引いて割っているのは、テキスト長方形の高さとアイコンサイズの高さが同じではないからで、空間を上下で同じにして、テキストの中央にアイコンが描かれるようにしています。

つぎに強調される場合の背景色を描いています。これは選択している時に青色で表示されているものです。ただし、サンプルのブラウザでは、一番左にあるもの以外は、灰色で強調されることになります。この色を決定しているのが、NSBrowserCell で定義されている highlightColorInView: メソッドです。ブラウザでは同様な動作をすることがあるので、このメソッドで適切な色を返すようになっています。つぎにその色を設定して、それで背景を描画しています。それからその上にアイコン画像を合成しています。

テキスト部分に関しては、ブラウザセルのメソッドにまかせています。以前に setAttributedStringValueFromFSNodeInfo: 内で属性付きの文字列が設定されているので、これが適切に動作します。

残りのメソッドは、継承メソッドのオーバーライドです。branchImagehighlightedBranchImage はクラスメソッドで、それぞれ通常と強調状態の枝項目の画像を返します。デフォルトは右方向の三角形です。このサンプルでは枝項目の前に三角形を表示させたくないので nil を返しています。

cellSizeForBounds:NSCell から継承されているメソッドで、渡された長方形に最大サイズを限定して、セルの内容を表示するために必要な最低限の長方形を返します。

FSBrowserCell.m > cellSizeForBounds:
- (NSSize)cellSizeForBounds:(NSRect)aRect { // アイコンをおさめる追加空間を与えるため、通常より少し高いセルにする NSSize theSize = [super cellSizeForBounds:aRect]; theSize.width += [[self iconImage] size].width + ICON_INSET_HORIZ + ICON_INSET_HORIZ; theSize.height = ICON_SIZE + ICON_INSET_VERT * 2.0; return theSize; }

このクラスでアイコンを扱っているため、上位クラスやブラウザはアイコンが表示されていることについて関知していません。そのため、属性付き文字列だけを表示するのに必要な空間を設定します。このクラスでは、アイコンを表示するため、単に文字の高さだけにもとづいてサイズを設定するわけにはいきません。ここでは、テキストだけで計算された長方形に対して、アイコンの幅とアイコンの高さにもとづいてサイズを再設定しています。

これで、このクラスのメソッドについて理解できたと思います。

9 まとめ

簡単なように見えて奥が深いサンプルです。特に独自描画を行うセルについての参考資料となっています。ブラウザ以外のセルについて、そのまま応用できるものではないですが、独自セルがどのように実装されているか、少しわかると思います。

このサンプルでは、受動的なブラウザを使用していることに注意してください。能動的なブウラザ委任では、さらに列内のマトリック全体を変えることもできます。また、セルのサブクラスの独自描画では、drawInteriorWithFrame:inView: ではなく、drawWithFrame:inView: も使えることに気をつけてください。


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