|
- Application Kit-
NSDocument
Application Kit - NSDocument
ドキュメント・ベース・アプリケーションとは
Keywords: document-based
Cocoa では、ドキュメント・ベース・アプリケーションっていうタイプのアプリケーションを標準で提供しているんだ。読んで字の如く、ドキュメントを取り扱うアプリケーションのテンプレートとなるものだ。ドキュメント・ベース・アプリケーションでは、複数のドキュメントを取り扱うことができるんだ。
具体的にどういうことができるかっていうと、標準的なメニューのうち、ドキュメントの操作に関わるものがすでに実装されているんだ。細かく言うと、
新規ドキュメントの作成(New)
ドキュメントを開く(Open...)
ドキュメントを保存する(Save, Save As...)
ドキュメントを復帰する(Revert)
ドキュメントを閉じる(Close)
ドキュメントを印刷する(Page Setup..., Print...)
などがある。これなら、アプリケーションに必要とされるものはほとんどあるじゃないか。
あと、ドキュメントが編集されたときのダーティ・フラグを立てるとか、ドキュメントを表示するウィンドウにタイトルを付けるとか、っていう機能もあるんだ。
もちろん、ドキュメント・ベース・アプリケーションを使わなくても、ドキュメントを取り扱うアプリケーションを作ることはできるよ。例えば、現段階では Example としてついてくる TextEdit は、ドキュメント・ベース・アプリケーションではないんだ。Cocoa が提供している機能では足りなかったり、特殊なことをやりたいときは、使わなくてもいいと思う。でも、どうせだったら、準備されているものを使った方が楽だと思うよ。
Application Kit - NSDocument
ドキュメント・ベース・アプリケーションの構成
Keywords: NSDocumentController, NSDocument, NSWindowController
ドキュメント・ベース・アプリケーションは、3 つのクラスをメイトして構成されているんだ。NSDocumentController、NSDocument、NSWindowController だ。
まず、ドキュメント・ベース・アプリケーションのために必要な前提から調べてみよう。Cocoa では、1 つのドキュメントは 1 つのファイルと結び付けられているんだ。1 ファイルが 1 ドキュメント。ドキュメントにはいろんな種類がある。たとえば、テキストドキュメントとか、HTML ドキュメントとかね。Cocoa では、ドキュメントの種類は、基本的に拡張子、補助的にファイルタイプを使って区別するんだ(賛否いろいろあるけど)。そして、ドキュメントを開くと、ウィンドウの中に表示される。このとき、1 つのドキュメントに対して、複数のウィンドウが開いてもいいんだ。1 ドキュメントに複数ウィンドウ。
以上の前提を踏まえて、ドキュメント・ベース・アプリケーションの構成を上から見てみよう。まず一番上は NSDocumentController。こいつは、ドキュメント管理の親玉だ。ドキュメントを開いたり、複数のドキュメントを管理したりする。こいつはアプリケーションごとに 1 つのインスタンスしかないんだ(アプリケーション側で作る)。
で、アプリケーションが新しいドキュメントを作ろうとしたり、ファイルを開けたりしようとすると、NSDocumentController が NSDocument を作る。NSDocument は、1 つのドキュメントにつき、1 つのインスタンスが作られるんだ。ドキュメントの中身のデータは、こいつが持つことになる。
ドキュメントが作られたら、ウィンドウを使って表示してやる必要があるよね。それを管理するのが NSWindowController だ。NSDocument は、必要なだけ NSWindowController を作って、ドキュメントの中のデータを渡して表示させてやるんだ。
3 つのクラスの関係は、こんな感じだ。
Application Kit - NSDocument
ファイルと NSDocument を関連づける
Keywords: Document Types
ドキュメント・ベース・アプリケーションでは、NSDocumentController がファイルを開いて、NSDocument のインスタンスを作るんだ。じゃあ、どうやってファイルと NSDocument を関連づけているんだ?
それは、アプリケーションの Info.plist の中で定義されているんだ。Info.plist の CFBundleDocumentTypes の中にその情報が書いてある。ここでは、ドキュメントの種類を定義している。ドキュメントの種類は、拡張子とファイルタイプの 2 つの情報で定義するんだ。(ただ、どっちの情報を優先的に見るかは、まだ明確な指針はなかったと思う)。あと、ドキュメントの種類には名前を付けることができて、NSDocument のクラスと結び付けられている。
Project Builder を使って編集する場合は、“Target”を選択して“Application Setting”のタグを選ぶ。その中の“Document Types”のところだ。

