第9章
第9章
MyClipの機能を整える
前章ではNSStringクラスを使ったテキストデータによるファイル入出力についての説明をいたしました。続けてバイナリデータによるファイル入出力の説明を、といきたいところですがその前にMyClipの体裁をもっとアプリケーションらしく整えたいと思います。
●この章で行うこと
この章で行うことを先に紹介しておきます。
第1節
Saveボタンをなくして通常のアプリケーションのように「File」メニューの「Save」コマンドから保存を行うようにします。
また、ボタンをなくす前にボタンをデフォルトボタンにする方法、ボタンなどのビューパーツのタイトルを変更する場合のInterface
Builderのクセなどについて説明したいと思います。
第2節
Macではウィンドウを閉じてもアプリケーションは終了しないのがデフォルトの仕様になっています。しかしMyClipではウィンドウを閉じればMyClipも終了するように変えたほうが良いでしょう。実際にMacにおいてもひとつのメインウィンドウしかもたいないアプリケーションではこのような仕様に変えているものは珍しくありません。
そして参考としてアプリケーション終了時に未保存のデータを強制的に保存する方法を説明します。しかしこの自動で保存を行ってしまう振る舞いはMacのデフォルトの振る舞いに慣れているユーザにとって違和感を覚える動きだと思います。この節ではこういう方法もあるという意味で説明していますが、のちの章ではMacのデフォルトの振る舞いにあわせてアプリケーション終了時に未保存データがある場合はアラートシートで保存するかどうかを聞くようなかたちに変えたいと思います。
この章では以上のことをMcClipに実装してよりアプリケーションらしくしていきたいと思います。
これからの作業はいつものとおりMyClipフォルダのプロジェクトファイルを使って行なっていきます。前章の第1節までの過程を残しておきたい場合はバックアップをとっておいてください。なおMyClip
7.2Networkはネットワークストレージからのデータ取得をテストするためだけのプロジェクトです。今後それ以外の目的で使用することはありません。
9. 1. 1 Controllerクラスの変更
今回もModelクラスについては変更はありません。SaveメニューはViewクラスの一員です。したがってViewの更新に責任を持つControllerクラスには関係(影響)があってもModelクラスにはなんら影響がでないことはご理解いただけるのものと思います。
●Controller.hの変更
Controller.hを次のように変更します。いつものとおり強調箇所が追加・変更されるコードです。
#import
<Cocoa/Cocoa.h>
#import
"Model.h"
@interface
Controller : NSObject {
Model *model;
IBOutlet NSTextView *textView;
//IBOutlet NSButton *button;
}
-
(void)readFromFile;
- (IBAction)writeToFile:(id)sender;
@end
この章ではボタンをなくします。したがってまずインスタン変数のNSButton型のbuttonアウトレットは不要になります。コード例では分かりやすくするためにコメント化で無効にしていますが、削除してもらっても構いません。というよりはコードをすっきりとさせるために削除するべきでしょう。次に保存を指示するwriteToFile:メソッドの戻り値をvoidからIBActionに変更しています。これでtargetにControllerをactionにwriteToFileを接続する作業をコードからではなくInterface Builderから行うことができるようになります。
デリゲートやターゲットアクションの接続については、これらのデザインパターンの仕組みをより一層理解していただくためにコーディングによる接続も行っていましたが、どちらかと言えばInterface Builderで接続を行うほうが一般的でしょう。しかしコーディングによる接続もObjective-Cをより理解されいく段階でInterface Builderにはない便利さがあることに気づかれることだろうと思います。
以上の変更の結果Controller.hは最終的に次のようになります。
#import
<Cocoa/Cocoa.h>
#import
"Model.h"
@interface
Controller : NSObject {
Model *model;
IBOutlet NSTextView *textView;
}
-
(void)readFromFile;
- (IBAction)writeToFile:(id)sender;
@end
●Controller.mの変更
Controller.mで変更されるメソッドはawakeFromNibとwriteToFileになります。
□awkeFromNibメソッド
-
(void)awakeFromNib
{
[textView setString:@""];
//[textView setDelegate:self];
//[button setTarget:self];
//[button
setAction:@selector(writeToFile:)];
if ([model string])
[textView setString:[model string]];
//[button setTitle:@"Save"];
}
[textView setString:@""];
TextViewの英文を消すためのメッセージの引数の空文字を生成する方法を[NSString string]というメッセージ式から@""というオブジェクト定数の生成に戻しました。理由は、前にも少し述べたと思いますがリソース(コスト)の節約にとっては無用なメモリ占有をさけることよりも無用なCPUの負荷をさけることのほうが優先されているからです。
なお、このリソースやコストの節約のことについて書くたびに少し胸が痛みます。Text
Viewでは文字列に変更を加えるたびにtextDidChangeデリゲートメソッドが送信され実行されています。MyClipはこのことで一番リソースを使っています。そのことを考えるとほか面でリソースやコストのことを考えても全体的にはtextDidChangeメソッドでのリソース使用に比べるとすべて微々たるものになります。 しかしめげずに今後のためにもリソースやコストについては機会があるたびに繰り返し説明していきたいと思います。
次に次の4つのメッセージ式は説明のためにコメント化で無効にしていますが確認が終わりましたら削除してもらったほうが良いでしょう。最初の3つのメッセージ式はデリゲートとターゲットアクションの接続を行うメッセージ式ですが、この章ではこれらの接続をInterface Builderで行います。したがってこのメッセージ式は不用になります。
[textView
setDelegate:self];
[button
setTarget:self];
[button
setAction:@selector(writeToFile:)];
[button
setTitle:@"Save"];
4つ目のメッセージ式はプッシュボタンのタイトルを設定するものですが、この章ではプッシュボタン自体なくします。またInterface Builderでのプッシュボタンのタイトルの変更方法ものちほど説明することにしています。
以上の変更の結果awakeFromNibメソッドは最終的に次のようになります。
- (void)awakeFromNib
{
[textView
setString:@""];
if
([model string])
[textView setString:[model
string]];
}
さらにInterface
BuilderでText Viewを最初から空文字として設定しておくことも出来ますが前にも言ったようにInterface BuilderでTextViewを選択する場合にデフォルトの英文を残しておいたほうが便利です。ま if ([model string])ではMainMMenu.xibファイルに含まれていないMedelオブジェクトが関係しています。したがってコーディングを行わずにInterface
Builderでこの箇所を設定するということはできません。
この時点で一度プロジェクトを「ビルドして進行」してみてください。そしていろいろな作業をしてみましょう。今までのコードが何の役目をしていたのかが掴めてくると思います。テストが終わりましたらMyClipを終了させて、再びコーディン作業に戻ります。
□writeToFile
- (IBAction)writeToFile:(id)sender
{
[[model string]writeToFile:filePath
atomically:YES encoding:4 error:NULL];
[[sender window] setDocumentEdited:NO];
}
メソッドの戻り値をヘッダファイルのメソッド宣言に合わせてvoidからIBActionに変えています。このメソッドでそのほかに変更する箇所はありません。
9. 1. 2 MainMenu.xibの変更
MainMenu.xibをダブルクリックしてInterface BuilderでMainMenu.xibを開きます。まずはじめに気になることはMainMenu.xibウィンドウに警告1のマークがでていることです。もしかするとこの警告マークがMyClipメインウィンドウで隠れている場合があります。一度MainMenu.xibウィンドウをクリックして最前面に出して確認してみてください。
図:MainMenu.xibウィンドウのエラー表示
この警告マークをクリックするとMainMenu.xib
Infoウィンドウが表示されます。
図:MainMenu.xib Info
警告の内容は「Push Buttonに接続していたbuttonアウトレットがもはや定義されていない」というものです。このまま作業を進めてもエラーになることはありませんが警告マークが出たままにしておくのは気になります。また中には重要な警告がある場合もあります。この警告マークを消すことにします。
MainMenu.xibウィンドウでControllerを右クリックすると接続関係の一覧が表示されます。一番最初の「Outlets」グループの「button」アウトレットが警告色の黄色になっています。接続先になっているPush
Button(Indicate)の左側の×印をクリックしてください。buttonアウトレットが一覧から削除されます。一覧を閉じるには一覧パネルの左上の×印をクリックします。MainMenu.xibウィンドウの警告マークも消えていると思います。MainMenu.xib
Infoウィンドウも閉じておきましょう。
図ボタンOutletの削除
●Saveメニューを有効にする
便宜的にSaveメニューと呼んでいますが正確にはSaveメニューアイテムです。
次の図のようにMainMenuの上部に並んでるものがメニューと呼ばれ、その下の階層にあるものはメニューアイテムと呼ばれています。なおこのInterface Builderでメニュー内容を編集するツールは正確にはウィンドウになっていますのでMainMenuウィンドウと呼びます。xibファイル全体を表しているMainMenu.xibウィンドウと混同しやすいので注意してください。またMainMenuウィンドウはその形からMainMenuバーと呼ばれることもあります。
図MainMenuウィンドウ
MainMenuというウィンドウ(バー)のFileメニューを選択してその下層にあるSaveアイテムを選択してください。
図File - Save
もしMainMenuというウィンドウが表示さてていないようでしたらMainMenu.xibウィンドウでMainMenuというオブジェクトをダブルクリックして開いてください。
図:MainMenuオブジェクト
Saveメニューアイテムが選択できましたら接続を行いますが、その前に少し注意しておくことがあります。ボタンの接続のところでも言いましたがこれまで接続を行う場合にはまず選択してから右クリック + ドラッグで接続するように言ってきました。しかしほとんどの場合は最初に選択していなくともいきなり右クリック + ドラッグで接続線を伸ばすことができます。しかしたまに、先に選択しておかないと右クリックで接続線を伸ばそうとするとそのビューバーツ自体がドラッグされてしまうことがあります。そういう理由から「まずは選択をしてから」ということにしています。なお、もしビューパーツ本体がドラッグされてしまった場合はcommand + Zで元に戻してください。
ではSaveメニューアイテムからMainMenu.xibウィンドウのControllerオブジェクトまで接続線を伸ばします。
図:Saveメニューアイテムの接続1
Controllerオブジェクトが強調表示されましたらマウスボタンを離します。表示される一覧のRecieved ActionsグループのwriteToFile:メソッドをクリックして接続を完了させます。
図:Saveメニューアイテムの接続2
続けてText Viewのデリゲートの接続も行いましょう。なおMainMenu.xibファイルの保存は指示がなくてもある程度の区切りごとに行ってください。保存方法はInterface Builderの「File」メニューから「Save」を選ぶか command + Sを押すという通常の方法と変わりませんがこの保存作業で保存されるのはMainMenu.xib ファイルです。実際に保存前にはMainMenu.xibウィンドウのクローズボタンに未保存を表す黒いドットが表示されていますが保存するとMainMenu.xibウィンドウのクローズボタンの黒いドットは消えます。
●Text Viewのdelegate接続
MyClipウィンドウの英文を2度にわけてクリックしText Viewを選択します。この方法は何度も説明しています。もしうまく選択できないときはMyClipウィンドウを選択しなおすところからやり直してみてください。そしてインスペクタパネルのタイトルがText View○○になっていることも合わせて確認してください。
図MyClipウィンドウ1
次に選択されたText ViewからMainMenu.xib ウィンドウのControllerオブジェクトまで右クリック + ドラッグで接続線を引きます。Controllerオブジェクトが強調表示されましたらマウスボタンを離します。
図:Text Viewデリゲートの接続1
接続先一覧が表示されましたらOutletsグループからdelegateを選びます。ただし今回はOutletsグループにはdelegateしか表示されていないと思います。この作業によりController.mのawakeFromNibで記述していた次のメッセージ式と同じことをしたことになります。
[textView
setDelegate:self];
図:Text Viewデリゲートの接続2
作業が終わりましたらMainMenu.xib を保存してください。さきほども言いましたとおり保存は読者の判断で随時こまめに行うようにください。
●テスト
これで接続関係の作業は完了しました。プロジェクトウィンドウに戻って「ビルドして進行」してテストを行ってください。次の2点を除いては正常に動作しているみたいです。
・ボタンの名前がIndicateになっている
・Saveコマンドを選んでもウィンドウの未保存を表す黒いドットが消えない
最初のボタンの名前については無視してもらって構いません。名前を設定するような作業は行っていませんし、このボタンはこの節の最後では削除することになります。
2番目の問題については該当するwriteToFileメソッドを見てみましょう。
Controller.m
-
(IBAction)writeToFile:(id)sender
{
[[model string]writeToFile:filePath
atomically:YES encoding:4 error:NULL];
[[sender window] setDocumentEdited:NO];
}
強調箇所の[sender window]というメッセージ式に問題がありました。このメッセージ式はsender(メッセージの送り手)がボタンの時のものをそのまま使っていました。何度か説明していますがビューパーツはウィンドウの上(中)になければ人の目に見えません。そしてそれぞれのビューパーツは自分が乗っかっている(所属している)ウィンドウをwindowというインスタン変数に格納することになっています。しかしメニュー(NSMenu)やメニューアイテム(NSMenuItem)はウィンドウに所属しているわけではないのでwindowインスタン変数を持っていません。今回のwriteToFileメッセージの送り手はSaveメニューアイテムですそのメニューアイテムに対して
[sender window]
とメッセージを送っても答えられるはずがありません。その結果、次のsetDocumentEdited:NOというメッセージにも応じることができなかったわけです。これはちょっとした驚きです。このことでメッセージ送信の実際が見えたのではないかと思います。確かにレシーバが応じられないメッセージを送ってもエラーは起こりませんでした。そしてそれ以前にコンパイラは警告やエラーを発することなくビルドを完了しています。
しかし驚いてばかりいても先に進めません。この問題についての解決策を次のように2つ考えました。
・新たにControllerクラスにNSWindow型のインスタン変数を宣言してそこにMyClipウィンドウを登録する。
・MyClipウィンドウに所属しているビューバーツText Viewのwindowメッセージを利用する
今回は2番目の方法を採用します。そのほうが面倒じゃない。という理由だけからです。writeToFileメソッドは次のように変更します。
-
(IBAction)writeToFile:(id)sender
{
[[model string]writeToFile:filePath
atomically:YES encoding:4 error:NULL];
[[textView window]
setDocumentEdited:NO];
}
コーディングの変更が終わりましたら再度「ビルドして進行」をしてテストしてみてください。今度は問題なく動作していると思います。
ここではControllerクラスがアウトレットとして持っているText Viewへの参照(インスタン変数)を利用してwindowというメッセージを送っています。しかしControllerオブジェクトが直接windowへの参照(インスタン変数)を持っていたほうが余計なメッセージ送信をひとつ減らせることになりコスト的にそちらのほうが有利だということは理解しておいてください。
9. 1. 3 デフォルトボタンなどの色々な設定
前項でこの節の主な目的であるSaveメニューを有効化する作業は終わりました。Saveボタンは付いているアプリケーションもあれば付いていないアプリケーションもあります。MyClipではSaveボタンは取り除くことにいたします。そしてその代わりテキストビューの画面を広げたいと思います。しかしボタンを取り除く前にボタンについて知っておいた方が良いInterface Builderでの設定などをこの機会に説明しておきたいと思います。
●デフォルトボタン
ボタンのなかにはクリックしなくてもReturnキーもしくはenterキーを押すことによってそのボタンを押したことになるものがあります。このボタンのことをデフォルトボタンと呼びます。
設定方法は、まずデフォルトボタンにしたいボタンを選択します。MyClipの場合にはSave(indicate)ボタンだけしかありませんのでSaveボタンを選択することになります。そしてButton AttributesインスペクタパネルのKey Equiv.とラベルの付いたフィールドを選択します。
図:デフォルトボタンの設定1
この状態でキーボードのreturnキーもしくはenterキーを押します。Key Equiv.フィールドにリターンを表すマークが表示されます。
図:デフォルトボタンの設定2
Interface BuilderのFileメニューからSimulate Interfaceを選ぶかキーボードでcommand + Rを押します。MyClipのシミュレート版が起動します。デフォルトボタンに設定したSave(Indicate)ボタンは青い色で脈打っています。ところがデreturnキーを押すと上の英文テキストが改行されてしまいます。そうですテキストデータとデフォルトボタンは一緒に使うのには向いていません。日本語を入力している場合にはreturnキーには変換文字列を確定させるという役割もありますのでなおさらです。
図:デフォルトボタンの設定3
Cocoa SimulatorメニューのQuit Cocoa Simulatorを選ぶかキーボードでcommand + Qを押してシミュレートを終了させてください。そしてデフォルトボタンの設定を解除するにはButton AttributesインスペクタパネルでKey Equiv.フィールドの右隣のClearボタンをクリックします。
図:デフォルトボタンの設定4
今回のMyClipではデフォルトボタンは使えません。確実にClearで設定を解除してファイルを保存してください。
●Text Viewのフォント設定
Text Viewのフォントは起動してから変更することもできますが最初からデフォルト値としてのフォントの種類とサイズなどを設定しておくことができます。
まずText Viewを選択します。そして次に「Font」メニューから「Show Fonts」を選びます。
図Font - Show Fonts
フォントパネルが現れましたら好みのフォントと大きさなどを選びます。次の図では「日本語」→「ヒラギノ角ゴPro」→「W3」→「12」を選んでいます。この状態でシミュレーターで様子を見ながら色々と選んでみると良いでしょう。
図:フォントパネルの設定
現在Text Viewはプレーンテキスト対応(非リッチテキスト)になっています。そして保存するデータ形式(フォーマット)も文字のだけを保持できる形式になっています。MyClipを起動してからフォントや大きさを変更することはできますが、そのような属性(フォントの種類や大きさなど)まで保存できるデータ形式ににはなっていません。したがって起動しなおした時には文字データは保持していますがフォントやサイズなどはデフォルトの設定に戻ってしまいます。もし文字が小さいなどの不便さを感じるならば今行っているようにMainMenu.xibファイルでText Viewのデフォルト値としてフォントや大きさを決めておいたほうが良いでしょう。 なおMyClipは最終的にはリッチテキスト対応に変更します。そして保存するデータ形式もあらゆる属性(文字のフォント、大きさ、色、スタイル、さらには添付画像まで)を保持できる形式に変更いたします。
●Push Buttonなどのタイトル変更
Interface BuilderでMyClipウィンドウのPush Buttonをダブルクリックするとタイトルが編集可能になります。
図Push Buttoのタイトル編集1
もしくはボタンを選択してインスペクタパネルのButton
AttributesのTitle欄でボタン名を変更することができます。
図Push Buttonのタイトル編集2
この2つの設定方法を比べるとインスペクタパネルでタイトルを変更するほうがしっかりした作業になっているような気がします。しかし意外なことにビューパールをダブルクリックしてタイトルを変更するほうが確実な場合があります。例えばメニューアイテムやのちほど登場してくるマトリックス内の各パーツのタイトルを変更する場合などはインスペクパネルではなぜかうまくいかない場合が多いです。そのようなパーツの場合はパーツ自体をダブルクリックしてタイトルを編集すると確実にタイトル名を変更できます。
●ボタンの削除を削除してText
Viewをウィンドウ一杯まで広げる
ではこの節の最後に不用になったボタンを削除してText Viewをウィンドウ一杯までひろげます。まずボタンを選択してください。そしてdeleteキーを押すか「Edit」メニューで「Delete」を選びます。
図Edit - Delete
次にMyClipウィンドウのScroll Viewのあるあたりをクリックします。Text ViewはSroll Viewの中に含まれていることを覚えていますか。Scroll Viewを選択できたかどうかはインスペクタパネルのタイトルバーで確認してください。
図MyClip - Scroll View編集1
Scroll Viewの下辺にあるドットをドラッグしてScroll Viewをウィンドウの下辺にスナップするように一杯まで広げます。Scroll Viewを広げることによって内包されているText Viewもウィンドウ一杯まで広がったことになります。
図MyClip - Scroll View編集2
以上で本節の作業はすべて終わりました。MainMenu.xibファイルを保存してプロジェクトウィンドウに戻り「ビルドして進行」をクリックしてください。ビルドが終わりましたら色々とテストしてみてください。正常に動作しているかどうかはプロジェクトウィンドウをクリックしてXcodeに戻り「実行」メニューから「コンソール」を選んでコンソールを表示させます。そして再びMyClipをクリックして色々と行ってみてください。コンソールに警告が出ないようであれば無事に作業は完了しています。お疲れ様でした。ここまでの段階のバックアップをとっておかれることをおすすめします。
Windowsなどではアプリケーションの最後のメインウィンドウを閉じるとそのアプリケーションは終了します。しかしMacでは一部のアプリケーションを除いてアプリケーションの最後のメインウィンドウを閉じてもそのアプリケーションは終了しないのがデフォルトの振る舞いになっています。しかしMyClipのように一つのメインウィンドウズしか持たず、しかも一度閉じたウィンドウを再び開くコマンドを持たないアプリケーションではメインウィンドウを閉じるとアプリケーションも終了するようにしたほうが良いでしょう。
9.
2. 1 メインウィンドウとキーウィンドウ
●メインウィンドウ
前述では「ウィンドウを閉じると」とは言わずに「メインウィンドウを閉じると」と言いました。ひとつのアプリケーションには通常さまざまな役割をするウィンドウが複数あります。その複数のウィンドウを大きく分けると「メインウィンドウ」と「そのほかのウィンドウ」に分かれます。もしあなたがグラフィックアプリケーションを使用したことがあればこのことについての一番分かりやすい例になると思います。
グラフィックアプリケーションには実際に絵を描くウィンドウがあります。そのほかにも円を描くのか四角を描くのか、円や四角は周りの線だけを描くのか中も塗りつぶすのか、色は何色にするのかなどを指定する一般的にツールパレットと呼ばれるウィンドウもあります。この場合、実際に絵を描いているウィンドウがメインウィンドウになりツールパレットはそのほかのウィンドウになります。そしてメインウィンドウを閉じるとそのときに描いているグラフィックのデータも閉じられることになります。それに対してツールパレットを閉じてもメインウィンドウは開いたままで描いているデータも閉じられることはありません。
さらに例を出します。Mac付属のテキストエディットではテキスト書類を表示しているウィンドウが「メインウィンドウ」になります。しかしフォントパネルやカラーパネルを表示することもできます。また環境設定パネルや「テキストエディットについて」という通常アバウトボックスと呼ばれるウィンドウそしてさらにヘルプウィンドウも表示できます。これらはすべて「そのほかのウィンドウ」になります。
※パレットやパネルと呼ばれるものもウィンドウの仲間です。
グラフィックアプリケーションやテキストエディットの例ではどれがメンウィンドウになるかは分かりやすかったと思います。これらは一般的に「作業の対象となるウィンドウがメインウィンドウです」と説明されています。ではXcodeではどれがメインウィンドウになるでしょうか。答えはプロジェクトウィンドウです。コードを記述するエディタやコンソールウィンドウを閉じてもプロジェクトは閉じません。しかしプロジェクトウィンドウを閉じるとそのプロジェクトは閉じられエディタやコンソールウィンドウも閉じられます。
さらに難しくなりますがInterface Builderではメインウィンドウはどれになるでしょうか。答えはMainMenu.xib
(English)ウィンドウです。インペクタパネル、ライブラリパネル、MainMenuウィンドウ(バー)さらにはややこしい話しになりますがメインウィンドウと呼ばれているウィンドウを閉じてもxibファイルが閉じることはありません。しかしMainMenu.xib
(English)ウィンドウを閉じるとMainMenuウィンドウ、メインウィンドウ も一緒に閉じてしまいます。ただしインスペクタパネルとライブラリパネルは残ります。
なおInterface Builderでメインとなるウィンドウをメインウィンドウと呼んでいるのは、その作成中のアプリケーションが将来コンパイルされて本当のアプリケーションになったときに「メインウィンドウになる」という意味でメインウィンドウと呼んでいるだけです。
このように「そのウィンドウを閉じるとそのファイルも閉じてしまうウィンドウがメインウィンドウです」という見分け方もできます。実際に「作業の対象となるウィンドウがメインウィンドウです」という説明では次に出てくるキーウィンドウの説明と混同してしまう場合があります。
●キーウィンドウ
アプリケーションの中で現在作業をしているウィンドウが「キーウィンドウ」になります。例えばツールパレットでツールを選んでいる時にはツールパレットがキーウィンドウになります。またヘルプウィンドウでヘルプを検索しているときにはヘルプウィンドウがキーウィンドになります。つまりそのときどきにキーボードからのキー入力を受け付けるウィンドウがそのときのキーウィンドウということになります。したがってキーウィンドウはアプリケーションに含まれているすべてのウィンドウが成り得る可能性があります。
しかしファンクションキーやショートカットキーは各ウィンドウに関係なくアプリケーション全体、あるいはシステム全体に働きかけることが多いのでキーウィンドウを定義する場合の「キー入力」の例外と考えてください。キー入力については「文字入力を受け付けるウィンドウ」と考えていただければ理解しやすいと思います。
ただし例えばグラフィックアプリケーションを例にするとツールパレットには文字入力するフィールドがない場合がほとんどだと思います。しかしツールパレットでツールを選んでいるときはツールパレットがキーウィンドウになっています。キーウィンドウとキー入力(文字入力)を結びつけるのは理解するための「例え」のようなものだと思ってください。
9. 2. 2 メインウィンドウのデリゲートを実装する
NSWindow Class Referenceを見てみるとTasksの最後のほうに「Closing Windows」というグループがあります。このなかに次の2つデリゲートメソッドがあります。デリゲートメソッドの右横には「delegate method」とちゃんと書かれていますのですぐに分かると思います。
- windowShouldClose:
- windowWillClose:
デリゲートメソッドを使う理由はサブクラス化せずに実装を追加することができるというです。デリゲートメソッドは「私はこのタイミングこれをするけどその間にあなたがしたいことがあったらやってくださいね」という自分自身のやるべき実装を持っていてさらにプログラマがやってもらいたい実装も追加できるトリガーメソッドになっています。そしてもうひとつのデリゲートメソッドの利点は必要なデリゲートメソッドにだけ実装を行えば良いということです。不必要なデリゲートメソッドは何もせずにほっておけば良いのです。ところでNSWindowクラスには本当に「ウィンドウを閉じた時にはアプリケーションも終了する」という機能はないのでしょうか。答えは「ありません」となります。あきらめて自分で実装することにしましょう。今回は「ウィンドウを閉じた時にMyClipも終了する」という実装を追加することになります。なお「Closing
Windows」グループのほかのメソッドには実装を追加することはできません。これらのメソッドの中にアプリケーションを終了するという実装を追加しようとすればNSWindowを継承してサブクラスをつくらなければならなくなりまた堂々巡りが始まります。そろそろ読者も「デリゲート使うのが当たり前じゃないか」という気分になってきませんか。
●デリゲートメソッドのパターン
前述の2つのメソッドクリックして詳細ページを見ていただければ分かりますがwindowShouldClose:は戻り値としてBOOL型、つまりYESかNOを返します。それに対してwindowWillClose:は戻り値を返しません。windowShouldCloseのShouldは婉曲的に許可を求めるshouldになっています。YESが返ってくればウィンドウは閉じられNOが返ってくればウィンドウが閉じられることが中止されます。windowWillCloseのwillは単純未来というよりは固い意思未来ななっています。このメソッドが実行されればそのウィンドウは必ず閉じられます。ただしウィンドウが閉じられる時にアプリケーションにして欲しい処理をプログラマが実装することができます。
このShouldとWillをメソッド名に含めたデリゲートメソッドは各クラスに多く存在しています。そしてそれぞれのデリゲートメソッドの中での意味も同じです。
そのほかにもすでに使っているtextDidChange:デリゲートメソッドのように何かが終わったことを表すDidもよく使われます。そしてそのほかにもデリゲートメソッド名はほとんどパターン化されていますが、ここでの説明は省略させてもらいます。
●Controller.mの変更
今回もメインウィンドウのデリゲート先をController.mに設定いたします。デリゲートの接続は接続先がオブジェクトでさえあれば、まえもって準備しておくことは何もありません。しかし今回は接続前にController.mにデリゲートメソッドの実装をすませてしまいましょう。
またtextDidChange:デリゲートメソッドとwriteToFile:メソッドの中で次のようなメッセージ式を送信しています。
[[textView window] setDocumentEdited:○○];
このなかの [textView window] メッセージ式では、textViewインスタン変数に接続されているText Viewオブジェクトの所属している(載っている)ウィンドウ、つまりメインウィンドウを取得していることは前にも申し上げました。しかしControllerオブジェクト自体がメインウィンドウへのアウトレットを持てばこの
[textVew window] というメッセージ式は省くことできます。そしてメッセージ式全体として次のようにすることができます。
[window setDocumentEdited:○○];
このように [textView window] というメッセージ式をひとつ省くとそれだけCPUへの負荷が減らせることもすでに説明しました。実際にtextDidChage:メソッドとwriteToFile:メソッドは呼び出し回数も多いのです。この部分を前述のようにメッセージ式をひとうでも減らせれば「リソースの節約」あるいは「コストの削減」効果は大きいだろうと思います。
なお [textView window] のwindowというメソッドを実際に持っているのはウィンドウに表示されているオブジェクトのひとつ上か2つ上のスーパークラスにあたるNSViewであることを参考まで言っておきます。
では以上のことを含めてコーディングしていきます。NSWindowのデリゲートメソッドとしてまずwindowWillClose:を使います。このメソッドの記述場所は@implementation Controllerと@endの間であればどこでもかまいませんが、Contoroller.mファイルの見た目をまとめるという意味でtextDidChange:メソッドの次が良いでしょう。結果としてController.mは次のように変更されます。強調箇所が追加・変更された箇所です。textDidChange:メソッドからwriteToFile:メソッドまでを掲載します。そのほかの箇所に変更はありません。
Controller.m
- (void)textDidChange:(NSNotification
*)aNotification
{
[model setString:[[textView string] copy]];
[window setDocumentEdited:YES];
}
- (void)windowWillClose:(NSNotification
*)notification
{
if ([window isDocumentEdited])
[self writeToFile:self];
[NSApp terminate:window];
}
- (void)readFromFile
{
[model setString:[[NSString stringWithContentsOfFile:filePath
encoding:4 error:nil] copy]];
}
- (IBAction)writeToFile:(id)sender
{
[[model string] writeToFile:filePath
atomically:YES encoding:4
error:NULL];
[window setDocumentEdited:NO];
}
●コード説明
textDidChange:メソッドとwriteToFile:メソッドの変更箇所についてはすでにお分かりいただいていると思います。新しく追加したNSWindowのwindowWillChange:デリゲートメソッドにはNSNotification型の引数がついていますが、この引数は使わずに放置しています。NSNotifaicationについては実践編で説明されています。このメソッドの実装では、まずif文でwindowアウトレットにisDocumentEditedメッセージを送っています。
if ([window isDocumentEdited])
このisDocumentEditedを送るとsetDocumentEditedで設定された値が返されます。つまり未保存のデータがあればYESが返されて、未保存のデータがなければNOが返されます。YESが返されればif文の条件式がtrue(真)と評価されて次の式が実行されます。
[self writeToFile:self];
Controllerオブジェクト自身に定義されているデータ書き出しメソッドのwriteToFile:を呼び出してText Viewのデータを保存しています。引数にはメソッドの送り手のオブジェクトを指定しなければならないのでここでもselfを使っています。
なおif文にもコストがかかります。したがってif文を使わずに必ず [self writeToFile:self] が実行されるようにしてもかまいません。しかしこのif文が実行されるのはアプリケーション終了時の1回のみです。それに対してwriteToFile:メソッドはデータがすでに保存されているかどうかに関わらずにデータを書き出します。そしてText Viewに記入されている文字数が多数である場合もあり得ます。ここは安全策をとってif文でデータ保存の必要性の有無を確認したほうがコスト的にもパフォーマンス(実行速度)的にも良い選択だと思います。
そして次がアプリケーションを終了させる式です。
[NSApp terminate:window];
Cocoaではアプリケーション、つまりGUIを持つプログラムはプログラマが記述しなくてもNSApplicationというアプリケーション全体を管理するクラスのインスタンスが自動的に作成されています。アプリケーションが持つNSApplicationのインスタンスはひとつだけと決まっています。そしてそのインスタンスを表すグローバル変数NSAppが自動的に作成されています。そのNSAppにアプリケーションを終了させるメソッドterminate:を送っています。正式なシグネチャは次のとおりになっています。
-
(void)terminate:(id)sender
引数には送り手のオブジェクトを指定することになっていますので一応windowを渡しましたがselfでもかまいません。
●Controller.hの変更
次にController.hも変更します。強調箇所が追加されています。
Controller.h
#import <Cocoa/Cocoa.h>
#import "Model.h"
@interface Controller : NSObject {
Model *model;
IBOutlet NSTextView *textView;
IBOutlet NSWindow *window;
}
- (void)readFromFile;
- (IBAction)writeToFile:(id)sender;
@end
NSWindow型のインスタン変数windowをInterface Builderからも認識できるIBOutletとして宣言しています。
9.
2. 3 Interface Builderでの接続作業
●MainMenu.xibの変更
プロジェクトウィンドウでMainMenu.xibをダブルクリックで開いてください。
MainMenu.xibウィンドウのControllerからWindow(MyClip)へ接続線を引きます。図のよう現れる接続先が「Window (MyClip)」であることは必ず確認してください。
図:Controller-windowアウトレットの接続 1
もうひとつの接続方法として同じMainMenu.xibウィンドウ内の「Window(MyClip)」に接続してもらっても構いません。デスクトップに現れているMyClipウィンドウもxibファイル内のWindow(MyClip)も同じものを表しています。
図Controller-windowアウトレットの接続2
どちらの方法でもWindow(To Do)が強調表示されましたらマウスボタンを離します。そして次に現れるOutletsの一覧からwindowをクリックして接続を完了させます。
図:Controller-windowアウトレットの接続 3
次は逆にWindow(MyClip)からControllerへ接続線を引きます。
図:Windowオブジェクトのdelegate接続 1
Controllerが強調表示されましたらマウスボタンを離します。次に現れるOutletsの一覧からdelegateをクリックして接続を完了させます。
図:Windowオブジェクトのdelegate接続 2
以上でControllerオブジェクトのwindowアウトレットの接続とメインウィンドウのdelegateの接続が完了しました。接続を確認するにはMainMenu.xibでControllerを右クリックすると接続関係の一覧が表示されます。一覧のなかの各接続先にマウスポインタを乗せると対応するオブジェクトがハイライト表示されます。
「multiple」となっているところは複数のオブジェクトから接続されているところです。左側のディスクロージャー(三角形)をクリックして下向きの三角形に変えると個別の接続先が表示されます。
図:Controllerオブジェクトの接続一覧
これでInterface Builderでの作業は終わりました。作業を保存してXcodeに戻ってください。
●実行
プロジェクトウィンドウで「ビルドして進行」をクリックします。なお次回からはたんに「コンパイルしてください」もしくは「ビルドしてくださ」と表現することもあります。MyClipウィンドウが起動しましたら色々と試してください
。クローズボタンを押すとMyClipも終了します。未保存データが残っていた場合は強制的に保存して終了することになります。またウィンドウのクローズボタンを押す代わりに「File 」メニューの「Close」を選ぶか command + Wでもウィンドウは閉じられデータは保存されアプリケーションは終了します。
便利なことに「MyClip」メニューから「Quit New Application」を選ぶかcommand + Qでアプリケーションを終了させた場合も未保存データは強制的に保存されます。これはアプリケーション終了に先立ってまずウィンドウを閉じているからです。
Column クローズボタンを無効にするという方法もあります。
メインウィンドウのクローズボタンをクリックしてもアプリケーションが終了しないのはMac独特の振る舞い(動き)です。このMac独特の振る舞いはメインウィンドウをいくつも表示できるドキュメントベースドアプリケーション(のちの章で説明いたします)のような場合には問題ないのですが、メインウィンドウをひとつしか持たないMyClipのようなアプリケーションにとっては都合の悪い問題です。
しかしこの問題の解決策としてもっと簡単にクローズボックスを無効化してしまうという方法をとることも有効な手段だと思います。方法は至って簡単です。
・MainMenu.xibファイルを開いてInterface Builderでメインウィンドウを選択します。
・インスペクタパネルのタイトルがWindow ○○になっていることを確認してください。
・次にインスペクタパネルのAttributesタブを選びます
・そしてインスペクタパネルのControlsグループの「Close」の左横のチェックを外します。
これでメインウィンドウのクローズボタンは使えなくなり、アプリケーションを終了するには「MyClip」メニューの「Quit New Application」を選ぶかcommand + Qを押すというMacとしての通常の方法しか使えなくなります。なお「MyClip」メニューのようにそのアプリケーションの名前になっているメニューのことを「Applicationメニュー」と呼びます。
図:Window Attributes Close
ではMainMene.xibを保存してXcodeに戻ります。そしてMyClipをビルドしてください。メインウィンドウが起動しましたら色々と試してみてください。クローズボタンは使えなくなっています。「File」メニューの「Close」も選べなくなっていますしショートカットキーの cmmand + Wも使えなくなっています。しかしアプリケーション終了時には未保存のデータがあれば強制的に保存される機能はそのまま有効に働いています。
アプリケーションは終了する時に「では、お先に!」と言ってさっさとに終了してしまうわけではありません。アプリケーションの後片付けをしてから終了することになっています。つまりアプリケーションを終了する前に表示されているMyClipウィンドウを片付けよう(閉じよう)とします。そしてその結果windowWillCloseメッセージがdelegateに送信されて未保存のデータがあれば保存するという一連の作業が行われることになります。
しかしこれはたまたま「棚からぼた餅」的にこうなってしまっただけです。こうなることを予測してコーディングを決めたわけではありません。ウィンドウのクローズ機能を無効にしたのであれば、アプリケーション終了時に未保存データを保存するように変更するべきでしょう。しかしMyClipではクローズボタンを無効にする予定はありません。このコラムでは「こういう方法もあります」という紹介をするために一時的にクローズボタンを無効にしているだけです。
しかしアプリケーション自身が終了時に未保存データを保存するというコーディングも知っておくべきでしょう。そこでこの基礎編の「補足資料A」で「NSApplicationのデリゲート」というタイトルでアプリケーション自身が終了時に未保存データを保存する方法を説明しています。興味があればご覧になってください。
お疲れ様でした。これで本章は終わります。
テストのためにCloseボタンを無効していた設定を有効に戻してMainMenu.xibファイルを保存してください。そしてこの節の結果としてバックアップをとられることをおすすめします。私の場合は「MyClip 8.2」としました。次章からはいつものとおり「MyClip」プロジェクトフォルダのMyClip.xcodeprojで作業を続けていきます。
This site is available in Safari and Snow Leopard. | (c) viva Cocoa 2006 - 2010 |