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

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

概要 Examples ADC Samples 3rd Parties etc CBOriginals

SimpleComboBox

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

目次:
1 README.rtf の日本語訳
2 アプリケーション
3 ファイル構成
4 MainMenu.nib
5 CDInfoDocument.nib
6 CDInfoDocument クラス
7 まとめ

1 README.rtf の日本語訳

SimpleComboBox は、Cocoa の NSComboBox の使い方を示す小さなドキュメントベースのアプリケーションです。アプリケーションは、音楽 CD についての情報をユーザーが編集して保存できるようにします。他の事柄とともに、ドキュメントはユーザーが音楽のジャンルルを指定できるようにするコンボボックスを含んでいます。このサンプルコードは、以下のことを実演します。

以下で、主要なソースファイルと何が興味深いかを示します。

CDInfoDocument.m

CDInfoDocument(CD 情報書類)は、このサンプルのドキュメントコントローラーで、NSDocument のサブクラスです。

ドキュメントの基本機能(保存や開く)の管理とともに、CDInfoDocument は、データソースとして、genreComboBox もコントロールします。データソースメソッド numberOfItemsInComboBox: はコンボボックス内にある項目数を返し、comboBox:objectValueForItemAtIndex: は与えられた行に表示される文字列を供給します。

CDInfoDocument は、ジャンルルコンボボックスがウェブブラウザのアドレスフィールドによく似た自動補完をできるように、comboBox:completedString: を実装します。

最後に、CDInfoDocument は、コンボボックスと他のフィールド(バンド名、CD タイトル)に対して NSControlTextDidBeginEditingNotificationNSControlTextDidEndEditingNotification を登録することで、テキストフィールドに対する取り消し・やり直しを提供します。通知は、各キーストロークではなく、ユーザーがテキストフィールドまたはコンボボックスのどれかの編集を完了したときに受けとられます。そのため、これらのフィールドに対する取り消し・やり直しは、キーストロークごとではないものになります。

2 アプリケーション

まず、Xcode 上でビルドして実行してみます。アプリケーションを起動すると、次のようなウインドウが表示されます。

「CD Title(CD タイトル)」「Band Name(バンド名)」「Genre(ジャンルル)」各フィールドと下のテキストビューに文字が入力できます。入力途中で取り消しを選べば、以前の内容に戻ります。

ジャンルルコンボボックスの右の下向きの矢印をクリックすれば、「Acid」「Alternative」などの値をリストするメニューが出ます。値を選べば、その値がテキストフィールドに入力されます。別の値を独自に入力しても、それはメニューには反映されないことを注意してください。メニュー内にあるジャンルル、たとえば「Jazz」を入力しようと、「J」を入力すれば即座に Jazz が入力されます。「C」を入力すれば、「Classic Rock」が表示され、つぎに「o」を入れると補完は「Country」に変わります。このように補完は一番最初に一致するものが表示されます。

メニューから「Save(保存)」を選ぶと書類を保存できます。保存した後でウインドウを閉じ、保存したファイルを開くと、以前入力した内容が出現します。

3 ファイル構成

プロジェクトを構成しているファイルは、以下のようになります。

CDInfoDocument.h と .m が、ドキュメントクラスを実装しているファイルです。通常の MainMenu.nib とは別に、CDInfoDocument.nib があることに注意してください。これはドキュメントを開いたときにその内容を表示するウインドウのユーザーインターフェースを定義しているファイルです。

また、Info-SimpleComboBox.plist にドキュメントタイプなどについての指定があることにも注意してください。これは、Xcode の「グループとファイル」内のターゲット内でインスペクタから設定することもできます。名前は「cdinfo document type」で、拡張子は cdinfo で、クラスには CDInfoDocument クラスが設定され、バイナリ形式で保存されます。役割がエディタなので、保存が行えます。ここれがビューアなら閲覧だけしかできないことになります。パッケージがチェックされていますが、保存コマンドが通常ファイルに書き出しているだけなので、保存されたファイルはパッケージではありません。

クラスに CDInfoDocument が設定されているので、書類が開かれたり、新規書類が作られるときに、このクラスのインスタンスが作成され、そのメソッドが呼ばれることになります。

4 MainMenu.nib

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

MainMenu.nib は、ウインドウがなくメニューだけの状態のままです。メニューも直接インスタンスに接続するのではなく、First Responder(一次レスポンダ)に接続されていることに注意してください。

このサンプルでは、独自な内容はすべてドキュメントの操作に関するものなので、独自のアプリケーションオブジェクトがなく、中心的なコントローラーは NSApplication をそのまま使っています。起動時に新規書類が作られたり、開くなどで保存された書類が開かれるときに、ドキュメントクラスが作られ、それがすべての独自な操作を処理します。

