(2008/11/17 記)
いよいよ手元の Cocoa アプリの移植作業に入った。このアプリは、いずれクロスプラットフォームにすることを最初から目論んでいたので、主要なロジックは Plain C で書き、Cocoa 依存の部分だけ Objective-C で書くようにしていた。以前に(2006年2月)日記に書いたクロスプラットフォーム開発に関する議論が頭にあったからこういう設計にしたのだと思う。コメントを見ると2006年3月に作り始めているし。実際移植作業をしてみると、この方針は間違ってなかったようで、Plain C で書いた部分はほんのわずかな修正だけで使い回しできている。
ドキュメントベースのアプリケーションでは、ドキュメント周りの設計がやはり中心になるので、まずはここから手をつけることにした。
wxDocument
では、ドキュメントの内部表現を実装し、ファイルの読み書き・ドキュメントウィンドウの管理・編集作業(undo サポートを含む)・ペーストボードの読み書きなどを受け持つ。
Xcode で「ドキュメントベースのアプリケーション」テンプレートを開くと、NSDocument
の サブクラスで loadDataRepresentation:ofType:
と dataRepresentationOfType:
(10.4 以降ではそれぞれ readFromData:ofType:error:
と dataOfType:error:
)をオーバーライドするようになっている。しかし、これらのメソッドは NSData
との読み書きになっており、実際にファイルの読み書きを Plain C で書くには少々都合が悪い。僕のスタイルでは、代わりに readFromFile:ofType:
, writeToFile:ofType:
をオーバーライドして、こんな風に実装している。
- (BOOL)readFromFile:(NSString *)fileName ofType:(NSString *)docType { const char *fname; fname = [fileName fileSystemRepresentation]; /* molecule は MyDocument のインスタンス変数で、データの内部表現 */ molecule = MoleculeNewFromFile(fname); /* Plain C で書かれた関数を呼び出す */ if (molecule == NULL) return NO; else return YES; } - (BOOL)writeToFile:(NSString *)fileName ofType:(NSString *)docType { char buf[128]; const char *fname = [fileName fileSystemRepresentation]; int retval = MoleculeWriteToFile(molecule, fname); /* Plain C 関数 */ if (retval != 0) return NO; else return YES; }
これを wxDocument
で実装しようと思って、wxWidgets のドキュメント(ややこしいけど、これはリファレンスマニュアルのことね)をいろいろひっくり返して見てみたが、ちょうど readFromFile:ofType:
, writeToFile:ofType:
に対応するメソッドがなかなか見つからない。付属サンプルでは wxDocument::LoadObject, wxDocument::SaveObject
をオーバーライドしているが、これは引数が wxInputStream, wxOutputStream
なので、Plain C の関数とは相性が悪い。ドキュメントには wxDocument::OnOpenDocument, wxDocument::OnSaveDocument
という関数も載っているけど、これをオーバーライドするとエラー処理も全部自前で実装しないといけない。この間のメソッドはないんかいな、と wxWidgets のソースを調べてみると、こんなのがありました。
virtual bool DoSaveDocument(const wxString& file); virtual bool DoOpenDocument(const wxString& file);
ドキュメントされてないメソッドだけど、まあこのあたりの仕様が変化することはあまりなかろう、というわけで、これらをオーバーライドすることにする。上の Objective-C での実装をそのまま移植すると、だいたいこんな感じになる。
bool MyDocument::DoOpenDocument(const wxString& file) { const char *fname = file.mb_str(wxConvFile); /* molecule は MyDocument のインスタンス変数で、データの内部表現 */ molecule = MoleculeNewFromFile(fname); /* Plain C で書かれた関数を呼び出す */ if (molecule == NULL) return false; else return true; } bool MyDocument::DoSaveDocument(const wxString& file) { const char *fname = file.mb_str(wxConvFile); int retval = MoleculeWriteToFile(molecule, fname); /* Plain C 関数 */ if (retval != 0) return false; else return true; }
wxString::mb_str
は、wxMBConv
を引数にとり、その引数が指定するエンコーディングで wxString
をバイト列に変換する。NSString
の関数と対比させると次のようになる。(ws, ns, cs
はそれぞれ wxString
, NSString *
, const char *
型とする)
NSString メソッド(戻り値の型) |
wxString メソッド(戻り値の型) |
機能 |
---|---|---|
[ns fileSystemRepresentation] |
ws.mb_str(wxConvFile) |
(システム依存の)ファイル名エンコーディングで C 文字列に変換する |
[ns UTF8String] |
ws.mb_str(wxConvUTF8) |
UTF8 エンコーディングで C 文字列に変換する |
[NSString stringWithUTF8String: cs] |
wxString ws(cs, wxConvUTF8) |
UTF8 エンコーディングで C 文字列から変換する |
(const wxCharBuffer)
型は (const char *)
型にキャストして使うことができる。const char *
型の変数に代入したり、プロトタイプで引数型が const char *
と指定されている関数に渡したりする時は暗黙の型変換が行われるが、たとえば printf
の引数にするときは明示的にキャストが必要。
実はメモリ管理がどうなっているのか今ひとつよく理解していない。wx.mb_str(wxConvUTF8)
で得たポインタは、元の wx
が delete
されたらたぶん無効になってしまうんだろうから、必要に応じて strncpy
などでコピーする。ソースを読んでちゃんと理解しておいた方がいいんだろうけど、C++ はどうも苦手でねぇ…
デバッグ用に wxString
をコンソールに出力しようとして、ちょっと苦労した。要するに NSLog("%@", ns)
みたいなことをやりたくて、C++ だから cout
でしょ、なんて思って cout << ws
とか cout << ws.c_str()
とかやってみたけど、単にポインタの値が表示されるだけでダメ。正解は printf("%s", (const char *)ws.mb_str(wxConvUTF8))
。上に書いた通り、(const char *)
への明示的なキャストは必須。cout << object
みたいな簡便記法、あってもよさそうなんだけど…
NSDocument
のサブクラスでは、ウィンドウ(wxWidgets 流に言うと「フレーム」のことね)を開くために windowNibName
か makeWindowControllers
のどちらかをオーバーライドする。僕のスタイルでは、NSWindowController
のサブクラスを作って、ウィンドウ内の GUI 部品の動作とドキュメントの内部データの橋渡しをほとんどすべてこのサブクラス中で記述している。この場合は、makeWindowControllers
をオーバーライドして、例えば次のように書くことになる。
- (void)makeWindowControllers { id controller; controller = [[[MyWindowController allocWithZone:[self zone]] init] autorelease]; [self addWindowController:controller]; [[controller window] makeKeyAndOrderFront: self]; /* mainWindowController は MyDocument のインスタンス変数。 */ mainWindowController = controller; }
mainWindowController
に相当するポインタは [[self windowControllers] objectAtIndex: 0]
で参照できるので、別にインスタンス変数として保持しておく必要はないのだが、わりと頻繁に参照するので保持しておくと便利。なお、リファレンスカウントを持っておく必要はないので、autorelease
してある。一般的には autorelease
したポインタをインスタンス変数に保持するとあとで痛い目にあうので、よいこはマネをしないように。
wxWidgets のスタイルでは、このようなコードは wxDocument
のサブクラスでは書かなくてよい。そのかわり、wxApp
のサブクラスの OnInit()
の中で、こんなコードを書く。
m_docManager = new wxDocManager; (void) new wxDocTemplate((wxDocManager *) m_docManager, _T("Molecule"), _T("*.*"), _T(""), _T(""), _T("Molecule Doc"), _T("Molecule View"), CLASSINFO(MyDocument), CLASSINFO(MoleculeView));
wxDocManager
は、Cocoa の NSDocumentController
にだいたい対応している。wxDocTemplate
は、ファイルタイプ (*.*)
・ドキュメントタイプ (MyDocument)
・ウィンドウタイプ (MoleculeView)
を関連づけている。MoleculeView
というのは wxView
のサブクラスで、前にも書いたが NSWindowController
(のサブクラス)に対応する。
上のように、アプリケーションの初期化の時に wxDocTemplate
のインスタンスを作って wxDocManager
に登録しておくと、ファイルオープンのダイアログでファイルタイプを選んで、そのタイプのドキュメントとして開くことができるようになる。
まあ、なんだな、こういう「なんとか (*.*)」みたいな表示がファイルオープンダイアログに出てくると、「なんか Windows っぽいし感じ悪い」という雰囲気がただよってくるのが難点と言えば難点だけど、拡張子でファイルタイプを判別するのはもはや Mac の世界でも普通のことになってきているし、実用的ではある。
1つのドキュメントで複数のウィンドウを開く、ということになると、wxDocTemplate
だけでは対応しきれない(と思う)ので、wxDocument::AddView
などを使う。今回はこの機能は使っていないので、この話題はまたの機会に。
さて、こいつがちょっと厄介なんだ。ドキュメントを何か編集するとする。例えば、ドキュメントが Atom という型のオブジェクトの配列であるとして、新しい Atom を指定された位置に追加する、という作業を考えよう。Undo をサポートする場合は、この作業の「逆作業」である「指定された位置の Atom を削除する」作業を同時に定義して、NSUndoManager
のインスタンスに「登録」することになる。コードとしてはこんな風になる。
- (int)addAtom: (Atom *)anAtom atIndex: (int)index { [[[self undoManager] prepareWithInvocationTarget: self] deleteAtomAtIndex: index]; [atoms addObject: anAtom]; } - (Atom *)deleteAtomAtIndex: (int)index { Atom *anAtom = [atoms objectAtIndex: index]; [[[self undoManager] prepareWithInvocationTarget: self] addAtom: anAtom atIndex: index]; [atoms removeObjectAtIndex: index]; }
これを初めて見た時はびっくりしましたね。prepareWithInvocationTarget
の後には、どんなメソッド呼び出しを書いてもいい。メソッド呼び出しと引数が自動的にまとめられて NSInvocation
型のオブジェクトが作られ、それが undo 用にスタックに保持される。Objective-C のランタイムがそういう機能を持っているからこそ実現できる技である。これは C++ では到底真似できるものではない。
で、どうしたか。僕は一応 C++ ベースのアプリケーションフレームワークを使ってみた経験もあるので(まあお遊びレベルだけど…それを言うなら今書いているのだってお遊びレベルだし)、この undo メカニズムは C++ では実現困難だな、ということは認識していた。また、なるべくメインロジックは Plain C で書く、という方針でもあったので、1つ1つの編集作業に対して「アクションオブジェクト」を生成して、それを実行する際に「逆作業」のアクションオブジェクトを MyDocument
の undoManager
に登録する、という風に実装した。
/* アクションオブジェクトのパラメータの型 */ typedef struct MolActionArg { char type; union { int ival; double dval; struct Atom *aval; } u; } MolActionArg; typedef struct MolAction { int refCount; const char *name; int nargs; MolActionArg *args; } MolAction; int MolActionPerform(Molecule *mol, MolAction *action) { /* action の中身に応じて何か作業する */ MolAction *undoAction = ....; /* 「逆作業」オブジェクトを作る */ MolActionRegisterUndo(mol, undoAction); /* 登録する */ MolActionRelease(undoAction); }
以上が Plain C で書いた部分。次に、Objective-C との橋渡し部分。
int MolActionRegisterUndo(Molecule *mol, MolAction *action) { MyDocument *doc = [MyDocument documentFromMolecule: mol]; if (doc != nil) { MyMolActionObject *obj = [[[MyMolActionObject alloc] initWithMolAction: action] autorelease]]; [[doc undoManager] registerUndoWithTarget: doc selector: @selector(performMolAction:) object: obj]; } } /* MolAction を Objective-C オブジェクトとして扱うためのラッパー */ @implementation MyMolActionObject - (id)initWithMolAction: (MolAction *)action { molAction = action; MolActionRetain(molAction); } - (void)dealloc { MolActionRelease(molAction); [super dealloc]; } - (MolAction *)molAction { return molAction; } @end @implementation MyDocument - (void)performMolAction: (MyMolActionObject *)anObject { MolActionPerform([self molecule], [anObject molAction]); } @end
MolAction
というのが「アクションオブジェクト」。Plain C で書いてあるので、明示的に refCount
メンバを持ち、参照数カウントでメモリ管理ができるようにしてある(MolActionRetain()
と MolActionRelease()
; コードは自明なので上には載せていない)。さらに、NSUndoManager
がこのメモリ管理メカニズムを利用できるように、MyMolActionObject
というラッパーオブジェクトを作っている。なお、undo アクションを登録するとき、メソッドが NSObject
のサブクラス1つだけを引数に取る時は、registerUndoWithTarget:selector:object:
メソッドを使うことができる。
まあ、ここまでは Cocoa の話。次はこれをどうやって wxWidgets に持っていくかだ。
前にも書いた通り、wxWidgets での undo メカニズムは、wxCommand
と wxCommandProcessor
から成っている。wxCommand
は「アクションオブジェクト」に相当するものなのだが、ちょっと面倒なのは、Do()
と Undo()
の両方を実装しておかないといけないことだ。Cocoa スタイルの実装だと、Do()
を始めた時点では Undo()
の内容がわかっていなくてもあまり支障はない。たとえば、Atom を挿入する位置は実際挿入してみないとわからない、といった場合、Undo()
に必要な情報「(挿入した)Atom を削除すべき位置」は Do()
が終了して初めて確定する。このような場合、実際の作業の「前に」Do()
と Undo()
の両方を確定させておかないといけない wxWidgets のメカニズムには直接は移行できない。
もう1つ厄介なのは、複数のアクションが組合わさったものがユーザーから見て「1つの作業」になっている場合である。たとえば、「選択した Atom をすべて削除する」という作業を「1つの Atom を削除する」という作業の繰り返しで実装した場合、1つ1つのアクションが1つの redo/undo に対応していたとしたら、ユーザーが undo を実行したとき戸惑うだろう(削除した Atom が1つしか復活しない)。Cocoa の NSUndoManager
は、beginUndoGrouping/endUndoGrouping
というメカニズムを持っていて、イベントループの最初に beginUndoGrouping
, 最後に endUndoGrouping
を(自動的に)呼ぶことによって、「ユーザーから見て1つの作業」が自動的にグループ化されるようになっている。wxCommandProcessor
には、どうもこの機能は備わっていないようで、自前でアクションをグループ化する必要がある。
これを解決するため、こんな実装を考えた。wxCommand
のサブクラスとして MyCommand
を実装するが、これと MyDocument
との連携で、Cocoa と同様の undo メカニズムを実現する。実際の処理の流れを Cocoa 版と wxWidgets 版とで対比させて説明する。
MolAction
は経由せずに、直接編集する。ただし、このとき「逆作業」に対応する MolAction
オブジェクトを同時に作る。(Cocoa 版、wxWidgets 版とも同じ)MolAction
オブジェクト (MolAction *action
とする) を undo スタックに登録する。
action
から MyMolActionObject
オブジェクトを作り、registerUndoWithTarget:selector:object:
で undoManager に登録。action
を MyDocument
内部の一時スタックに保持。一時スタックが空だった時は、wxPostEvent()
を使って自分自身あてにイベントを投げておく。このイベントは、次のイベントループに入る直前に処理される。NSUndoManager
が自動的に処理するため、コーディングは不要。MyCommand
インスタンスを作成する。MyCommand
クラスは MolAction **undoActions
というインスタンス変数を持っており、これまでに MyDocument
の一時スタックに積まれた MolAction
オブジェクトをそのまま保持する。MyCommand
を wxCommandProcessor
に登録するため、Submit()
を呼ぶ。Submit()
の中で MyCommand
の Do()
メソッドが呼ばれるが、もう編集作業は済んでいるので、このメソッドでは何も作業しない。NSUndoManager
が undo スタックから MyMolActionObject
オブジェクトの集まりを取り出し、登録されたのと逆順に performMolAction:
メソッドを実行する。performMolAction:
は、MolAction
オブジェクトが示す編集操作を実施する。このとき、やはり「逆作業」に対応する MolAction
オブジェクトが undoManager に登録される。wxCommandProcessor
が undo スタックから MyCommand
オブジェクトを取り出し、Undo()
メソッドを呼ぶ。このメソッドは、最初に積まれたのと逆順に MolAction
オブジェクトを取り出して、編集操作を実施する。このとき、やはり「逆作業」に対応する MolAction
オブジェクトが MyDocument
内部の一時スタックに積まれる。また、MyDocument
の内部に「現在実行中の MyCommand
」へのポインタを保持しておき、フラグ isUndoing
を true
にしておく。NSUndoManager
が自動的に処理するため、コーディングは不要。たぶん内部に「ただいま undo 中」のフラグを持っていて、undo と redo のスタックのどちらに積むかを判断しているのだろう。MyCommand
」のインスタンス変数 MolAction **redoActions
に保持する。MyCommand
は wxCommandProcessor
によって自動的に redo スタックに移されるので、改めて Submit()
などをする必要はない。MyCommand
の Do()
メソッドが呼ばれるが、これは isUndoing
フラグを false
にする以外は Undo()
とほとんど同じ処理である。一番最初に Submit()
から呼ばれる Do()
redoActions
インスタンスが空かどうかで判断している。
言葉で説明するとえらく長くなるな。コードは下のようになっている。一部単純化してあるのでこれだけでは動かないが、大まかな流れの紹介ということで。まずは MyDocument
から抜粋。
/* イベントループの終わりに作業するためのイベントハンドラ */ const wxEventType MyDocumentEvent = wxNewEventType(); BEGIN_EVENT_TABLE(MyDocument, wxDocument) EVT_COMMAND(MyDocumentEvent_willNeedCleanUndoStack, \ MyDocumentEvent, MyDocument::OnNeedCleanUndoStack) END_EVENT_TABLE() void MyDocument::PushUndoAction(MolAction *action) { /* 自前のスタックに action を積む */ if (countUndoStack == 0) undoStack = (MolAction **)malloc(sizeof(MolAction *) * 8); else undoStack = (MolAction **)realloc(undoStack, sizeof(MolAction *) * (countUndoStack + 1)); undoStack[countUndoStack++] = action; MolActionRetain(action); if (countUndoStack == 1) { /* あとで OnNeedCleanUndoStack() が呼ばれるようにイベントを投げておく */ wxCommandEvent myEvent(MyDocumentEvent, MyDocumentEvent_willNeedCleanUndoStack); wxPostEvent(this, myEvent); } } void MyDocument::CleanUndoStack(bool shouldRegister) { /* 積まれた action を「現在のコマンド」にセットする */ if (undoStack != NULL) { if (shouldRegister) { MyCommand *cmd = (MyCommand *)currentCommand; if (cmd == NULL) cmd = new MyCommand(mol); /* "Do" 中なら undoStack に、"Undo" 中なら redoStack に */ if (isUndoing) cmd->SetRedoActions(undoStack, countUndoStack); else cmd->SetUndoActions(undoStack, countUndoStack); if (currentCommand == NULL) { if (!GetCommandProcessor()->Submit(cmd)) delete cmd; } } else { int i; for (i = 0; i < countUndoStack; i++) MolActionRelease(undoStack[i]); free(undoStack); } } isUndoing = false; undoStack = NULL; countUndoStack = 0; currentCommand = NULL; } /* イベントハンドラ。イベントループの終わりに呼ばれる */ void MyDocument::OnNeedCleanUndoStack(wxCommandEvent& event) { CleanUndoStack(true); }
次に MyCommand
。undoActions
と redoActions
は実は同時に保持する必要はないのだが、不要な方をその都度解放したりするとバグの温床になりそうな気がしたので、効率は二の次としてシンプルに実装した。
/* 複数のアクションで1つのコマンドを形成する */ MyCommand::MyCommand(Molecule *aMolecule, const wxString& name): wxCommand(true, name) { mol = MoleculeRetain(aMolecule); undoActions = redoActions = NULL; numUndoActions = numRedoActions = 0; } MyCommand::~MyCommand() { MoleculeRelease(mol); SetUndoActions(NULL, 0); SetRedoActions(NULL, 0); } void MyCommand::SetUndoActions(MolAction **actions, int count) { int i; if (undoActions != NULL) { for (i = 0; i < numUndoActions; i++) MolActionRelease(undoActions[i]); free(undoActions); } undoActions = actions; numUndoActions = count; } void MyCommand::SetRedoActions(MolAction **actions, int count) { int i; if (redoActions != NULL) { for (i = 0; i < numRedoActions; i++) MolActionRelease(redoActions[i]); free(redoActions); } redoActions = actions; numRedoActions = count; } bool MyCommand::Do() { MyDocument *doc = MyDocumentFromMolecule(mol); int i; if (doc != NULL) { bool retval = true; if (redoActions != NULL) { doc->SetCurrentCommand(this); for (i = numRedoActions - 1; i >= 0; i--) { if (MolActionPerform(mol, redoActions[i]) != 0) { retval = false; break; } } doc->CleanUndoStack(retval); } return retval; } return false; } bool MyCommand::Undo() { MyDocument *doc = MyDocumentFromMolecule(mol); int i; if (doc != NULL) { bool retval = true; if (undoActions != NULL) { doc->SetIsUndoing(true); doc->SetCurrentCommand(this); for (i = numUndoActions - 1; i >= 0; i--) { if (MolActionPerform(mol, undoActions[i]) != 0) { retval = false; break; } } doc->CleanUndoStack(retval); } return retval; } return false; }
どうして wxWidgets 流にすなおに MyCommand
一本でいかないのかって? すでに Plain C で書いてある部分に手を入れたくなかったというのもあるが、「何か処理をする都度その『逆処理』を登録する」というスタイルは、コーディングが楽なんですよ。特に、スクリプト言語を内蔵して複数の処理を一度に実行することまで考えると、undo メカニズムのところで多少苦労したとしても、他のところが格段に書きやすくなる。これで難関一つ突破だ。