ホームページ開発ツール>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のウィンドウデリゲートクラス名は、以下で取得しました。
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)))
 もしここで、自作したデリゲートを指定してしまうと、デリゲートは一つしか持てないことから、既存のデリゲートをオーバーライドしてしまい、結果として、ウィンドウデリゲートが提供している機能(例えばResizing,Resizedイベント)が損なわれてしまう、ということが起こります。

 これを解決する(一つの?)方法は、(これも以前やった)既存のデリゲートをカテゴリ拡張してメソッドを追加する、というものです。
 カテゴリによるクラス拡張は、全てのインスタンスに適用される、という特性がありますが、windowWillReturnUndoManager:メソッドの実装は任意で、しなければデフォルトのUndoManagerが割り当てられるため、副作用は特にないと考えられます。

 参考サイト(1):windowWillReturnUndoManager: - NSWindowDelegate | Apple Developer Documentation


 サンプルの仕様

 今回は、2ページ構成のタブパネル内それぞれのページに置かれたTextAreaに、個別にUndoManagerを割り当ててみます。
 仕様は以下の通りとしました。

 Xojoでの実装
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. Xojoで新規プロジェクトを作成
  2. 前回プロジェクトから、NSTextViewCategoryクラス、CustomTextAreaクラスをコピー&ペースト
  3. NSTextViewCategoryクラス内のmyWindow共有プロパティを削除
  4. 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
    
  5. 以下をNSTextViewCategoryにペースト(できなければ共有プロパティに、名前:UndoMgrT、データ型:Ptr、を追加)
    Public Shared Property UndoMgrT as Ptr
    
  6. CustomTextAreaクラスから、Initメソッド以外を削除
  7. 新規クラスを作成(名前は、ここでは「XOJWinCntlCategory」とした。)
  8. 以下を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
    
  9. 以下をXOJWinCntlCategoryにペースト
    Private Shared Function windowWillReturnUndoManager(id As Ptr, SEL As CString, window As Ptr) as Ptr
      // 自作したUndoManagerを返す
      return UndoMgrW
    End Function
    
    注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)
  10. 以下をXOJWinCntlCategoryにペースト(できなければ共有プロパティに、名前:UndoMgrW、データ型:Ptr、を追加)
    Public Shared Property UndoMgrW as Ptr
    
  11. 以下をAppにペースト(できなければ、Sub - Endの間をOpenイベントに記述)
    Sub Open() Handles Open
      // XOJWindowControllerクラスのカテゴリ拡張
      XOjWinCntlCategory.RegisterMethod()
      
      // NSTextViewのカテゴリ拡張(TextAreaが一つでも表示されると、変換中の日本語が不可視となる現象が発生するため、できるだけ早い時点で処理する)
      NSTextViewCategory.CategorySet()
    End Sub
    
  12. MainMenuBar>編集メニュー>取り消すの次に、項目(Name:EditRedo、Text:やり直す)を追加
  13. Window1にTabPanel(名前は「TabPanel1」) を配置
  14. 以下を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
    
  15. TabPanel1のTab 0にTextArea(名前は「TextArea1」) を配置し、SuperをCustomTextAreaに設定
  16. TabPanel1のTab 1にTextArea(名前は「TextArea2」) を配置し、SuperをCustomTextAreaに設定
  17. 以下を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
    
  18. 以下を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
    
  19. 以下を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
    
  20. 以下をWindow1にペースト(できなければ、Sub - Endの間をEditRedoメニューハンドラに記述)
    Function EditRedo() As Boolean
      declare sub redo lib "Cocoa" selector "redo" (receiver as Ptr)
      redo(pUnmgr)
      
      Return True
    End Function
    
  21. 以下をWindow1にペースト(できなければ、Sub - Endの間をEditUndoメニューハンドラに記述)
    Function EditUndo() As Boolean
      declare sub undo lib "Cocoa" selector "undo" (receiver as Ptr)
      undo(pUnmgr)
      
      Return True
    End Function
    
  22. 以下を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
    
  23. 以下をWindow1にペースト(できなければプロパティに、名前:pUnmgr、データ型:Ptr、を追加)
    Protected Property pUnmgr as Ptr
    
  24. 以下をWindow1にペースト(できなければプロパティに、名前:pUnmgr1、データ型:Ptr、を追加)
    Protected Property pUnmgr1 as Ptr
    
  25. 以下を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]