5 CDInfoDocument.nib

ドキュメントが開かれたり、新規書類が作成されたときに使われる CDInfoDocument.nib は次のようになっています。

File's Owner(ファイル所有オブジェクト)を選択して、インスペクタで「Custom Class(独自クラス)」を見てみると、AlbumDocument となっています。これはこのサンプルが作成される最初にそういう名前のクラスが存在したのだと思われ、その後修正されないままになっているようです。クラス自体を調べてみると、CDInfoDocument と同じアウトレットを持っています。問題がないのは、この 2 つのインターフェースが同じで、実際に nib を開いたものが File's Owner になるからです。そのため、AlbumDocument となったままでも、インターフェースが同じ CDInfoDocument インスタンスが開くことで問題なく動作しているわけです。気持ち悪い方は修正しましょう。ややこしいので修正したものとして説明します。

File's Owner(ファイル所有オブジェクト)にはアウトレットがたくさんあります。cdTitleCell は一番上の「CD Title」というラベルをもつフォームセルに、bandNameCell は上から 2 番目の「Band Name」というラベルをもつフォームセルに接続されています。これら 2 つは NSForm インスタンス内のセルで、アウトレット infoForm がそのフォームに接続されています。

genreComboBox は「Genre」というラベルの右のコンボボックスに接続されています。アウトレット infoTextView は、スクロールビュー内のテキストビューに接続されています。そして、window がウインドウに接続されています。

ウインドウの delegate が File's Owner(ファイル所有オブジェクト)である CDInfoDocument に接続されていることに注意してください。

コンボボックスのアウトレットを見てみましょう。dataSourcedelegate が File's Owner(ファイル所有オブジェクト)に、nextKeyView が一番上の CD Title: というラベルをもつフォームセルに接続されています。後の方のものは、キービューループを作るもので、上の NSForm を見てみると、コンボボックスに接続されています。じつは、このキービューループは有効ではありません。実際、一度接続を外すとフォームセルには再び接続できません。とはいえ、ビルドして実行してみると正常に動作しているようですが、ここで設定されているようにフォームに戻る動作は起こりません。このへんをチェックしようとすると、管理人の環境ではアクセス権の変更などがあり、再起動ごとに動作が違ったりするので、このサンプルもしくは他のソフトが何らかの問題を起こしているようです。そのため、理屈では説明できますが、実際にどのように修正され機能しているか判りません。

アクションは特に設定されていません。コンボボックスの属性で、データソースの使用と補完にチェックが入っていることに注意してください。これらがチェックされていないなら、ウインドウ表示時にコンボボックスに対してメソッドを使ってそれらを設定しなければなりません。

6 CDInfoDocument クラス

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

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