ここで、このアプリケーションが開くドキュメントの種類を追加するんだ。GUI のフィールドに、上から順に、
ドキュメントの種類の名前
拡張子
ファイルタイプ
アイコンファイル
NSDocument のクラスの名前
を入れていく。
ここの情報をもとにして、NSDocumentController は開くドキュメントを決める。「開く...」を呼んだときに、アクティブに表示されるファイルは、ここで関連付けられているファイルだ。
Application Kit - NSDocument
NSDocumentController を使ってすべてのファイルを開く
Keywords: Extension, OS Type
ここでは、ドキュメント・ベース・アプリケーションを使って、すべてのドキュメントを開くことを考えてみよう。そのアプリケーションが作ったドキュメントだけじゃなくて、他のアプリケーションが作ったやつも開ける、っていう場合ね。これが、ちょっとめんどくさい。
上で見た通り、NSDocumentController は、拡張子とファイルタイプを使って、開くべきファイルを決定しているんだ。だから、その 2 つを使って、すべてのドキュメントを定義してやらないといけない。問題は、現段階では、Mac OS 9 から Mac OS X への以降期で、2 つの OS のファイルが混在していること。さらに、ファイルタイプの扱いにまだ迷いがみられることなんだ。ここでは、ドキュメントタイプを以下の 4 つに分類してみよう。
拡張子とファイルタイプを指定する
拡張子だけを指定する
ファイルタイプだけを指定する
それ以外のファイル
1. 2. 3. の場合のやり方は分かると思う。それぞれ必要なものを指定しておけばいいんだ。では、4. はどうする?4. の場合は、拡張子に "*"、ファイルタイプに "" を指定することによって対応できるんだ。ファイルタイプを空にするんじゃなくて、空文字を指定するのがポイント。例は、下のようになる。

