Document-based アプリケーションを作る

ここには、NSDocument を使った Document-based アプリケーションの作り方をメモする。

まだいじり始めたばかりなので、間違っているところとか、もっといい方法があるかもしれないけど、とりあえず忘れないうちに。

リッチテキストとプレインテキストファイルを扱えるテキストエディタもどきを作ってみる。実は、この後バインディングを使った方法を少し理解したけど、これを手直しするのが面倒なので、別のページにメモすることにした。Document-based アプリケーションを作る その2に書いてあるのでそちらを参照。プレインテキストの文字コードを選択させるためなど、文書を開くときの Open Panel をいじる方法は、Document-based アプリケーションを作る その3にメモした。応用的なものなどはこちらにメモを加えていくつもり

まず、Xcode の新規プロジェクトで、Application から Cocoa-Ruby Document-based Application を選ぶ。

このプロジェクトには、Cocoa-Ruby Application と違って、最初から MyDocument.rb という NSDocument のサブクラスファイルと MyDocument.nib という nib ファイルもある。さらに Credits.rtf なんてのもあるけど、それはほっておく。

これだけで即実行すると、次のようなウィンドウを持つアプリケーションとして動く。

MyDocument.rb ファイルの中身を見てみると、コメントを除くととてもシンプルなものになっている。

まず最初に、テンプレートに一部変更を加える。そのままでも一応動くけど、Interface Builder との連携が変わったらしく、IB 上の MyDocument.nib で window という outlet が見つからない、と出る。実際は、つなぎ変えたりしなければそのまま動くが、何か嫌なので、変更する。もし、使っている Xcode のバージョンが古ければ、この問題は起こらないかもしれない。

変更は次の通り、require 'osx/cocoa' の次に、include OSX を追加する。そして、クラス継承の OSX::NSDocumentNSDocument にする。

require 'osx/cocoa'

include OSX

class MyDocument < NSDocument

これは、何をしているかというと、最新の Interface Builder では、RubyCocoa のクラス継承をそのまま読み込むらしく、デフォルトのままだと、MyDocument は OSX のサブクラスだとして認識されてしまう(あくまでも、Interface Builder 上の問題)。

さて、上のスクリプトのメソッドの簡単な説明。

windowNibName は開く nib ファイルを指定している。デフォルトの MyDocument を変えなければ、ここはそのままでいい。

windowControllerDidLoadNib(aController) は NSObject の awakeFromNib みたいなもので、nib ファイルが読み込まれた後に呼ばれる。

dataRepresentationOfType(aType) は文書を保存するためのもの、loadDataRepresentation_ofType(data, aType) は文書を読み込むためのもの。ただ、デベロッパドキュメントの Core Library を見ると両方とも Deprecated in Mac OS X v10.4 なんて書いてある。これでも動くけど、なんか嫌なので、後で、これにしろと書いてあるのにする。

文書を保存する方法は、そのままそこに記述してもいいけど、読み込むの方は読み込んだものを表示させる先が nib ファイルが読み込み終わった後じゃないと準備されないので、windowControllerDidLoadNib に書く。まあ、何らかのオブジェクトに読み込んで、表示の設定だけそこですればいいのかもしれないけど。

とりあえず、リッチテキストとプレインテキストを扱いたいので、スクリプトをいじる前に Xcode で設定する。ターゲットを開いて中のアプリケーション名がついたターゲットを右クリックして情報を見るを選ぶ。

すると、情報ウィンドウが開くので、プロパティを選ぶ。

下の方に、書類のタイプというのがあるので、ここに扱いたい書類のタイプを加えていく。

それぞれに名前をつけて拡張子と OS タイプにそれぞれ rtf、txt と RTF、TEXT と入れて、クラスは MyDocument にする。タイプを保存は、最初のが 2 進になっていたのでそうしたけど、他でもいいのかもしれない。

名前はファイルを保存するときにでる Save パネルにどちらのタイプで保存するか選択できるポップアップボタンが自動で作られて、そのボタンのアイテムのタイトルになるので、そこで表示したい名前にする。こんな感じ。

できたら、これで閉じる。

次に、MainMenu.nib をダブルクリックして Interface Builder で開いて、メニューアイテムの File -> NewOpenFirst Responder と結ぶ。

こうすることで、New を選ぶと新しい文書ウィンドウが Untitled # というタイトルで開き、Open を選ぶと、ファイルを開く処理に入る。

後もう一つ。デフォルトでの Format のメニューアイテムは貧弱なので、リッチテキスト文書を扱うために Library にある Format のメニューアイテムと置き換える。

こうすると、MainMenu.nib に Font Manager が追加されて、Format がテキストエディット.app にあるようなものになる。

今度は、MyDocument.nib ファイルを開く。

一つだけあるウィンドウには Text Field があって Your document contents here と書いてあるので、これを消して Text Field を配置する。

あと、Inspector で Attributes と View の設定もする。Attributes では Undo と Non-contigious Layout にチェックを入れる。後者はファイルがでかくないとあんま意味ないかもしれないけど、Undo にチェックを入れると、自動で Undo をしてくれるので便利。

Size は Autosizing でウィンドウを大きくしたときの追随して大きさが変わるようにする。

あと、デフォルトで入っているテキストは消しておく。

ここまで準備したら、スクリプトの方へ。

ここでは、ファイルの内容を NSAttributedString もしくは、NSString として読み込んで、それを NSTextView に setString で表示させる方法をとっている。これを書いた後に、バインディングを使って表示させる方法を理解したので、それはDocument-based アプリケーションを作る その2にまとめてみた。そちらも参照してください。

