ホームページ>開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでリッチテキストを扱う・独自UndoManagerを使ってみる
Xojo / Real Studio Trial and Error
目次
CocoaのDeclareでリッチテキストを扱う・独自UndoManagerを使ってみる 
はじめに
以下は、Xojo Cocoaビルドについての話題です。
TextArea個別に取り消し/やり直しを実行したい場合等、独自のUndoManagerを使う方法について調べてみました。
なお検証には、Xojo 2016 Release 3を用いています。(Mac mini mid 2010 + OS X 10.12.6 Sierra)
独自UndoManagerへの対応
ウィンドウには、(何もしないと)デフォルトのUndoManagerが割り当てられますので、ウィンドウ全体で一つ使う分には、これだけで事足ります。
ですが、例えばウィンドウ内に複数のTextAreaを置いて、それぞれ独自に取り消し/やり直しを行いたい場合は、これではうまくいきません。
この場合は、それぞれに固有のUndoManagerを自分で用意する必要があります。
UndoManager(のインスタンス)自体は、通常のalloc,initで作ることができます。
が、作るだけではダメで、ウィンドウに、自作したUndoManagerを教えてやる必要があります。
そのためには、windowWillReturnUndoManager:メソッドを使います。
ここで注意しなければいけないのは、これがNSWindowのデリゲートクラス(NSWindowDelegate)のメソッドであるという点です。
というのも、デリゲート自体は、これまでと同様ランタイムAPIを使って実装できますが、Xojoのウィンドウには、既にデリゲート(XOJWindowControllerクラス:注)が割り当てられているからです。
注)Xojoのウィンドウデリゲートクラス名は、以下で取得しました。もしここで、自作したデリゲートを指定してしまうと、デリゲートは一つしか持てないことから、既存のデリゲートをオーバーライドしてしまい、結果として、ウィンドウデリゲートが提供している機能(例えばResizing,Resizedイベント)が損なわれてしまう、ということが起こります。
Declare Function myDelegate Lib "Cocoa" selector "delegate" (class_id As Integer) As Ptr // Window1のデリゲートを取得 Declare Function myClass Lib "Cocoa" selector "class" (receiver As Ptr) As Ptr // インスタンスからクラスを取得 Declare Function NSStringFromClass Lib "Cocoa" (aClass As Ptr) As CFStringRef // クラス名を取得 Dim st As String = NSStringFromClass(myClass(myDelegate(Window1.Handle)))
これを解決する(一つの?)方法は、(これも以前やった)既存のデリゲートをカテゴリ拡張してメソッドを追加する、というものです。
カテゴリによるクラス拡張は、全てのインスタンスに適用される、という特性がありますが、windowWillReturnUndoManager:メソッドの実装は任意で、しなければデフォルトのUndoManagerが割り当てられるため、副作用は特にないと考えられます。
参考サイト(1):windowWillReturnUndoManager: - NSWindowDelegate | Apple Developer Documentation
サンプルの仕様
今回は、2ページ構成のタブパネル内それぞれのページに置かれたTextAreaに、個別にUndoManagerを割り当ててみます。
仕様は以下の通りとしました。
- 新規プロジェクトとする
 - NSTextViewのCategory拡張クラス、TextAreaのサブクラスは既存のものを流用する
 - NSTextViewのCategory拡張クラスは、UndoManagerの指定箇所のみ、変更する
 - UndoManagerの切り替えは、タブパネルのタブをクリックしたタイミングで行う
 
