ホームページ>開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでUndoManagerを利用する一考察
Xojo / Real Studio Trial and Error
目次
CocoaのDeclareでUndoManagerを利用する一考察
はじめに
以下は、Xojo Cocoaビルドについての話題です。
一般的な操作(注1)において、CocoaのNSUndoManagerがXojoからも使えるかどうか、調べてみました。(注2)
なお検証には、Xojo 2016 Release 3を用いています。(Mac mini mid 2010 + OS X 10.12.6 Sierra)
注1)テキスト入力の取り消し/やり直しについては以前検証したが、ここではそれ以外の、メソッドで実現するような操作を想定している。そのため、以前の続きではなく、新たなトピックとした。
注2)今回は、単純なケースを対象としていて、より汎用的な用途においては不明な点もあるため、「一考察」とした。
方針
基本的なやり方は、以前のトピックの「ペースト(ドロップ)時のアンドゥ」と同じですが、改めて整理しておくと、UndoManagerには、Undo/Redo時に実行するメソッドと復元に必要なデータ(パラメータ)を登録します。
異なる点は、上記トピックでは処理用メソッドをUndoManagerに直接指定(注3)していましたが、より汎用化させるため、登録するのは処理用メソッドそのものではなく、メソッド名とパラメータを引数とするダミーメソッドとし、実際の処理は、その引数を受け取ったXojo側のメソッドで行うようにしたことです。
注3)XojoのメソッドをUndoManagerに直接登録することはできない(?)ので、ランタイムAPIを用いて動的に作成したCocoaのメソッドを介している。さて、サンプルですが、現段階では実現の可否確認が先決なので、極くシンプルなものを試してみることにしました。
分かり易いのは、何らかの項目の追加と削除あたりではなかろうかと思われます。例えば、以下のサイトに示されているようなケースです。
参考サイト(1):NSUndoManagerを使って簡単にアンドゥ、リドゥを実装する
ここでは、リストボックスの行追加(挿入)と行削除を用いることとしました。
以上を踏まえ、サンプルの仕様は以下の通りとしました。
- 機能は、行追加/行挿入/行削除。
- Undo/Redoのペアをより明確にするため、行追加はAddRowではなく、InsertRowを用いて、内部処理的には行挿入との一本化を図る。
- メソッドは、通常の操作とUndo/Redoで同じものを使う。そのため、引数の行番号が文字列渡しになるといった冗長性が生じるが、やむを得ないものとする。
- 保持するパラメータは、行番号と各セルのテキストで、区切り記号はタブ(Chr(9))とする。
(もし、アプリがタブを許可する仕様としたい場合は、csvのエクセル方式にするか、配列にセットしてシリアライズするか、等の対応が必要。)
Xojoでの実装
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、取り消し/やり直しが機能することを確認しました。
- Xojoで新規プロジェクトを作成
- Window1にListBox(名前はListBox1)を追加
- 以下をListBox1にペースト(できなければ、Sub - Endの間をChangeイベントに記述)
Sub Change() Handles Change // 挿入と削除ボタンは、行が選択されている時だけ有効にする if me.ListIndex>=0 then PushButton2.Enabled=true PushButton3.Enabled=true else PushButton2.Enabled=false PushButton3.Enabled=false end if End Sub
- Window1にListBox(名前はListBox2)を追加(注:デバッグ用であり、必須ではない。)
- Window1にPushButton(名前はPushButton1、キャプションはAdd Row)を追加
- 以下をPushButton1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action() Handles Action LB1_InsertRow(Str(ListBox1.ListCount)) End Sub
- Window1にPushButton(名前はPushButton2、キャプションはInsert Row)を追加
- 以下をPushButton2にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action() Handles Action LB1_InsertRow(Str(ListBox1.ListIndex)) End Sub
- Window1にPushButton(名前はPushButton3、キャプションはRemove Row)を追加
- 以下をPushButton3にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action() Handles Action // 指定行の各列の内容を連結する Dim st As String = Str(ListBox1.ListIndex)+Chr(9)+JoinRowText(ListBox1.ListIndex) LB1_RemoveRow(st) End Sub
- 以下をWindow1にペースト(できなければ、Sub - Endの間をEnableMenuItemsイベントに記述)
Sub EnableMenuItems() Handles EnableMenuItems // NSUndoManagerの取得 declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(Window1.Handle) declare function canUndo lib "Cocoa" selector "canUndo" (receiver as Ptr) as Boolean // Return BOOL if canUndo(pnt1) 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(pnt1) then EditRedo.Enabled=true else EditRedo.Enabled=false end if End Sub
- 以下をWindow1にペースト(できなければ、Sub - Endの間をOpenイベントに記述)
Sub Open() Handles Open Dim cls As Window1Undo = new Window1Undo(AddressOf xojoUndoManager) // クラスのインスタンスを生成(Actionを受け取るメソッドを、引数で指定する) // ListBox1に初期値をセット InitListBox1() // 挿入と削除ボタンを無効化 PushButton2.Enabled=false PushButton3.Enabled=false End Sub
- 以下をWindow1にペースト(できなければ、Sub - Endの間をEditRedoメニューハンドラに記述)
Function EditRedo() As Boolean declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(Window1.Handle) declare sub redo lib "Cocoa" selector "redo" (receiver as Ptr) redo(pnt1) Return True End Function
- 以下をWindow1にペースト(できなければ、Sub - Endの間をEditUndoメニューハンドラに記述)
Function EditUndo() As Boolean declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(Window1.Handle) declare sub undo lib "Cocoa" selector "undo" (receiver as Ptr) undo(pnt1) Return True End Function
- 以下をWindow1にペースト
Private Sub InitListBox1() Listbox1.AddRow "" Listbox1.Cell(Listbox1.LastIndex,0)="abc" Listbox1.Cell(Listbox1.LastIndex,1)="def" Listbox1.Cell(Listbox1.LastIndex,2)="ghi" Listbox1.AddRow "" Listbox1.Cell(Listbox1.LastIndex,0)="jkl" Listbox1.Cell(Listbox1.LastIndex,1)="mno" Listbox1.Cell(Listbox1.LastIndex,2)="pqr" Listbox1.AddRow "" Listbox1.Cell(Listbox1.LastIndex,0)="123" Listbox1.Cell(Listbox1.LastIndex,1)="456" Listbox1.Cell(Listbox1.LastIndex,2)="789" End Sub
- 以下をWindow1にペースト
Private Function JoinRowText(idx As Integer) as String Dim i As Integer Dim st As String // まず、先頭列の内容をセット st=ListBox1.Cell(idx,0) // タブを区切り記号として、2列目以降の内容を連結 for i=1 to ListBox1.ColumnCount-1 st=st+Chr(9)+ListBox1.Cell(idx,i) next return st End Function
- 以下をWindow1にペースト
Private Sub LB1_InsertRow(paramStr As String) // Undo/Redo時に実行するメソッドをUndoManagerに登録 SetUndoMgr("LB1_RemoveRow",paramStr) // 行番号を取得 Dim idx As Integer = Val(NthField(paramStr,Chr(9),1)) // 行を挿入 ListBox1.InsertRow(idx,"ins.") // 各セルの内容があれば、復元 if CountFields(paramStr,Chr(9))>1 then Dim i As Integer for i=0 to ListBox1.ColumnCount-1 ListBox1.Cell(idx,i)=NthField(paramStr,Chr(9),i+1+1) next end if End Sub
- 以下をWindow1にペースト
Private Sub LB1_RemoveRow(paramStr As String) // Undo/Redo時に実行するメソッドをUndoManagerに登録 SetUndoMgr("LB1_InsertRow",paramStr) // 行番号を取得 Dim idx As Integer = Val(NthField(paramStr,Chr(9),1)) // 行を削除 ListBox1.RemoveRow(idx) End Sub
- 以下をWindow1にペースト
Private Sub SetUndoMgr(mName As String, paramStr As String) // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr // 文字列を直接登録することはできない?ようで、一度NSString型に変換する(Ptr型で扱えるようにするため) Dim pntSt1 As Ptr = NSClassFromString("NSString") declare function stringWithString lib "Cocoa" selector "stringWithString:" (receiver as Ptr, target As CFStringRef) As Ptr pntSt1 = stringWithString(pntSt1, mName) Dim pntSt2 As Ptr = NSClassFromString("NSString") pntSt2 = stringWithString(pntSt2, paramStr) // NSUndoManagerの取得 declare function undoManager lib "Cocoa" selector "undoManager" (obj_id as Integer) as Ptr // Return NSUndoManager* Dim pnt1 As Ptr = undoManager(self.Handle) // UndoManagerにUndo/Redo時に使うメソッドとパラメータを登録 declare function prepareWithInvocationTarget lib "Cocoa" selector "prepareWithInvocationTarget:" (receiver as Ptr, target As Ptr) As Ptr Dim pnt2 As Ptr = prepareWithInvocationTarget(pnt1, Window1Undo.registMethodInst) Declare Sub registMethodWithParam Lib "Cocoa" Selector "registMethod:withParam:" (receiver As Ptr, mName As Ptr, param As Ptr) registMethodWithParam(pnt2, pntSt1, pntSt2) ListBox2.AddRow "set:"+mName // デバッグ用 End Sub
- 以下をWindow1にペースト
Public Sub xojoUndoManager(mName As String, paramStr As String) Listbox2.AddRow "un/redo:"+mName // デバッグ用 Listbox2.Cell(Listbox2.LastIndex,1)=paramStr // デバッグ用 select case mName case "LB1_InsertRow" // 行を挿入(追加を含む) // Undo実行 LB1_InsertRow(paramStr) case "LB1_RemoveRow" // 行を削除 // Undo実行 LB1_RemoveRow(paramStr) end select End Sub
- MainMenuBar>編集メニュー>取り消すの次に、項目(Name:EditRedo、Text:やり直す)を追加
- 新規クラスを作成(名前は、ここでは「Window1Undo」とした。)
- 以下をWindow1Undoにペースト(できなければ移譲に、名前:ActionDelegate、引数:mName As String, paramStr As String、を追加)
Private Sub ActionDelegate(mName As String, paramStr As String)
- 以下をWindow1Undoにペースト
Public Sub Constructor(action As ActionDelegate) // UndoManagerに登録するメソッドを動的に生成 RegisterMethod() // クラス生成元でActionを受け取るメソッドを登録 ActionHandler = action End Sub
- 以下をWindow1Undoにペースト
Private Shared Sub ActionEvent(mName As String, paramStr As String) ActionHandler.Invoke(mName,paramStr) // クラス生成元でActionを受け取るメソッドを呼び出す End Sub
- 以下をWindow1Undoにペースト
注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)Private Shared Sub myRegistMethodParam(id As Ptr, SEL As CString, mName As Ptr, paramStr As Ptr) // NSString型から文字列を取り出す Declare Function UTF8String Lib "Cocoa" Selector "UTF8String" (receiver As Ptr) As CString Dim st1 As String = UTF8String(mName) Dim st2 As String = UTF8String(paramStr) // インスタンスに渡す ActionEvent(st1,st2) End Sub
- 以下をWindow1Undoにペースト
Private Shared Sub RegisterMethod() // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr // Declare宣言 Declare Function objc_allocateClassPair Lib "Cocoa" (superclass As Ptr, name As CString, extraBytes As Integer) as Ptr Declare Sub objc_registerClassPair Lib "Cocoa" (cls As Ptr) Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean // 既にインスタンス作成済なら戻る if registMethodInst <> nil then return end if // クラス名をmyIdentifier、メタクラス名をNSObjectにして、生成 Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myIdentifier", 0) // ランタイムに登録(参照を可能とするため) objc_registerClassPair newClassId // UndoManagerから呼ばれるメソッドの受け口となるメソッドを追加(registMethod:withParam:をXojo側で用意したmyRegistMethodParamメソッドで受け取る。) if not class_addMethod (newClassId, NSSelectorFromString("registMethod:withParam:"), AddressOf myRegistMethodParam, "v@:@@") then msgBox "error." return end if // 上記で生成したクラスのインスタンスを作成 Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr Dim targetId As Ptr = init(alloc(newClassId)) // インスタンスを保持 registMethodInst = targetId End Sub
- 以下をWindow1Undoにペースト(できなければ共有プロパティに、名前:ActionHandler、データ型:ActionDelegate、を追加)
Private Shared Property ActionHandler as ActionDelegate
- 以下をAccessoryPanelにペースト(できなければ共有プロパティに、名前:registMethodInst、データ型:Ptr、を追加)
Public Shared Property registMethodInst as Ptr
おわりに
メソッドの振り分けは必要になるものの、スタックの管理をしなくていいのは楽です。
ただし、今回のようにパラメータがシンプルならいいのですが、実際にはもっと複雑なケースも考えられます。
かといって、パラメータ用のスタックをXojo側に置いたりすると、NSUndoManagerを使うメリットが半減してしまうので、悩ましいところではあります。
配列やDictionaryは、オーバーヘッドを覚悟して、NSArrayやNSDictionaryに変換してしまう手はありそうだが…この辺は、実際に実装しながら試行錯誤するしかないかも。
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):NSUndoManagerを使って簡単にアンドゥ、リドゥを実装する
更新履歴
2018.07.30 Xojoでの実装の26項を改訂
2017.10.14 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]