NSDocument

NSDocument

Application Kit - NSDocument

ドキュメント・ベース・アプリケーションとは

Keywords: document-based

Cocoa では、ドキュメント・ベース・アプリケーションっていうタイプのアプリケーションを標準で提供しているんだ。読んで字の如く、ドキュメントを取り扱うアプリケーションのテンプレートとなるものだ。ドキュメント・ベース・アプリケーションでは、複数のドキュメントを取り扱うことができるんだ。

具体的にどういうことができるかっていうと、標準的なメニューのうち、ドキュメントの操作に関わるものがすでに実装されているんだ。細かく言うと、

・新規ドキュメントの作成(New)

・ドキュメントを開く(Open...)

・ドキュメントを保存する(Save, Save As...)

・ドキュメントを復帰する(Revert)

・ドキュメントを閉じる(Close)

・ドキュメントを印刷する(Page Setup..., Print...)

などがある。これなら、アプリケーションに必要とされるものはほとんどあるじゃないか。

あと、ドキュメントが編集されたときのダーティ・フラグを立てるとか、ドキュメントを表示するウィンドウにタイトルを付けるとか、っていう機能もあるんだ。

もちろん、ドキュメント・ベース・アプリケーションを使わなくても、ドキュメントを取り扱うアプリケーションを作ることはできるよ。例えば、現段階では Example としてついてくる TextEdit は、ドキュメント・ベース・アプリケーションではないんだ。Cocoa が提供している機能では足りなかったり、特殊なことをやりたいときは、使わなくてもいいと思う。でも、どうせだったら、準備されているものを使った方が楽だと思うよ。





Application Kit - NSDocument

ファイルと NSDocument を関連づける

Keywords: Document Types

ドキュメント・ベース・アプリケーションは、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

ドキュメント・ベース・アプリケーションの構成

Keywords: NSDocumentController, NSDocument, NSWindowController

ドキュメント・ベース・アプリケーションでは、NSDocumentController がファイルを開いて、NSDocument のインスタンスを作るんだ。じゃあ、どうやってファイルと NSDocument を関連づけているんだ?

それは、アプリケーションの Info.plist の中で定義されているんだ。Info.plist の CFBundleDocumentTypes の中にその情報が書いてある。ここでは、ドキュメントの種類を定義している。ドキュメントの種類は、拡張子とファイルタイプの 2 つの情報で定義するんだ。(ただ、どっちの情報を優先的に見るかは、まだ明確な指針はなかったと思う)。あと、ドキュメントの種類には名前を付けることができて、NSDocument のクラスと結び付けられている。

Project Builder を使って編集する場合は、“Target”を選択して“Application Setting”のタグを選ぶ。その中の“Document Types”のところだ。

documentTypes.jpg

ここで、このアプリケーションが開くドキュメントの種類を追加するんだ。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. の場合は、拡張子に "*"、ファイルタイプに "" を指定することによって対応できるんだ。ファイルタイプを空にするんじゃなくて、空文字を指定するのがポイント。例は、下のようになる。

allDocuments.jpg

これで、一見いいように見えるけど、実は問題があるんだ。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





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 への参照を持っているんだ。こいつにテキストを表示させる。

classStructure.gif

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





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


Application Kit