これで、一見いいように見えるけど、実は問題があるんだ。4. のための UnknownDocumentType を指定してしまうと、3. のタイプが開けなくなってしまうんだ。上の例では TextDocumentType が UnknowDocumentType に変わってしまう。これはなぜかというと、おそらく、NSDocumentController は、拡張子を優先してドキュメントの種類の判別を行っているんだ。だから、"*" を指定してしまうと、そっちに奪い取られてしまう。
これを防ぐにはどうすればいいのか?強引だけど、NSDocumentController を継承して、ドキュメントタイプをすげかえてしまう、っていうことをやってみた。makeDocumentWithContentsOfFile:ofType: をオーバーライドするんだ。
MyDocumentController.m (sample)
- (id)makeDocumentWithContentsOfFile:(NSString*)fileName
ofType:(NSString*)docType
{
if ([docType isEqualToString:@"UnknownDocumentType"]) {
// HFS タイプコードを取得する
NSFileManager* fm = [NSFileManager defaultManager];
NSDictionary* attr = [fm fileAttributesAtPath:fileName
traverseLink:YES];
NSNumber* HFSTypeCode = [attr
objectForKey:@"NSFileHFSTypeCode"];
if (HFSTypeCode) {
// HFS タイプ名を取得する
NSString* HFSTypeName = NSFileTypeForHFSTypeCode(
[HFSTypeCode unsignedLongValue]);
// Info.plist のドキュメントタイプを取得する
NSDictionary* infoDict =
[[NSBundle mainBundle] infoDictionary];
NSArray* docTypes = [infoDict
objectForKey:@"CFBundleDocumentTypes"];
int i;
for (i = 0; i < [docTypes count]; i++) {
NSDictionary* docType = [docTypes objectAtIndex:i];
// タイプ名と OS タイプ名を取得する
NSString* typeName = [docType
objectForKey:@"CFBundleTypeName"];
NSArray* OSTypeNames = [docType
objectForKey:@"CFBundleTypeOSTypes"];
if (OSTypeNames) {
int j;
for (j = 0; j < [OSTypeNames count]; j++) {
NSString* OSTypeName = [OSTypeNames objectAtIndex:j];
// クォートを追加する
OSTypeName = [NSString stringWithFormat:@"'%@'",
OSTypeName];
if ([OSTypeName isEqualToString:HFSTypeName]) {
// 親クラスを、置き換えられたタイプ名で呼び出す
return [super makeDocumentWithContentsOfFile:fileName
ofType:typeName];
}
}
}
}
}
}
return [super makeDocumentWithContentsOfFile:fileName
ofType:docType];
}
このメソッドに渡ってくるドキュメントタイプが UnknownDocumentType だったら、他のドキュメントタイプを隠してしまっている場合があるんだ。だから、そのファイルの OS タイプを取得して、Info.plist の内容と見比べて、ドキュメントタイプをつけてやる、っていう処理をしてみた。こういう処理は、フレームワーク側でやって欲しいよな。
これで、どうにかすべてのファイルを開くことができたよ。
(この記事を書くにあたって、Yosiki さんに助言をいただきました。ありがとうございます)
■サンプルダウンロード:
OpenDocument.tar.gz
Application Kit - NSDocument
NSDocument でファイルを開く 3 つの方法
Keywords: loadDateRepresentation, readFromFile, loadFileWrpper
ドキュメント・ベース・アプリケーションを利用すると、ファイルを開くところまでは、フレームワークがやってくれる。じゃ、その後は?というわけで、開いたファイルを取り扱う方法だ。3 つあるぜ!
1 つ目は、ファイルの中身を NSData として取り出した形で受け取る方法。loadDataRepresentation:ofType: をオーバーライドする。
Application Kit/NSDocument.h
- (BOOL)loadDataRepresentation:(NSData *)docData
ofType:(NSString *)docType;
引数に付いてくるのはドキュメントタイプだ。問題なく開けたら、YES を返す。
2 つ目は、選択したファイルのパスを受け取る方法。readFromFile:ofType: をオーバーライドしてくれ。
Application Kit/NSDocument.h
- (BOOL)readFromFile:(NSString *)fileName
ofType:(NSString *)docType;
ファイルパスだけが渡ってくるので、そこからファイルを好きなようにオープンしてくれ。これをオーバーライドしてると、上の loadDataRepresentation:ofType: は、自動的には呼び出されなくなるよ。
3 つ目は、RTFD やアプリケーションみたいな、ファイルラッパーを開けるとき。loadFileWrapperRepresentation:ofType: を使おう。
Application Kit/NSDocument.h
- (BOOL)loadFileWrapperRepresentation:(NSFileWrapper *)wrapper
ofType:(NSString *)docType;
この 3 つを必要に応じて使い分ければ、ローカルのファイルは開けるぜ!単純なドキュメントの時は loadDataRepresentation:ofType:、ファイルを開くときに、エンコーディングとか気をつけなくちゃいけないときは readFromFile:ofType: ってとこかな。
Application Kit - NSDocument
テキストファイルを開く
Keywords: oepn document
じゃ、いよいよ実際のアプリケーションだ。ドキュメント・ベース・アプリケーションを使って、テキストファイルを開いてみよう。
まず、クラスの構成から。ここでは、TextDocument と TextController っていう 2 つのクラスを使う。それぞれ NSDocument と NSWindowController を継承しているよ。TextController の方は、NSTextView への参照を持っているんだ。こいつにテキストを表示させる。

