Geometry 関数
NSBundle
NSDictionary
NSFileManager
NSMutableAttributedString
NSNumber
NSScanner
NSString
NSTimer
Graphic 関数
NSApplication
NSBezierPath
NSCell
NSColor
NSCursor
NSCustomImageRep
NSDocument
-ドキュメント・ベース・アプリケーションとは
-ドキュメント・ベース・アプリケーションの構成
-ファイルと NSDocument を関連づける
-NSDocumentController を使ってすべてのファイルを開く
-NSDocument でファイルを開く 3 つの方法
-テキストファイルを開く
-NSDocument でファイルを保存する 3 つの方法
-テキストファイルを保存する
NSDocumentController
NSDragging
NSEvent
NSFontManager
NSGraphicsContext
NSImage
NSMenu
NSOutlineView
NSPanel
NSPopUpButton
NSResponder
NSScrollView
NSString 追加
NSTableColumn
NSTableView
NSTextStorage
NSTextView
NSToolbar
NSView
NSWindow
NSWorkspace
その他
.nib ウィンドウ
Views パレット
クラス
インスタンス変数
メソッド
CFXML
Carbon Event
Carbon Graphics
Cocoa で日本語
メソッド
その他

- 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”のところだ。

documentTypes

ここで、このアプリケーションが開くドキュメントの種類を追加するんだ。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. それ以外のファイル

1. 2. 3. の場合のやり方は分かると思う。それぞれ必要なものを指定しておけばいいんだ。では、4. はどうする?4. の場合は、拡張子に "*"、ファイルタイプに "" を指定することによって対応できるんだ。ファイルタイプを空にするんじゃなくて、空文字を指定するのがポイント。例は、下のようになる。

all documents

これで、一見いいように見えるけど、実は問題があるんだ。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 への参照を持っているんだ。こいつにテキストを表示させる。

class structure

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



[Home] [Download] [Archives] [BBS] [Cocoa Programming Tips 1001] [Core Foundation の秘密] [Safari Developer Center] [はじめてのブラウザのつくり方] [Sketch BP] [スクリーンセイバーを作ろう] [Objective-C 最適化] [Authorization API 完全理解] [Mac OS X Programming Books Review] [オブジェクト指向の言語比較論] [panther-dev]

mailto: mkino@xd5.so-net.ne.jp