CDInfoDocument.h > 継承とインスタンス変数
@interface CDInfoDocument : NSDocument { @private IBOutlet NSForm *infoForm; // 一番上のフォーム IBOutlet NSFormCell *bandNameCell; // フォーム2番目のセル IBOutlet NSFormCell *cdTitleCell; // フォーム一番上のセル IBOutlet NSComboBox *genreComboBox; // ジャンルル用コンボボックス IBOutlet NSComboBoxCell *genreComboBoxCell; // コンボボックスセル IBOutlet NSTextView *infoTextView; // 一番下のテキストビュー NSString *initEditString; NSMutableArray *genres; // ジャンルルメニュー用配列 NSData *dataFromFile; // ファイルからのデータ }

NSDocument のサブクラスであることに注意してください。各インスタンス変数の意図は、コメントでわかると思います。

@private は、プライベートな変数であることを指定します。何も指定しないと @public になるので、これを指定しないと他からインスタンス変数が操作できてしまい、オブジェクト指向のクラス分離の考え方に反してしまいます。インスタンス変数は @private、サブクラスから参照したい場合は @protected をつける習慣をつけたほうがいいでしょう。Objective-C 言語では、インスタンス変数を直接操作するのではなく、アクセサを通じてアクセスすることを推奨しています。ただし、これをしないと動かないわけではありません。クラス階層が単純だったり、小規模なアプリケーションなどでは、あまり気にしなくても大丈夫です。

6.2 初期化

さて、コントローラーが見当たりませんでしたが、このアプリケーションの起動時の挙動はどうなるのでしょう。このサンプルでは、アプリケーション全体の初期化がないですが、NSApplication は、デフォルトで起動したとき、新規書類を作成するように設定されています。そのため、このサンプルが起動した場合、nib が読み込まれてアプリケーションの起動が終了した段階で、新規書類を作成しようとして、このクラスのドキュメント関連メソッドが呼ばれることになります。これは通常の「開く」が選ばれたときと同じで、新しく作成したデフォルトデータでウインドウを設定するのか、ファイルから読み込まれたデータでウインドウが設定されるのか、が違うだけです。そのため、次の標準のドキュメントメソッドで説明します。

起動時の新規書類作成をオフにするには、アプリケーションの委任としてコントローラークラスを作成し、それから委任メソッド applicationShouldOpenUntitledFile: に対して NO を返します。起動時の新規書類は作成するものの、ドックからアクティブにされた場合だけ新規書類を開きたくないなら、 applicationShouldHandleReopen:hasVisibleWindows:NO を返します。これは『ドキュメントベースのアプリケーション概説』>「よくある質問と回答」の最後の「どうしたら起動時にアプリケーションが名称未設定の書類を作成することを避けられるでしょうか?」に書かれています。

6.3 標準のドキュメントメソッド

まず、ドキュメントを扱うアプリケーションの全体的な構造を思い出してください。NSDocumentController またはそのサブクラスが、ドキュメント関連全体のコントロールを行います。新規ドキュメントを作る場合などは、これに newDocument: が送られ、既存の書類を開くなら openDocument: が送られます。

そして、NSDocument またはそのサブクラスが指定されたタイプで作成されます。既存の書類を開く場合は、まず NSOpenPanel が開かれ、ファイルが指定されます。そこで指定されたファイルのタイプと読み込まれたデータ内容によって、NSDocument またはそのサブクラスが作成されることになります。

NSDocument またはそのサブクラスは、データを設定して、自分に NSWindowController またはそのサブクラスを設定します。最後にドキュメントが画面に表示され、その時に NSWindowController がウインドウを表示することになるわけです。

この流れにおいて、新規作成などの時に通常とは違う事を行いたい場合、NSDocumentController のサブクラスを作ることになります。また、1 つの書類に対して 1 つのウインドウではなく、複数のウインドウを作成したりなど、通常とは違う動作を行うなら、NSWindowController のサブクラスを作ることになります。このサンプルでは、このどちらのサブクラスも作られておらず、通常の動作の流れのままで、NSDocument のサブクラスだけが作られています。

さて、このサンプルで起動した時に新規書類が作られるときは、NSDocumentControllernewDocument: が送られ、結果として CDInfoDocumentinitWithType: error: が送られることになります。また、既存の書類を開く場合は、NSDocumentControllerNSOpenPanel を使ってファイルの位置を取得し、結果として CDInfoDocumentinitWithContentsOfURL:ofType:error: が送られます。

これら 2 つのメソッドは、CDInfoDocument クラスでは実装されていません。したがって、NSDocument クラスの実装がそのまま使われることになります。

この初期化の過程で、ドキュメントのデータが新規作成されたり、ファイルから読み込まれることになります。このときに実装するメソッドは、データ基準と位置基準の 2 種類があり、このサンプルではデータ基準の方が使われています。これは、dataOfType:error:(保存すべきデータを返す)と readFromData:ofType:error:(読み込まれた未加工のデータからドキュメント内容を設定)の 2 つになりますが、Mac OS X v10.3 以前との互換性をもつには、これらはそれぞれ dataRepresentationOfType:loadDataRepresentation:ofType: です。このサンプルが作成されたのが古いので、このサンプルでは後の方のメソッドが実装されています。まず保存のほうのメソッドを見てみましょう。

CDInfoDocument.m > dataRepresentationOfType:
- (NSData *)dataRepresentationOfType:(NSString *)aType { // loadDocumentWithInitialData が期待する形式でデータをアーカイブ化 NSMutableData *data = nil; if ([aType isEqualToString: CDINFO_DOCUMENT_TYPE]) { NSArray *formCellStrings = [NSArray arrayWithObjects: [self cdTitle], [self bandName], [self musicGenre], nil]; NSArchiver *archiver = [[[NSArchiver alloc] initForWritingWithMutableData: [NSMutableData data]] autorelease]; [archiver encodeObject: formCellStrings]; [archiver encodeObject: [infoTextView textStorage]]; data = [archiver archiverData]; } return data; }

まず、ローカル変数 data を宣言して、nil に設定しています。成功すれば、これにデータが入ります。

つぎにタイプがこのサンプルで指定されているドキュメントのタイプと一致するか確認します。定数 CDINFO_DOCUMENT_TYPE は実装ファイルの先頭で定義されています。

#define CDINFO_DOCUMENT_TYPE @"cdinfo document type"

これは、ターゲットの情報インスペクタのプロパティで設定されている「名前」と同じであることに注意してください。

タイプが問題なければ、データの作成にうつります。まず、3 つのオブジェクトを要素として持っている配列を作成しています。この要素は、自身のアクセサメソッドを経由してセルやコンボボックスから取得した NSString になります。つぎにアーカイバーを作成し、そのアーカイバーにいま作ったオブジェクトをコード化しています。それからテキストビューから取得したテキストをコード化しています。

このサンプルでは順次アーカイブが使われていることに注意してください。そのため、このコード化の順番が重要になります。Mac OS X v10.2 から順序に関係ない、キー付きアーカイバーが実装されており、そちらを使うことが推奨されていることに注意してください。ちょっと考えてみればわかりますが、順序によるものの場合、将来内容の要素が追加されたり削除された時に対応が大変です。キー付きなら、古い形式は単に新しく追加したものが存在しないだけですし、古い形式と新しい形式の両方に対応するのが簡単になります。

最後にアーカイバーでアーカイブ化されたデータをローカル変数に取得し、それを返しています。タイプが違うなら、データは nil のままになります。

つぎに、このメソッドと対応する読み込みのほうを見てみましょう。データ自体はすでにファイルから未加工の状態で取り出されていることに注意してください。渡されるのはファイルの場所ではなく、読み込まれたデータ内容を保持している NSData です。上のメソッドでアーカイブ化を行ったので、データは上の形でアーカイブ化されたものです。

CDInfoDocument.m > loadDataRepresentation: ofType:
- (BOOL)loadDataRepresentation:(NSData *)data ofType:(NSString *)aType { if ([aType isEqualToString: CDINFO_DOCUMENT_TYPE]) { dataFromFile = [data retain]; return YES; } return NO; }

なんか肩すかしをくらったように思うかもしれません。ここで、データがアーカイブ復元されることを期待していたかもしれません。この時点で、まだウインドウコントローラーが設定されていないことに注目してください。通常の流れでは、ドキュメントオブジェクトが初期化された後で、ウインドウコントローラーが追加されますが、データ関連メソッドはその前の初期化のあいだに呼ばれます。

このドキュメントクラスでは、データそのものを独自に保持せず、ウインドウ内の各コントロールに持たせています。そのため、このメソッドが呼び出されるタイミングでデータを分解しても、そのデータを保持することができません。インスタンス変数やデータモデルオブジェクト等に別個にデータを保持し、そのデータとユーザーインターフェースを同期している場合なら、このタイミングでデータオブジェクトを作成するなどの作業を行うことができるでしょう。

このサンプルではそういう訳にはいかないので、ここでは受けとったデータのタイプが正しいなら、それをインスタンス変数のなかに保持し、あとでウインドウが作成された後で、ウインドウ内のユーザーインターフェース項目に設定するようにしています。

データがタイプどおりで問題がないなら、次にウインドウコントローラーが設定されます。このサンプルでは古いメソッドが使われていますが、error: 引数をもつ新しいメソッドではタイプが一致しないデータを受けとった場合など、エラー処理を行う必要があるかもしれません。

さて、このサンプルではデフォルトの NSWindowContoller が使われます。nib ファイルの説明を思い出してください。このサンプルでは、CDInfoDocument.nib でドキュメントで書類用のウインドウを定義していました。ターゲット設定では、クラスの名前を設定しただけなので、どの nib ファイルを利用すべきかはわかりません。これを返しているのが次の継承メソッドです。

CDInfoDocument.m > windowNibName
- (NSString *)windowNibName { return @"CDInfoDocument"; }

『ドキュメントベースのアプリケーション概説』>「NSDocument のサブクラスの作成」>「オプションで実装するメソッド」で説明されているように、これはドキュメントと関連する nib が 1 つで、ウインドウも 1 つの時だけ、ということに注意してください。そうでない場合、makeWindowControllers をオーバーライドして、ウインドウコントロラーを作ったり、独自動作を行う必要があります。

nib の名前が返されたので、ウインドウコントローラーが自動的に作成され、nib が読み込まれます。読み込みが完了したとき、windowControllerDidLoadNib: が呼ばれます。

CDInfoDocument.m > windowControllerDidLoadNib:
- (void)windowControllerDidLoadNib:(NSWindowController *) aController { [super windowControllerDidLoadNib:aController]; [[genreComboBox window] setFrameAutosaveName:@"MainWindow"]; [[genreComboBox window] setFrameUsingName: @"MainWindow"]; // 出力の画一性のためだけにコンボボックスセルの参照を取得 //(すべての場所でコントロールではなく、セルを扱う) genreComboBoxCell = [genreComboBox cell]; // いくつかの標準ジャンルルを読み込む。 // メモリ使用を減少させ、(実装されていたなら)リストの変更または追加を共有できるように // 実際の実装がおそらく「genres」を共有できるオブジェクト内に移すだろうことに注意 genres = [NSArray arrayWithObjects: @"Jazz", @"Acid", @"Funk" , @"Classic Rock", @"Rock", @"Pop" , @"R&B" , @"Hip Hop" , @"Trip Hop" , @"Classical" , @"Swing" , @"Metal" , @"Country" , @"Folk", @"Grunge", @"Alternative", nil]; genres = [[genres sortedArrayUsingSelector:@selector(compare:)] retain]; // セルが編集を開始・終了した時に知りたいことを NSNotificationCenter に伝える //(それにより適切なレベルでテキストフィールドに対する取り消し操作をセットアップできる) [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(cellTextDidBeginEditing:) name: NSControlTextDidBeginEditingNotification object: infoForm]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(cellTextDidEndEditing:) name: NSControlTextDidEndEditingNotification object: infoForm]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(cellTextDidBeginEditing:) name: NSControlTextDidBeginEditingNotification object: genreComboBox]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(cellTextDidEndEditing:) name: NSControlTextDidEndEditingNotification object: genreComboBox]; // loadDataRepresentation: が以前に使われていたら得られているだろう // データを読み込むための標準的な作業を行う if (dataFromFile) [self loadDocumentWithInitialData]; }

上位クラスの実装が呼ばれていることに注意してください。ドキュメントウインドウの位置を自動保存するための名前を設定するために setFrameAutosaveName: が呼ばれています。また、それと対応して以前に保存されている位置に新しく作成したウインドウを移すように、setFrameUsingName: が呼ばれています。

つぎにジャンルルコンボボックスで使われるデータを準備しています。nib ファイルでデータソースがこのクラスに設定されていたので、コンボボックスがメニューデータを必要な時にデータソースメソッドが呼ばれることになりますが、この配列はその時に使われるものです。コメントにもありますが、複数の書類で共通するデータを実装するなら、このデータはドキュメントクラスではなく、アプリケーションコントローラークラスを新たに作り、そこに移したりする必要があります。このサンプルでは、書類インスタンスごとにこの配列が作成されることになります。

つぎにテキストが入力される各セルなどについて、編集が完了したときに通知するように指定します。この通知を使って、取り消しを実装します。

最後に、ウインドウのユーザーインターフェースがすべて読み込まれて設定されたので、以前に説明した loadDataRepresentation: ofType: でインスタンス変数に設定されていたファイルデータを分解し、各インターフェースにデータを設定するメソッドを呼び出しています。このメソッドが判りづらいなら、dataRepresentationOfType: を参考にしてください。

CDInfoDocument.m > loadDocumentWithInitialData
- (void)loadDocumentWithInitialData { // 以前にアーカイブ化されたデータをコード復元 // 形式は、文字列の配列で、テキストビューに対するテキスト格納がそれに続く NSUnarchiver *unarchiver = [[[NSUnarchiver alloc] initForReadingWithData: dataFromFile] autorelease]; NSArray *archivedFormCellStrings = [unarchiver decodeObject]; NSTextStorage *archivedTextStorage = [unarchiver decodeObject]; // アーカイブ復元したデータでテキストフィールドとテキストビューを満たす if ([archivedTextStorage length]) [infoTextView insertText: archivedTextStorage]; [self setCDTitle: [archivedFormCellStrings objectAtIndex:INDEX_CD_TITLE]]; [self setBandName: [archivedFormCellStrings objectAtIndex: INDEX_BAND_NAME]]; [self setMusicGenre: [archivedFormCellStrings objectAtIndex: INDEX_MUSIC_GENRE]]; // ドキュメントが編集済とは考えないようにする // 上の insertText: の呼び出しは、編集済と考えられてしまう可能性がある [[self undoManager] removeAllActions]; [dataFromFile release]; dataFromFile = nil; }

まず、アンアーカイバーを使って、データをアーカイブ復元します。コメントに書かれているように、このデータは以前に dataRepresentationOfType: 内でアーカイバーを使ってコード化されたものです。よって、コード化したオブジェクトと同じクラスのローカル変数にコード復元しています。

つぎに、復元したデータをユーザーインターフェイス内の項目に設定しています。それから取り消しマネージャーを呼び出して、この設定自体が取り消し可能なアクションにされてしまわないように、現在までのすべてのアクションを消去するように命じています。

最後に、インスタンス変数で保持していたデータはもう必要でなくなったので、それを解放しています。

これでデータの保存と復元に関する部分は理解できたと思います。こうして、アプリケーションが起動され、新規作成される場合は単に nib ファイルの状態のままで、ファイルが開かれる場合はそれにデータが設定されることになります。これ以降は、ユーザーがアプリケーションに対して行った操作に対応するだけです。

あとのメソッドは、どの順番で調べなければならないということはないですが、最初にコンボボックスのデータソースとしてのメソッドを見てみることにしましょう。

6.4 コンボボックスに対するデータの供給

起動したアプリケーションのドキュメントウインドウでジャンルルコンボボックスの右側の下矢印をクリックすれば、メニューが表示されます。このメニューのデータを提供するには、2 つの方法があります。ひとつは内部の項目リストを操作する方法で、コンボボックスに対してメソッドを呼び出すことで、内部項目リストの追加・削除を行います。もうひとつはデータソースを設定し、データが必要な時にコンボボックスがデータソースからデータを取得するように設定することです。このサンプルでは、nib ファイル内でコンボボックスのデータソースとしてドキュメントクラスが設定されていました。そして、属性インスペクタ内でデータソースの使用にチェックが入っていました。そのため、後の方の方法を使うことになります。

データソースは NSComboBoxDataSource 簡易プロトコルのメソッドを実装することになります。NSComboBoxCellDataSource というプロトコルあることに注意してください。通常は前の方のメソッドを実装します。この 2 つのプロトコルのメソッドで似たようなものもありますが、メソッドの最初が comboBox...comboBoxCell... となっているので区別できます。

まず、numberOfItemsInComboBox: メソッドは表示する項目の数を返します。

CDInfoDocument.m > numberOfItemsInComboBox:
- (int)numberOfItemsInComboBox:(NSComboBox *)aComboBox { return [genres count]; }

このメソッドは、単にコンボボックスのメニューに表示する項目を保持している配列である genres の要素数を返しているだけです。

つぎに、comboBox:objectValueForItemAtIndex: が指定された番号の項目のデータを返します。

CDInfoDocument.m > comboBox: objectValueForItemAtIndex:
- (id)comboBox:(NSComboBox *)aComboBox objectValueForItemAtIndex:(int)index { return [genres objectAtIndex:index]; }

このメソッドは、単に genres 配列の指定された番号にある要素を返しているだけです。

上の 2 つのメソッドは、コンボボックスのデータソースとしては必須のメソッドです。データソースは他のメソッドも定義していて、さらなる機能を使うためにはそれらを実装します。comboBox: indexOfItemWithStringValue: は渡された文字列に対応する項目に対する番号を返します。

CDInfoDocument.m > comboBox: indexOfItemWithStringValue:
- (unsigned int)comboBox:(NSComboBox *)aComboBox indexOfItemWithStringValue:(NSString *)string { return [genres indexOfObject: string]; }

ここでは、NSArray のメソッドを使って、渡された文字列に対応する要素の番号を返しています。これを実装することで、メニューを表示したまま、コンボボックス内にテキストを入力したとき、テキストに一致するメニュー項目が表示されるようにできます。サンプルアプリでためしてみてください。サンプルでは補完機能を実装しているので、最初の 1 文字を入力しただけで対応するものがあれば、補完が行われ、結果の文字列によってメニューがそこまでスクロールされることが判ると思います。

つぎに補完のためのメソッドを見てみましょう。『コンボボックス』>「コンボボックスにおける自動補完の使用」で説明されているように、ユーザーが文字を入力するたびにコンボボックスの completedString: メソッドが呼び出されます。このメソッドは、最初にデータソースが comboBox: completedString: または comboBoxCell: completedString: に応答するかを調べ、応答しない場合だけ、メニュー項目を1つずつ探していきます。そのため、補完を有効にするなら、これらのメソッドを実装したほうが、速いレスポンスを可能にできます。このサンプルでは、comboBox: completedString: を実装しています。

CDInfoDocument.m > comboBox: indexOfItemWithStringValue:
- (NSString *)comboBox:(NSComboBox *)aComboBox completedString:(NSString *)inputString { // このメソッドは、ユーザーによって各文字がタイプ入力された後で受信される // これは IB において genreComboBox に対して「補完」フラグをチェックしていたから // ユーザーがタイプ入力した // inputString が与えられたとき、接頭語でジャンルルを検索できるかをチェックし、 // それを提案された補完文字列として返す NSString *candidate = [self firstGenreMatchingPrefix: inputString]; return (candidate ? candidate : inputString); }

まず、このクラスで実装されている firstGenreMatchingPrefix: メソッドを呼び出して、補完候補を取得します。これが nil でなければ、それを返し、そうでなければ入力されたままの文字列を返します。ここで使われている独自メソッドを見てみましょう。

CDInfoDocument.m > firstGenreMatchingPrefix:
- (NSString *) firstGenreMatchingPrefix:(NSString *)prefix { NSString *string = nil; NSString *lowercasePrefix = [prefix lowercaseString]; NSEnumerator *stringEnum = [genres objectEnumerator]; while ((string = [stringEnum nextObject])) { if ([[string lowercaseString] hasPrefix: lowercasePrefix]) return string; } return nil; }

まず、渡された文字列を小文字化します。それからジャンルルの項目のデータソース内の各要素をひとつずつ操作します。string に毎回つぎの文字列オブジェクトが設定され、最後まできたときに while 文が終了します。各要素の文字列も小文字化し、渡された文字列を接頭語として持つかどうかが調べられます。もし一致が見つかればすぐにそれを返します。while 文が終了したということは、どの文字列も接頭語として渡された文字列を持っていなかったということなので、nil を返します。

これで、データソース関連のメソッドはすべて調べました。つぎは、取り消し関連のメソッドを見てみましょう。

6.5 取り消し操作の実現

取り消しについての基本的なことは、「DotViewUndo」で軽く説明しました。NSDocument クラスのリファレンスを見てわかるように、ドキュメントベースでは、各ドキュメントクラスがそれに関連した取り消しマネージャーを持っています。これに関する細かい事柄は NSDocument で実装されているので、あとはその取り消しマネージャーを使う部分を実装すればいいだけです。すでに loadDocumentWithInitialData メソッドの実装のなかで、[self undoManager] が呼ばれ、取り消しマネージャーに登録されているアクションをすべて消去するように指示する部分がありました。このように、ドキュメントベースでは取り消しはすでに対応されているので、作成したアプリケーション独自の部分だけを実装すればいいようになっています。

windowControllerDidLoadNib: で、セルなどに対して編集完了時に通知されるように設定していたことを思い出してください。テキスト入力が行われたとき、別のフィールドに移るか、リターンで確定されれば、これらの通知が行われることになります。infoForm(一番上の 2 つのフィールドが含まれるセル)と genreComboBox(ジャンルルを指定する上から 3 番目のコンボボックス)に対して、cellTextDidBeginEditing:cellTextDidEndEditing: が呼ばれるように設定されていました。ちなみに、テキストビューは独自に取り消しに対する対応を実装しているので、何もしなくても取り消しマネージャーとやりとりし、必要な動作を行っていることに注意してください。そのため、このサンプルでは、上の 3 つの入力部分に対してだけ通知を設定しています。まず、編集が開始された時に呼ばれるメソッドを見てみます。

CDInfoDocument.m > cellTextDidBeginEditing:
- (void)cellTextDidBeginEditing:(NSNotification *)notif { // cellTextDidBeginEditing: が受信されたときに、 // 実際に変更されていた場合がわかるように、 // 編集されるセルのスナップショットをとる NSText *fieldEditor = [[notif userInfo] objectForKey: @"NSFieldEditor"]; initEditString = [[fieldEditor string] copy]; }

編集開始時に、セルのテキスト内容のコピーをとっています。これは編集終了時に修正があったかどうかを判定するためです。フィールドエディタは、ウインドウ内のテキストフィールドで共有で編集操作を行うためのオブジェクトです。利用者情報ディクショナリからフィールドエディタを取得して、それに編集を開始するテキストを要求していることに注意してください。

つぎに編集完了時のメソッドを見てみます。

CDInfoDocument.m > cellTextDidBeginEditing:
- (void)cellTextDidEndEditing:(NSNotification *)notif { // 編集されたセルの文字列値が変更されているかチェック // そうなら、取り消しスタックにアクションを追加(これはドキュメントを編集済にする) NSText *fieldEditor = [[notif userInfo] objectForKey: @"NSFieldEditor"]; NSString *endEditString = [fieldEditor string]; // そうなら、さらに偏執狂的なチェックを行う if (!initEditString) initEditString = [@"" retain]; if (!endEditString) endEditString = @""; if (initEditString!=endEditString && ![endEditString isEqualToString: initEditString]) { NSCell *editedCell = [[notif object] selectedCell]; NSArray *undoInfo = [NSArray arrayWithObjects: editedCell, initEditString, nil]; [[self undoManager] registerUndoWithTarget: self selector:@selector(applyCellUndo:) object: undoInfo]; [[self undoManager] setActionName: [self undoActionNameForCell: editedCell]]; } [initEditString release]; initEditString = nil; }

編集開始と同様に、利用者情報ディクショナリからフィールドエディタを取得しています。そして、編集完了時の現在の文字列を取得しています。

それから、編集開始時にメソッドが呼び出されていないというあまりありそうがない場合を考慮して、その場合 initEditString を空文字にしています。また、同様に編集完了時の文字列が取得されない場合も endEditString を空文字に設定しています。

つぎに編集開始時と編集終了時で文字列が同じかどうか、その内容が等しいかどうかをチェックしています。違うなら以下の文が実行されることになります。まず、通知の対象となったセルを取得します。次に、取り消しの情報として、編集されたセルと、初期文字列を配列に格納しています。つぎにそれを使って取り消しマネージャーに取り消し操作を登録します。セレクタとしてこのクラスで実装されているメソッドか指定されています。このメソッドに渡されるのが先ほど作った配列です。最後にアクション名を独自メソッドを呼び出して設定します。それから編集開始時文字列を解放し、nil を代入しています。アクション名を設定しているメソッドを見てみます。

CDInfoDocument.m > undoActionNameForCell:
- (NSString *)undoActionNameForCell:(NSCell *)cell { // (「編集」のもとで)「取り消し/やり直し」メニュー項目に表示されてほしい文字列を返す if (cell==cdTitleCell || cell==bandNameCell) return [[cell title] substringToIndex: [[cell title] length] - 1]; else return @"Genre"; }

フォームのいずれかなら、タイトルから「:」を除いた文字列を返し、そうでなければ「Genre」という文字列を返しています。

さて、実際に取り消しが選ばれたとき、登録されたように、applyCellUndo:undoInfo が渡されて呼ばれることになります。この undoInfo は、メソッドの実装を見てわかるように、セルと復元後の文字列(つまり編集される前の文字列)を要素として持つ配列です。

CDInfoDocument.m > applyCellUndo:
- (void)applyCellUndo:(NSArray *)undoInfo { // 指定された取り消しを適用し、別の取り消しを登録 // (たとえば、取り消しをやり直すとき)これは現在の状態にリセットする効果を持つものになる NSCell *affectedCell = [undoInfo objectAtIndex:0]; NSString *newString = [undoInfo objectAtIndex:1]; [[self undoManager] registerUndoWithTarget: self selector:@selector(applyCellUndo:) object: [NSArray arrayWithObjects:affectedCell,[affectedCell stringValue],nil]]; [affectedCell setStringValue: newString]; }

行われていることは簡単です。渡された配列の最初の要素は編集されたセルなので、それを affectedCell へと代入します。また、次の要素は、編集前の文字列だったので、それを newString に設定します。その後で、これから行う取り消し操作をさらに取り消す場合に備えて、取り消し操作を登録します。その後で、設定したセルに編集前の文字列を設定します。ここで編集前…という言葉を使いましたが、取り消しのやり直し操作の場合は、これは編集後の文字列となります。このように、取り消しマネージャーでは、取り消しとやり直しをあまり区別しません。とにかく、いま行った作業を元に戻すための操作を登録して、それを行えばいいだけです。

6.6 残りのメソッド

残りのメソッドは単純です。それは単なるアクセサで、インスタンス変数を参照するわけではなく、単にセルから文字列を抽出するだけのものです。これらは、データを保存するときに主に使われることになります。単純なので、ここではくわしく説明しません。

7 まとめ

このサンプルでは、ドキュメントベースのアプリケーションの基本が示されます。しかし、サンプルの作成自体が古いので、古いメソッドが使われていることに注意してください。Mac OS X v10.2 以降ではドキュメントクラスのメソッドが追加され、それらの使用が推奨されています。このへんについては、『ドキュメントベースのアプリケーション概説』を読んでください。

特に、Mac OS X v10.4 以降では、ドキュメントクラスだけに限らず、多くのクラスで error: 引数がメソッドに追加されていることに注意してください。これらについては、『Cocoa のためのエラー処理プログラミングガイド』を見てください。以前は、例外だけだったので、エラーを総体的に扱うのがけっこう難しい状態でした。Mac OS X v10.4 からは、error: 引数が追加されたメソットによって、コード作成時の例外は NSException によって、アプリケーション実行時のエラーは NSError によって対応するという役割分担が確立されました。これによって、例外処理がよりわかやすく、カスタマイズしやすくなっています。特に、メッセージ文字列を設定すれば、特に対応しなくても、最終的にエラーがアプリケーションレベルへと伝播し、アラートパネルを表示してくれるので、特に独自の処理ができない場合の対応が簡単です。なので、エラーオブジェクトの設定はきちんとしたほうがいいでしょう。


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