ホームページ>開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでリッチテキストを扱う・アンドゥの問題と対策
Xojo / Real Studio Trial and Error
目次
CocoaのDeclareでリッチテキストを扱う・アンドゥの問題と対策
はじめに
以下は、Xojo Cocoaビルドについての話題です。
TextAreaが複数存在する場合や、TextFieldを使用する場合、前回手法によるアンドゥには幾つか問題点が確認されましたので、対策も含めて纏めておきました。
なお検証には、Xojo 2016 Release 1.1を用いています。(Mac mini mid 2010 + OS X 10.11.5 El Capitan)
複数TextAreaへの対応
前回のサンプルのように、「メインウィンドウに、一つの、アンドゥ対象TextArea」であれば、特に問題はないのですが、複数の場合は、
1.項については、もしアンドゥ登録したくないTextAreaがある場合は、アンドゥ登録前にアンドゥを有効にする設定かどうか判定し、無効なら登録をスキップするステップを追加することで、対応可能になります。
- カテゴリ拡張のため、全てのTextAreaがMethod Swizzling対象となり、(何もしないと)アンドゥ登録を行おうとしてしまう。
- 条件によっては、日本語の入力に支障が出る。
Method Swizzling用に置き換えたメソッドの第1引数が、現在入力中のTextAreaのインスタンスなので、これのallowsUndoプロパティを見て、trueならアンドゥ登録、falseならパスする。なお、TextAreaのアンドゥはデフォルトで有効になっているので、無効にしたい場合は明示的に指定する必要があります。
2.項について、確認できたのは、
(1) TextAreaを配置したメインウィンドウ上でMethod Swizzling設定を実行し、
(2) その後、(TextAreaを配置した)別のウィンドウやコンテナコントロールを表示/生成した場合、
(3) 後から表示/生成した方のTextAreaで、日本語入力時に変換途中の文字列が表示されなくなり(変換確定後は表示される)、アンドゥ登録もされない。
(4) ただし、英数字(1バイト文字)は表示/アンドゥ登録ともに問題ない。
どうも、Method Swizzling設定時にTextAreaのインスタンスが配置されていると、そのインスタンスのみ、日本語入力時の変換表示/アンドゥ登録が正常に行われる、と言う感触です。(試行錯誤の結果なので、本当のところはよく分からない。)
なので、メインウィンドウが表示される前に設定すればいいのでは?、と思って、AppのOpenイベントに記述するようにしたら、全てのTextAreaで正常に処理されるようになりました。
TextFieldへの対応
参考サイト(1):Text Fields, Text Views, and the Field Editor
参考サイト(2):Text Editing
XojoのTextFieldはNSTextFieldを継承している(と思われる)ため、文字の入力にはフィールドエディタ(NSTextView)を使用しているのですが、このフィールドエディタはウィンドウのデフォルトではなく、独自に割り当てられたもののようです。
このフィールドエディタはアンドゥが有効となっているため、(NSTextViewのMethod Swizzlingによって)アンドゥ登録されるのですが、実際にアンドゥしてみると以下のような事態となりました。(他にもあるかも。)
この問題を最も手っ取り早く解消する方法は、TextFieldの代わりに、「マルチラインプロパティをオフにして高さを1行分にしたTextAreaを使う」というものです。
- TextFieldを単独で使った場合は、実際のキー操作以上のステップが登録される。
- 別のTextAreaと跨った場合は、キー操作は記録されているものの、文字の復元は行われない。
これですと、TextArea間を跨ったアンドゥも問題なく行われます。(ただし、フォーカスの移動は記録されませんので、必要なら自分で登録する必要があります。)
またもし、TextFieldがアンドゥ不要でしたら、以下の方法も考えられます。
- TextFieldに、アンドゥを無効にしたカスタムフィールドエディタを割り当てる。
- (TextFieldもどきの)TextAreaのインスタンスを個別にアンドゥ無効にする。
アンドゥを無効にしたカスタムフィールドエディタを使用したところ、ListBoxのインラインエディタもアンドゥが無効になりました。(実験の結果なので、常にそうなるかは不明。)
サンプルの仕様
以上を踏まえて今回は、複数TextAreaに対応したものと、アンドゥ無効カスタムフィールドエディタの割り当てを試してみることにしました。
それぞれの仕様は以下の通りとしました。
例1(複数TextArea対応)
例2(アンドゥ無効カスタムフィールドエディタ)
- 新規プロジェクトとする
- ランタイムAPIのコードは原則として前回プロジェクトのものを用いるが、クラス化する
- TextAreaのサブクラスを作成して、インスタンスごとにカスタマイズ可能とする
- Method Swizzling設定は、AppのOpenイベントに記述する
- 新規プロジェクトとし、例1で作成したクラスをコピー&ペーストする
- windowWillReturnFieldEditor:toObject:をオーバーライドして、カスタムフィールドエディタを設定する
- アンドゥは無効にし、無効時はアンドゥ登録しないよう、判別処理を追加する
- Delegateの設定とオーバーライドメソッドはクラス化する
Xojoでの実装(例1)
実行してみたところ、アンドゥが機能することを確認しました。
- Xojoで新規プロジェクトを作成
- 以下をAppのOpenイベントに記述
// カテゴリによる機能拡張 NSTextViewCategory.CategorySet()
- 新規クラスを作成(名前は、ここでは「NSTextViewCategory」とした。)
- 以下をNSTextViewCategoryの共有メソッド(Shared Methods)に追加
メソッド名: CategorySet // UndoManagerに登録するメソッドを動的に生成 RegisterMethod() // カテゴリ機能を使ってメソッドをNSTextViewに追加 RegisterMethodNSTextView() // メソッドの入れ替え setupHook()
- 前回プロジェクトから、insTextStrage、myDeleteBackward、myInsertText、myPaste、myPerformDragOperation、mySetAttributedString、mySetMarkedTextSelectedRange、myShouldChangeTextInRangeReplacementString、RegisterMethod、RegisterMethodNSTextView、setupHookの各共有メソッド、hookDone、myWindow、nstextviewDone、stAttrStrInstanceの各共有プロパティ、NSRange(構造体)をコピー&ペースト
- 新規クラスを作成(名前は、ここでは「CustomTextArea」) し、SuperをTextAreaに設定
- CustomTextAreaの新規イベント定義に、EnableMenuItemsを追加
- 以下をCustomTextAreaの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 // インスタンスに継承 EnableMenuItems()
- 以下をCustomTextAreaのメニューハンドラ(Menu Handlers)に追加
メニュー項目名: EditRedo 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
- 以下をCustomTextAreaのメニューハンドラ(Menu Handlers)に追加
メニュー項目名: EditUndo 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
- 以下をCustomTextAreaのメソッドに追加
メソッド名: Init 引数: flg1 As Boolean, flg2 As Boolean, flg3 As Boolean // NSTextViewの取得 declare function documentView lib "Cocoa" selector "documentView" (obj_id as Integer) as Ptr // Return NSTextView* Dim pnt1 As Ptr = documentView(me.Handle) // インスペクターバーを表示(スタイルなしの場合も表示は必要) declare sub setUsesInspectorBar lib "Cocoa" selector "setUsesInspectorBar:" (receiver as Ptr, flag as Boolean) setUsesInspectorBar(pnt1, true) // ルーラーを表示/非表示 declare sub setRulerVisible lib "Cocoa" selector "setRulerVisible:" (receiver as Ptr, flag as Boolean) setRulerVisible(pnt1, flg1) // リッチテキスト有効/無効 declare sub setRichText lib "Cocoa" selector "setRichText:" (receiver as Ptr, flag as Boolean) setRichText(pnt1, flg2) // フォントや色を変更可能/不可 declare sub setUsesFontPanel lib "Cocoa" selector "setUsesFontPanel:" (receiver as Ptr, flag as Boolean) setUsesFontPanel(pnt1, flg2) // ---------------- Undo // Delegate設定 Declare Sub setDelegate Lib "Cocoa" Selector "setDelegate:" (receiver As Ptr, id As Ptr) setDelegate(pnt1, nil) // なぜかコメントアウトするとUndoできなくなる。戻り値は設定値でもnilでも同じ。 // Undo有効/無効 declare sub setAllowsUndo lib "Cocoa" selector "setAllowsUndo:" (receiver as Ptr, flg As Boolean) setAllowsUndo(pnt1, flg3)
- 以下をWindow1のプロパティに追加
プロパティ名: myWindow データ型: Window 標準値: なし
- Window1にTextArea(名前は「TextArea1」) を配置し、Superを
NSTextViewCategoryCustomTextAreaに設定。- 以下をWindow1のActivateイベントに追加
// UndoManagerを指定 NSTextViewCategory.myWindow = self
- 以下をWindow1のOpenイベントに追加
// 初期化 NSTextViewCategory.myWindow = self TextArea1.Init(true, true, true) // UndoManagerを指定 TextArea1.myWindow = self
- 新規ウィンドウを作成(名前は「Window2」)
- Window2にTextArea(名前は「TextArea1」) を配置し、Superを
NSTextViewCategoryCustomTextAreaに設定。- 以下をWindow2のActivateイベントに追加
// UndoManagerを指定 NSTextViewCategory.myWindow = self
- 以下をWindow2のOpenイベントに追加
// 初期化 NSTextViewCategory.myWindow = self TextArea1.Init(true, true, true) // UndoManagerを指定 TextArea1.myWindow = self
Xojoでの実装(例2)
実行してみたところ、TextFieldのアンドゥが無効になることを確認しました。
- Xojoで新規プロジェクトを作成
- 例1で作成したNSTextViewCategoryクラスをコピー&ペースト
- Window1にTextFieldを配置
- 以下をWindow1のOpenイベントに記述
// Delegate設定 Declare Sub setDelegate Lib "Cocoa" Selector "setDelegate:" (receiver As Integer, id As Ptr) setDelegate(me.Handle, CustomFieldEditor.RegisterMethod()) // Field Editorの設定用
- NSTextViewCategoryの、insTextStrage、myDeleteBackward、myInsertText、mySetAttributedString、mySetMarkedTextSelectedRange、myShouldChangeTextInRangeReplacementStringメソッドに、アンドゥ登録判定を追加。(長くなるので、こちらに纏めました。)
- 新規クラスを作成(名前は、ここでは「CustomFieldEditor」とした。)
- 以下をCustomFieldEditorの共有メソッド(Shared Methods)に追加
注)SELの型をPtrとしていたが、CStringの方が相応しいので変更した。(未使用なので変更しなくても実害はなし。)(2018.07.30)メソッド名: myWindowWillReturnFieldEditor 引数: id As Ptr, SEL As CString, sender As Ptr, obj As Ptr 戻り値型: Ptr // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr // 渡ってきたオブジェクトがNSTextFieldなら Dim pnt0 As Ptr = NSClassFromString("NSTextField") Declare Function isKindOfClass Lib "Cocoa" selector "isKindOfClass:" (class_id As Ptr, cls As Ptr) As Boolean if isKindOfClass(obj, pnt0) then 'if myCustomFieldEditor = nil then // Xojoが割り当て直してしまう?ようで、呼ばれる度に設定しないと置き換わらない。 Dim pnt1 As Ptr = RegisterFieldEditor() // インスタンス生成後は既存のポインタを返すだけなので、メモリを浪費する訳ではない。 // フィールドエディタとして設定 declare sub setFieldEditor lib "Cocoa" selector "setFieldEditor:" (receiver as Ptr, flg As Boolean) setFieldEditor(pnt1, true) // Undoを無効に declare sub setAllowsUndo lib "Cocoa" selector "setAllowsUndo:" (receiver as Ptr, flg As Boolean) setAllowsUndo(pnt1, false) return pnt1 'end if end if return nil
- 以下をCustomFieldEditorの共有メソッド(Shared Methods)に追加
メソッド名: RegisterFieldEditor // 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。 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 myCustomFieldEditor <> nil then return myCustomFieldEditor end if // クラス名をmyFieldEditor、メタクラス名をNSTextViewにして、生成 Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSTextView"), "myFieldEditor", 0) // ランタイムに登録(参照を可能とするため) objc_registerClassPair newClassId // Delegateはないので何もしない // 上記で生成したクラスのインスタンスを作成 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 subclassId As Ptr = init(alloc(newClassId)) // インスタンスを保持 myCustomFieldEditor = subclassId // インスタンスを返す return subclassId
- 以下をCustomFieldEditorの共有メソッド(Shared Methods)に追加
メソッド名: 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 FieldEditorDelegate <> nil then return FieldEditorDelegate end if // クラス名を引数のname(名前は任意だが、重複を避けるためにアイテムのIdentifierにしている。)、メタクラス名をNSObjectにして、生成 Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myIdentifierWin", 0) // ランタイムに登録(参照を可能とするため) objc_registerClassPair newClassId // Delegateの対象となるメソッドを追加(windowWillReturnFieldEditor:toObject:をXojo側で用意したmyWindowWillReturnFieldEditorメソッドで受け取る。) if not class_addMethod (newClassId, NSSelectorFromString("windowWillReturnFieldEditor:toObject:"), AddressOf myWindowWillReturnFieldEditor, "@@:@@@") then msgBox "error." return nil 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 delegateId As Ptr = init(alloc(newClassId)) // インスタンスを保持 FieldEditorDelegate = delegateId // インスタンスを返す return delegateId
- 以下をCustomFieldEditorの共有プロパティ(Shared Properties)に追加
プロパティ名: FieldEditorDelegate データ型: Ptr 標準値: なし
- 以下をCustomFieldEditorの共有プロパティ(Shared Properties)に追加
プロパティ名: myCustomFieldEditor データ型: Ptr 標準値: なし
おわりに
随分と面倒なことになってきたので、デフォルトのアンドゥで事足りるのであれば、そのまま使うのが無難かもしれません。
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):Text Fields, Text Views, and the Field Editor
参考サイト(2):Text Editing
更新履歴
2018.07.30 Xojoでの実装(例2)の7項を改訂
2017.08.15 Xojoでの実装(例1)の、13,17項で、CustomTextAreaとすべきところがNSTextViewCategoryとなっていたので修正。
2016.07.15 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]