ホームページ>開発ツール>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]