Xojoでの実装
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、アンドゥが機能することを確認しました。
- Xojoで新規プロジェクトを作成
 - 前回プロジェクトから、NSTextViewCategoryクラス、CustomTextAreaクラスをコピー&ペースト
 - NSTextViewCategoryクラス内のmyWindow共有プロパティを削除
 - NSTextViewCategoryクラス内各メソッドの、上段の箇所(変数名は一例)を下段に書き換え(または、当該変数をUndoMgrTに置き換え)
 // NSUndoManagerの取得 declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(myWindow.Handle)Dim pnt1 As Ptr = UndoMgrT- 以下をNSTextViewCategoryにペースト(できなければ共有プロパティに、名前:UndoMgrT、データ型:Ptr、を追加)
 Public Shared Property UndoMgrT as Ptr- CustomTextAreaクラスから、Initメソッド以外を削除
 - 新規クラスを作成(名前は、ここでは「XOJWinCntlCategory」とした。)
 - 以下をXOJWinCntlCategoryにペースト
 Public Shared Sub RegisterMethod() // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr // XOJWindowControllerクラスの取得 Dim WinCntlPtr As Ptr = NSClassFromString("XOJWindowController") // Declare宣言 Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean // Delegateの対象となるメソッドを追加(windowWillReturnUndoManager:をXojo側で用意したwindowWillReturnUndoManagerメソッドで受け取る。) if not class_addMethod (WinCntlPtr, NSSelectorFromString("windowWillReturnUndoManager:"), AddressOf windowWillReturnUndoManager, "@@:@") then msgBox "error." return end if End Sub- 以下をXOJWinCntlCategoryにペースト
 注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)Private Shared Function windowWillReturnUndoManager(id As Ptr, SEL As CString, window As Ptr) as Ptr // 自作したUndoManagerを返す return UndoMgrW End Function- 以下をXOJWinCntlCategoryにペースト(できなければ共有プロパティに、名前:UndoMgrW、データ型:Ptr、を追加)
 Public Shared Property UndoMgrW as Ptr- 以下をAppにペースト(できなければ、Sub - Endの間をOpenイベントに記述)
 Sub Open() Handles Open // XOJWindowControllerクラスのカテゴリ拡張 XOjWinCntlCategory.RegisterMethod() // NSTextViewのカテゴリ拡張(TextAreaが一つでも表示されると、変換中の日本語が不可視となる現象が発生するため、できるだけ早い時点で処理する) NSTextViewCategory.CategorySet() End Sub- MainMenuBar>編集メニュー>取り消すの次に、項目(Name:EditRedo、Text:やり直す)を追加
 - Window1にTabPanel(名前は「TabPanel1」) を配置
 - 以下をTabPanel1にペースト(できなければ、Sub - Endの間をChangeイベントに記述)
 Sub Change() Handles Change // 選択タブに合わせてUndoManagerを切り替え select case me.Value case 0 pUnmgr=pUnmgr1 case 1 pUnmgr=pUnmgr2 end select // UndoManagerをセット XOJWinCntlCategory.UndoMgrW=pUnmgr NSTextViewCategory.UndoMgrT=pUnmgr End Sub- TabPanel1のTab 0にTextArea(名前は「TextArea1」) を配置し、SuperをCustomTextAreaに設定
 - TabPanel1のTab 1にTextArea(名前は「TextArea2」) を配置し、SuperをCustomTextAreaに設定
 - 以下をWindow1にペースト(できなければ、Sub - Endの間をCloseイベントに記述)
 Sub Close() Handles Close // UndoManagerをクリア XOJWinCntlCategory.UndoMgrW=nil NSTextViewCategory.UndoMgrT=nil // clean up Declare Sub release Lib "Cocoa" Selector "release" (receiver As Ptr) release(pUnmgr1) release(pUnmgr2) // Xojo側もクリア pUnmgr=nil pUnmgr1=nil pUnmgr2=nil End Sub- 以下をWindow1にペースト(できなければ、Sub - Endの間をEnableMenuItemsイベントに記述)
 Sub EnableMenuItems() Handles EnableMenuItems // 取り消しが可能ならメニューを有効に declare function canUndo lib "Cocoa" selector "canUndo" (receiver as Ptr) as Boolean // Return BOOL if canUndo(pUnmgr) then EditUndo.Enabled=true else EditUndo.Enabled=false end if // やり直しが可能ならメニューを有効に declare function canRedo lib "Cocoa" selector "canRedo" (receiver as Ptr) as Boolean // Return BOOL if canRedo(pUnmgr) then EditRedo.Enabled=true else EditRedo.Enabled=false end if End Sub- 以下をWindow1にペースト(できなければ、Sub - Endの間をOpenイベントに記述)
 Sub Open() Handles Open // UndoManagerの初期化 InitUndoManager() // UndoManagerをセット XOJWinCntlCategory.UndoMgrW=pUnmgr NSTextViewCategory.UndoMgrT=pUnmgr // スタイル設定 TextArea1.Init(true,true) // ルーラー=true、スタイル=true TextArea2.Init(true,true) // ルーラー=true、スタイル=true // インスペクターバー付加によるHeightの増加に、ウィンドウが追随しないことへの対策(なぜか、これでうまくいくみたい。) me.Height=me.Height End Sub- 以下をWindow1にペースト(できなければ、Sub - Endの間をEditRedoメニューハンドラに記述)
 Function EditRedo() As Boolean declare sub redo lib "Cocoa" selector "redo" (receiver as Ptr) redo(pUnmgr) Return True End Function- 以下をWindow1にペースト(できなければ、Sub - Endの間をEditUndoメニューハンドラに記述)
 Function EditUndo() As Boolean declare sub undo lib "Cocoa" selector "undo" (receiver as Ptr) undo(pUnmgr) Return True End Function- 以下をWindow1にペースト
 Protected Sub InitUndoManager() // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr pUnmgr1 = NSClassFromString("NSUndoManager") // クラスメソッドなので、まずNSUndoManagerクラスを取得 pUnmgr2 = NSClassFromString("NSUndoManager") // クラスメソッドなので、まずNSUndoManagerクラスを取得 // NSUndoManagerを生成 Declare Function alloc Lib "Cocoa" selector "alloc" (receiver As Ptr) As Ptr Declare Function init Lib "Cocoa" selector "init" (receiver As Ptr) As Ptr pUnmgr1 = init(alloc(pUnmgr1)) // タブ0内TextArea1用 pUnmgr2 = init(alloc(pUnmgr2)) // タブ1内TextArea2用 // 起動時はタブ0が表示されるので、それ用をセット pUnmgr = pUnmgr1 End Sub- 以下をWindow1にペースト(できなければプロパティに、名前:pUnmgr、データ型:Ptr、を追加)
 Protected Property pUnmgr as Ptr- 以下をWindow1にペースト(できなければプロパティに、名前:pUnmgr1、データ型:Ptr、を追加)
 Protected Property pUnmgr1 as Ptr- 以下をWindow1にペースト(できなければプロパティに、名前:pUnmgr2、データ型:Ptr、を追加)
 Protected Property pUnmgr2 as Ptr
おわりに
今回のサンプルのようなケースでは、筋から云えば、UndoManagerはデフォルトのものを使い、タブ操作もアンドゥ登録する、となるのでしょうが、Xojo側の操作をUndoManagerに登録するあたりがネックになリそうです。
オペレーション(メソッド)の数が少なければ、ランタイムAPIの機能を使って個別に全て登録する手もありそうですが、数が増えてきたら、(NSUndoManagerを利用するメリットは薄らいでしまうが)Xojo側のメソッド名とパラメータを文字列として記録するメソッドのみをランタイム登録して、あとはXojo側で振り分ける、とかすればなんとかなるのかなぁ…
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):windowWillReturnUndoManager: - NSWindowDelegate | Apple Developer Documentation
更新履歴
2018.07.30 Xojoでの実装の9項を改訂
2017.08.03 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]