まずは、@textView という Text View の outlet を作って、MyDocument.nib で結びつける。普通のアプリケーションとは違って、File's Owner を右クリックすると MyDocument.rb に作った outlet が現れるので、それを結びつける。

さて、読み込むためのスクリプトだけど、上で書いたように loadDataRepresentation_ofType は depricated なので、readFromURL_ofType_error(url,type,error) を使え、と書いてあるので、そうしてみる。 ここでは処理をさせないので、メソッドの中身は return true(true だけでもいい)のままにしておく。変えなくても動くのでそのままでもいいと思うけど、一応。

def readFromURL_ofType_error(url,type,error)

return true

end

つぎに、windowControllerDidLoadNib(aController) に読み込む処理を書いていく。self.fileName でファイル名が self.fileURL で URL が得られる。どちらの場合もフルパスになっている。さらに self.fileType でリッチテキストとプレインテキストのどちらが開かれようとしているのかが返ってくる。返ってくる名前は、上に書いたように Xcode のターゲットの情報のところで書類のタイプにつけた名前になる。これで、どちらのファイルタイプで読み込むか判断してそれぞれの処理を当てる。あともう一つ、新規の書類が開いたときは、ファイル名が nil なので、それを判断して新規書類のときの処理を書く。新規の書類のファイルタイプは書類のタイプに付けた名前で一番上にあるものになるので、ここではリッチテキストになる。というわけで、デフォルトでは判断なしでリッチテキストになる設定にする。

リッチテキストファイルは NSTextView に readRTFDFromFile(path) で直接読み込む方法をとって、プレインテキストファイルは NSStringstringWithContentsOfFile_encoding_error(path,encoding,error) を使って読み込んだテキストを setString(string) で Text View に入れてる。error は Ruby のエラーで帰ってくる以上の情報が得たい場合でなければ、nil にしておく。ついでに setRulerVisible(true) でリッチテキストときの Ruler の表示を setRichText(false) でプレインテキストの時のリッチテキストの表示を切っている。あと、プレインテキストの文字コードは UTF-8 にして NSUTF8StringEncoding を設定している。このあたりを変更したい場合は、環境設定パネルを作って、そこでデフォルトの文字コードを指定するとかする。

def windowControllerDidLoadNib(aController)

super_windowControllerDidLoadNib(aController)

if self.fileName

if self.fileType == "リッチテキスト"

@textView.setRulerVisible(true)

@textView.readRTFDFromFile(self.fileName)

elsif self.fileType == "プレインテキスト"

@textView.setRichText(false)

@textView.setString(NSString.stringWithContentsOfFile_encoding_error(self.fileName,NSUTF8StringEncoding,nil))

end

else

@textView.setRulerVisible(true)

end

end

さて、読み込み部分はこれで大丈夫なはずなので、次は保存。

dataRepresentationOfType(aType) のままでもいいと思うけど、dataOfType_error(type,error) にしなさいと書いてあるのでそうする。

ここでは、type で Save パネルでポップアップボタンで選ばれた方の書類のタイプが返ってくるので、それに対応した処理を書く。dataOfType_error なので、ここで返すのは NSData にする(return で指定するけど、return は省略可能)。リッチテキストには RTFFromRange(range) をプレインテキストには string で取り出してから NSStringdataUsingEncoding(encoding) を使う。文字コードを指定したい場合は、Save パネルに Accessory View で文字コードを選べる PopUpView を追加する。その方法はこちらを。

def dataOfType_error(type,error)

if type == "リッチテキスト"

return @textView.RTFFromRange([0,@textView.string.length])

elsif type == "プレインテキスト"

return @textView.string.dataUsingEncoding(NSUTF8StringEncoding)

end

end

あと、メニューには Revert To Saved というのがあって、必要なければメニューから消す。使いたければ、revertToContentsOfURL_ofType_error(url,type,error) というメソッドを加える。url で返ってくるので、読み込むときと同じ処理にするには .path を付けて path に変換する。

def revertToContentsOfURL_ofType_error(url,type,error)

if type == "リッチテキスト"

@textView.readRTFDFromFile(url.path)

elsif type == "プレインテキスト"

error = NSError.new

@textView.setString(NSString.stringWithContentsOfFile_encoding_error(url.path,NSUTF8StringEncoding,error))

end

end

これで、簡単なテキストエディタの出来上がり。

最後に、ここで書いたスクリプトのまとめ。

require 'osx/cocoa'

include OSX

class MyDocument < NSDocument

ib_outlets :textView

def windowNibName

return "MyDocument"

end

def windowControllerDidLoadNib(aController)

super_windowControllerDidLoadNib(aController)

if self.fileName

if self.fileType == "リッチテキスト"

@textView.setRulerVisible(true)

@textView.readRTFDFromFile(self.fileName)

elsif self.fileType == "プレインテキスト"

@textView.setRichText(false)

@textView.setString(NSString.stringWithContentsOfFile_encoding_error(self.fileName,NSUTF8StringEncoding,nil))

end

else

@textView.setRulerVisible(true)

end

end

def dataOfType_error(type,error)

if type == "リッチテキスト"

return @textView.RTFFromRange([0,@textView.string.length])

elsif type == "プレインテキスト"

return @textView.string.dataUsingEncoding(NSUTF8StringEncoding)

end

end

def readFromURL_ofType_error(url,type,error)

return true

end

def revertToContentsOfURL_ofType_error(url,type,error)

if type == "リッチテキスト"

@textView.readRTFDFromFile(url.path)

elsif type == "プレインテキスト"

error = NSError.new

@textView.setString(NSString.stringWithContentsOfFile_encoding_error(url.path,NSUTF8StringEncoding,error))

end

end

end