TextDocument でやらなくてはいけないことは以下の通り。
ドキュメントの中身であるテキストを保持する。あと、それへのアクセッサ
TextController のインスタンスを作る
開かれたファイルからテキストを取り出して、セットする
テキストを保持するために、インスタンス変数 _string を持つ。
TextDocument.h (sample)
@interface TextDocument : NSDocument
{
NSString* _string;
...
}
それへのアクセッサは、setString: と string ね。
TextDocument.h (sample)
- (void)setString:(NSString*)string;
- (NSString*)string;
次に、TextController のインスタンスを作ろう。それには makeWindowControllers をオーバーライドする。これはドキュメントを開くときに、勝手に呼び出されるんだ。
TextDocument.m (sample)
- (void)makeWindowControllers
{
TextController* ctrl = [[[TextController alloc]
initWithWindowNibName:@"TextDocument"] autorelease];
[self "addWindowController:ctrl];
}
addWindowController: を呼ぶと、TextController が、ウィンドウコントローラの 1 つとして、登録されるんだ。
そして、開かれたファイルの取り扱い。ここでは readFromFile:ofType: を使う。ファイルから、自分でテキストを取り出すことになる。
TextDocument.m (sample)
- (BOOL)readFromFile:(NSString*)fileName ofType:(NSString*)type
{
NSDictionary* attr;
NSAttributedString* attrStr;
attrStr= [[NSAttributedString alloc]
initWithPath:fileName documentAttributes:&attr];
[self setString:[attrStr string]];
// エンコードを取得する
_encoding = [[attr objectForKey:@"CharacterEncoding"]
intValue];
return YES;
}
NSAttributedString を使って、適切にエンコードされたテキストを取り出すんだ。setString: を使って、それをセットしておく。これでドキュメント側は、おしまい。
次は TextController 側。TextController のメインの仕事は TextView のハンドルだ。ドキュメントとうまく同期をとって、TextView にテキストをセットする必要がある。そのために、syncWithDocument というメソッドを作ってみた。このメソッドは、ドキュメントからテキストを取り出して、TextView にセットするためのメソッドだ。
TextController.m (sample)
- (void)syncWithDocument
{
TextDocument* doc = [self document];
// ドキュメントの文字列を設定する
if (doc) {
[self setString:[doc string]];
}
}
setString: の中で、テキストをセットする。これを、windowDidLoad で呼んでやるんだ。windowDidLoad の時点では、すでにドキュメントは開かれてる。これで TextView にテキストが設定されるぜ!
■サンプルダウンロード:
OpenTextDocument.tar.gz
Application Kit - NSDocument
NSDocument でファイルを保存する 3 つの方法
Keywords: dataRepresentationOfType, writeToFile, fileWrapperRepresentationOfType
では、続いて保存側を。こちらも 3 つだ!さっきのやつと、対になってるよ。好きなメソッドをオーバーライドして使おう。
1 つ目は、dataRepresentationOfType:。NSData 型のデータを返すタイプだ。
Application Kit/NSDocument.h
- (NSData *)dataRepresentationOfType:(NSString *)aType;
引数に渡ってくるのは、ドキュメントタイプ。タイプに応じたデータを NSData 型で渡してやれば、ファイルに保存してくれるぞ。
2 つ目は、ファイルパス形式。ファイルのパスが引数として渡ってくる、writeToFile:ofType: だ。
Application Kit/NSDocument.h
- (BOOL)writeToFile:(NSString *)fileName ofType:(NSString *)type;
このメソッドでは、指定されたファイルに自分でデータを書き込んでやらないといけない。これを使うと、上の dataRepresentationOfType: は呼び出されなくなるからね。
そして 3 つ目は fileWrapperRepresentationOfType: だ。ファイルラッパーに対応する必要があるときに使おう。
Application Kit/NSDocument.h
- (NSFileWrapper *)fileWrapperRepresentationOfType:(NSString *)aType;
自分でファイルラッパーを作って返してやってね。
これで、保存側も終了。
Application Kit - NSDocument
テキストファイルを保存する
Keywords: save document
ファイルを開いたら保存しないといけないわけで、保存の仕方の話だ。上で作ったサンプルの続きの話だよ。
上でアプリケーションの骨格はすでに説明されているので、ここでは保存するところだけにしぼって話をしよう。
保存するときには、アプリケーションはどういう流れになるのか?このサンプルでは、TextView を使ってテキストファイルを表示、編集しているんだ。だから、編集したとしても、その結果がすぐに TextDocument に反映されるわけではない。保存する前に、TextView の編集結果を TextDocument に移してやらないといけないんだ。
そのために、TextController に updateDocument というメソッドを用意してやる。このメソッドの仕事は、コントローラの内容をドキュメントに反映してやる、っていうことだ。
TextController.m (sample)
- (void)updateDocument
{
TextDocument* doc = [self document];
// ドキュメントに文字列を設定する
[doc setString:[self string]];
}
}
続いて、TextDocument 側の話。TextDocument ではドキュメントを保存するために dataRepresentationOfType: をオーバーライドしてるんだ。メニューから Save が選択されると、このメソッドが呼び出される。このメソッドやることは、まず最初にウィンドウコントローラの間と同期をとる。そのために syncWithController っていうメソッドを呼ぶんだ。実装は下の通り。
TextDocument.m (sample)
- (void)syncWithController
{
// updateDocument を呼び出す
[[self windowControllers]
makeObjectsPerformSelector:@selector(updateDocument)];
}
各ウィンドウコントローラの updateDocument メソッドを呼んでやるんだ。そうしてドキュメントがすべての編集を反映したら、NSData を作る。エンコードを指定してね。
TextDocument.m (sample)
- (NSData*)dataRepresentationOfType:(NSString*)type
{
[self syncWithController];
if (!_string) {
return nil;
}
// エンコーディングを指定して文字列を作る
return [_string dataUsingEncoding:_encoding];
}
これでテキストドキュメントの保存もできた。超簡易テキストエディタの出来上がりだ。
■サンプルダウンロード:
OpenTextDocument.tar.gz
|