ホームページ>開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでリッチテキストを扱う・修飾の種類を追加する
Xojo / Real Studio Trial and Error
目次
CocoaのDeclareでリッチテキストを扱う・修飾の種類を追加する
はじめに
以下は、Xojo Cocoaビルドについての話題です。
リッチテキスト(NSAttributedString)の修飾の種類の追加について試してみました。
なお検証には、Xojo 2016 Release 3を用いています。(Mac mini mid 2010 + macOS 10.13.6 High Sierra)
方針
スタイル付きテキストの修飾は、インスペクターバーで多くのことができますが、含まれないものもあります。
なので、追加してみることにしました。項目は以下の通りです。
(注:シャドウと取り消し線は、フォントパネルから指定できるのですが、これらも対象とすることにします。)
参考サイト(1):NSAttributedStringを使ってみる | Tea Leaves
項目名 Cocoaでのパラメータ名 Xojoでの指定名 アウトライン NSStrokeWidthAttributeName NSStrokeWidth シャドウ NSShadowAttributeName NSShadow カーニング NSKernAttributeName NSKern 取り消し線 NSStrikethroughStyleAttributeName NSStrikethrough
指定は、(1) 文字が未入力、または入力済だが非選択、(2) 文字が入力されていて、全部または一部が選択状態、の二通りに対応します。
(1)は、NSAttributedStringではなく、NSTextViewの属性。どちらのケースも、既存のアトリビュートに追加する形をとりますが、
(1)では、NSTextViewのtypingAttributesで現在のキャレット位置のアトリビュート(ディクショナリ形式)を取得して、適用します。
(2)では、textStorageで全体を取得後、選択範囲に対して適用します。この時、返ってくるのはNSAttributedStringなので編集できないため、Mutable化します。
(2)の場合、当初は選択範囲に含まれる既存のアトリビュートを個別に取得して、それぞれに適用する方法も試したのですが、それをしなくても「見かけ上は」同じ結果になったので、省略しても大丈夫(?)そうです。
また、それとも関連しそうですが、アウトラインやシャドウのカラーの設定は、しないとフォントカラーが適用される(?)ようです。
機能はメニューに実装することとし、ツールバーや独自のパネル類は使いません。
メニュー構成と動作は、原則として、テキストエディットを参考に行います。
以上を踏まえ、仕様は以下の通りとしました。
- 選択状態適用時は、既存のアトリビュートは取得せず、指定された範囲に適用のみ
- アウトライン/シャドウのカラー(NSStrokeColorAttributeName、NSShadowのshadowColor)指定は行わない
- アウトライン/シャドウで指定する数値は、固定値とする
- アウトライン/シャドウメニューはトグル動作とし。現在の値を反映する
- 取り消し線メニューは、なしを設定せず、(一本線と二本線のシーソーを含む)トグル動作とし。現在の値を反映する
- カーニングは(テキストエディットに倣って)既存値の取得はせず、適用のみ
Xojoでの実装
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、追加したテキストの修飾が機能することを確認しました。
- 前回プロジェクトをベースとする
- MainMenuBar>編集メニューの次に、メニュー(Name:Format、Text:フォーマット)を追加
- MainMenuBar>編集メニューに、メニュー項目(Name:FormatOutline、Text:アウトライン)(Name:FormatShadow、Text:シャドウ)を追加
- MainMenuBar>編集メニューに、サブメニュー(Name:FormatKerning、Text:カーニング)(Name:FormatStrike、Text:取り消し線)を追加
- MainMenuBar>編集>カーニングメニューに、メニュー項目(Name:FormatKerningDefault、Text:デフォルト)(Name:FormatKerningNone、Text:使用しない)(Name:FormatKerningCondense、Text:きつく)(Name:FormatKerningExtend、Text:ゆるく)を追加
- MainMenuBar>編集>取り消し線メニューに、メニュー項目(Name:FormatStrikeSingle、Text:一本線)(Name:FormatStrikeDouble、Text:二本線)を追加
- 以下をWindow1のEnableMenuItemsイベントの最後に追加
// チェックマーク判定 SetFormatMenu(TextArea1)
- 以下をWindow1にペースト(できなければ、Function - End Functionの間をFormatKerningCondenseメニューハンドラに記述)
Function FormatKerningCondense() As Boolean attKind.kind="KCondens" // 種別セット SetAttrOption() // 修飾処理 Return True End Function
- 以下をWindow1にペースト(できなければ、Function - End Functionの間をFormatKerningDefaultメニューハンドラに記述)
Function FormatKerningDefault() As Boolean attKind.kind="KDefault" // 種別セット SetAttrOption() // 修飾処理 Return True End Function
- 以下をWindow1にペースト(できなければ、Function - End Functionの間をFormatKerningExtendメニューハンドラに記述)
Function FormatKerningExtend() As Boolean attKind.kind="KExtend" // 種別セット SetAttrOption() // 修飾処理 Return True End Function
- 以下をWindow1にペースト(できなければ、Function - End Functionの間をFormatKerningNoneメニューハンドラに記述)
Function FormatKerningNone() As Boolean attKind.kind="KNone" // 種別セット SetAttrOption() // 修飾処理 Return True End Function
- 以下をWindow1にペースト(できなければ、Function - End Functionの間をFormatOutlineメニューハンドラに記述)
Function FormatOutline() As Boolean if FormatOutline.Checked then // 既にチェックありなら FormatOutline.Checked=false // クリア attKind.Outline=false else FormatOutline.Checked=true // チェック attKind.Outline=true end if attKind.kind="Outline" // 種別セット SetAttrOption() // 修飾処理 Return True End Function
- 以下をWindow1にペースト(できなければ、Function - End Functionの間をFormatShadowメニューハンドラに記述)
Function FormatShadow() As Boolean if FormatShadow.Checked then // 既にチェックありなら FormatShadow.Checked=false // クリア attKind.Shadow=false else FormatShadow.Checked=true // チェック attKind.Shadow=true end if attKind.kind="Shadow" // 種別セット SetAttrOption() // 修飾処理 Return True End Function
- 以下をWindow1にペースト(できなければ、Function - End Functionの間をFormatStrikeDoubleメニューハンドラに記述)
Function FormatStrikeDouble() As Boolean if FormatStrikeDouble.Checked then // 既にチェックありなら attKind.Strike=0 // クリア else attKind.Strike=2 // 2本線 end if attKind.kind="StrikeD" // 種別セット SetAttrOption() // 修飾処理 Return True End Function
- 以下をWindow1にペースト(できなければ、Function - End Functionの間をFormatStrikeSingleメニューハンドラに記述)
Function FormatStrikeSingle() As Boolean if FormatStrikeSingle.Checked then // 既にチェックありなら attKind.strike=0 // クリア else attKind.strike=1 // 1本線 end if attKind.kind="StrikeS" // 種別セット SetAttrOption() // 修飾処理 Return True End Function
- 以下をWindow1にペースト
Protected Sub SetAttrOption() if TextArea1.SelLength=0 then // 文字が選択されていない // 現在のキャレット位置にオプション属性をセット SetAttrTextArea(TextArea1) else // 文字が選択されている // 現在の文字選択範囲を退避 Dim sst, sln As Integer sst=TextArea1.SelStart sln=TextArea1.SelLength // TextAreaの取得 declare function documentView lib "Cocoa" selector "documentView" (obj_id as Integer) as Ptr // Return NSTextView* Dim pnt1 As Ptr = documentView(TextArea1.Handle) // 現在の選択範囲を取得(ここからキャレット位置が取れる) Declare Function selectedRange Lib "Cocoa" Selector "selectedRange" (receiver As Ptr) As NSRange Dim rngSel As NSRange = selectedRange(pnt1) // Cocoa側から取る // NSTextStorageの取得>NSAttributedStringの取得 declare function textStorage lib "Cocoa" selector "textStorage" (obj_id as Ptr) As Ptr // Return NSTextStorage* Dim pnt2 As Ptr = textStorage(pnt1) // 複製(Mutable化) Declare Function mutableCopy Lib "Cocoa" Selector "mutableCopy" (receiver As Ptr) As Ptr Dim attM As Ptr = mutableCopy(pnt2) // オプション属性のセット attM = SetAttrSelectedText(attM, rngSel) // ストレージにスタイル付きテキストをセット Declare Sub setAttributedString Lib "Cocoa" Selector "setAttributedString:" (receiver As Ptr, identifier As Ptr) setAttributedString(pnt2, attM) // 退避していた文字選択範囲を復元 TextArea1.SelStart=sst TextArea1.SelLength=sln // clean up Declare Sub release Lib "Cocoa" Selector "release" (receiver As Ptr) release(attM) end if End Sub
- 以下をWindow1にペースト
Protected Function SetAttrSelectedText(atstr As Ptr, rngSel As NSRange) as Ptr // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Sub addAttribute Lib "Cocoa" Selector "addAttributes:range:" (receiver As Ptr, nane As Ptr, range As NSRange) Declare Function attributesAtIndex Lib "Cocoa" Selector "attributesAtIndex:effectiveRange:" (receiver As Ptr, idx As Integer, rng As Ptr) As Ptr Declare Function dictionaryWithObject Lib "Cocoa" Selector "dictionaryWithObject:forKey:" (receiver As Ptr, objt As Ptr, key As CFStringRef) As Ptr Declare Function objectForKey Lib "Cocoa" Selector "objectForKey:" (receiver As Ptr, key As CFStringRef) As Ptr Declare Function numberWithFloat Lib "Cocoa" Selector "numberWithFloat:" (receiver As Ptr, path As CGFloat) As Ptr Declare Function numberWithInteger Lib "Cocoa" Selector "numberWithInteger:" (receiver As Ptr, path As Integer) As Ptr Declare Function floatValue Lib "Cocoa" Selector "floatValue" (receiver As Ptr) As CGFloat Declare Sub removeObjectForKey Lib "Cocoa" Selector "removeAttribute:range:" (receiver As Ptr, name As CFStringRef, range As NSRange) Declare Function length Lib "Cocoa" Selector "length" (receiver As Ptr) As Integer Dim ll As Integer = length(atstr) // 文字列長 Declare Sub beginEditing Lib "Cocoa" Selector "beginEditing" (receiver As Ptr) beginEditing(atstr) // 編集開始 Dim i As Integer select case attKind.kind case "Outline" , "Shadow" , "StrikeS" , "StrikeD" if attKind.kind="Outline" then // アウトライン if attKind.Outline then // アウトラインあり // NSDictionary生成 Dim dict As Ptr = NSClassFromString("NSMutableDictionary") Declare Function getDictionary Lib "Cocoa" Selector "dictionary" (receiver As Ptr) As Ptr dict = getDictionary(dict) // 実数をNSNumber形式に変換 Dim numb As Ptr = NSClassFromString("NSNumber") numb = numberWithFloat(numb, 3.0) // 3.0 Declare Sub setObjectForKey Lib "Cocoa" Selector "setObject:forKey:" (receiver As Ptr, obj As Ptr, key As CFStringRef) setObjectForKey(dict, numb, "NSStrokeWidth") // 線の幅 // 属性をセット addAttribute(atstr, dict, rngSel) else // 属性を削除 removeObjectForKey(atstr, "NSStrokeWidth", rngSel) end if elseif attKind.kind="Shadow" then // シャドウ if attKind.Shadow then // シャドウあり // NSShadow生成 Dim shadow As Ptr = NSClassFromString("NSShadow") Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr shadow = alloc(shadow) Declare Function init Lib "Cocoa" Selector "init" (receiver As Ptr) As Ptr shadow = init(shadow) Declare Sub setShadowOffset Lib "Cocoa" Selector "setShadowOffset:" (receiver As Ptr, range As CGSize) setShadowOffset(shadow, CGSizeMake(1.0, 7.0)) // 影のサイズ Declare Sub setShadowBlurRadius Lib "Cocoa" Selector "setShadowBlurRadius:" (receiver As Ptr, range As CGFloat) setShadowBlurRadius(shadow, 10.0) // ぼかしの半径 // 属性をセット Declare Sub addAttributeValue Lib "Cocoa" Selector "addAttribute:value:range:" (receiver As Ptr, nane As CFStringRef, val As Ptr, range As NSRange) addAttributeValue(atstr, "NSShadow", shadow, rngSel) // clean up Declare Sub release Lib "Cocoa" Selector "release" (receiver As Ptr) release(shadow) else // 属性を削除 removeObjectForKey(atstr, "NSShadow", rngSel) end if elseif attKind.kind="StrikeS" or attKind.kind="StrikeD" then // 取り消し線 if attKind.Strike>0 then // 取り消し線あり // 整数をNSNumber形式に変換 Dim numb As Ptr = NSClassFromString("NSNumber") Dim vv As Integer if attKind.kind="StrikeS" then vv = 1 // Single else vv = 9 // Double end if numb = numberWithInteger(numb, vv) // NSDictionary生成 Dim dict As Ptr = NSClassFromString("NSDictionary") dict = dictionaryWithObject(dict, numb, "NSStrikethrough") // 属性をセット addAttribute(atstr, dict, rngSel) else // 属性を削除 removeObjectForKey(atstr, "NSStrikethrough", rngSel) end if end if case "KExtend" , "KCondens" , "KDefault" , "KNone" // カーニング if not (attKind.kind="KNone") then // カーニングあり Dim vv As CGFloat = 0.0 // 初期値 = デフォルト if attKind.kind="KExtend" or attKind.kind="KCondens" then // 現在の値を読み込み Dim attk As Ptr = attributesAtIndex(atstr, rngSel.location, nil) Dim ptrk As Ptr = objectForKey(attk, "NSKern") if ptrk<>nil then vv = floatValue(ptrk) end if // 値の増減 if attKind.kind="KExtend" then vv=vv+4.0 else // KCondens vv=vv-4.0 end if end if // 実数をNSNumber形式に変換 Dim numb As Ptr = NSClassFromString("NSNumber") numb = numberWithFloat(numb, vv) // NSDictionary生成 Dim dict As Ptr = NSClassFromString("NSDictionary") dict = dictionaryWithObject(dict, numb, "NSKern") // 属性をセット addAttribute(atstr, dict, rngSel) else // 属性を削除 removeObjectForKey(atstr, "NSKern", rngSel) end if end select Declare Sub endEditing Lib "Cocoa" Selector "endEditing" (receiver As Ptr) endEditing(atstr) // 編集終了 // 矛盾の解消(効果ある??) declare sub fixAttributesInRange lib "Cocoa" selector "fixAttributesInRange:" (receiver as Ptr, range As NSRange) fixAttributesInRange(atstr, NSMakeRange(0, ll)) return atstr End Function
- 以下をWindow1にペースト
Protected Sub SetAttrTextArea(ef As TextArea) // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Sub setObjectForKey Lib "Cocoa" Selector "setObject:forKey:" (receiver As Ptr, obj As Ptr, key As CFStringRef) Declare Sub removeObjectForKey Lib "Cocoa" Selector "removeObjectForKey:" (receiver As Ptr, name As CFStringRef) Declare Function numberWithFloat Lib "Cocoa" Selector "numberWithFloat:" (receiver As Ptr, path As CGFloat) As Ptr // DocumentViewを取得 Dim pnt1 As Ptr declare function documentView lib "Cocoa" selector "documentView" (obj_id as Integer) as Ptr // Return NSTextView* pnt1 = documentView(ef.Handle) // 現在のキャレット位置のアトリビュート(NSDictionary形式)を取得 Declare Function typingAttributes Lib "Cocoa" Selector "typingAttributes" (receiver As Ptr) As Ptr Dim dict0 As Ptr = typingAttributes(pnt1) // 複製 Declare Function mutableCopy Lib "Cocoa" Selector "mutableCopy" (receiver As Ptr) As Ptr Dim dict1 As Ptr = mutableCopy(dict0) select case attKind.kind case "Outline" // アウトライン if attKind.Outline then // アウトラインあり // 実数をNSNumber形式に変換 Dim numb As Ptr = NSClassFromString("NSNumber") numb = numberWithFloat(numb, 3.0) // 3.0 setObjectForKey(dict1, numb, "NSStrokeWidth") // 線の幅 else // 属性を削除 removeObjectForKey(dict1, "NSStrokeWidth") end if case "Shadow" // シャドウ if attKind.Shadow then // シャドウあり // NSShadow生成 Dim shadow As Ptr = NSClassFromString("NSShadow") Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr shadow = alloc(shadow) Declare Function init Lib "Cocoa" Selector "init" (receiver As Ptr) As Ptr shadow = init(shadow) Declare Sub setShadowOffset Lib "Cocoa" Selector "setShadowOffset:" (receiver As Ptr, range As CGSize) setShadowOffset(shadow, CGSizeMake(1.0, 7.0)) // 影のサイズ Declare Sub setShadowBlurRadius Lib "Cocoa" Selector "setShadowBlurRadius:" (receiver As Ptr, range As CGFloat) setShadowBlurRadius(shadow, 10.0) // ぼかしの半径 setObjectForKey(dict1, shadow, "NSShadow") // 影 // clean up Declare Sub release Lib "Cocoa" Selector "release" (receiver As Ptr) release(shadow) else // 属性を削除 removeObjectForKey(dict1, "NSShadow") end if case "StrikeS" , "StrikeD" // 取り消し線 if attKind.Strike>0 then // 取り消し線あり // 整数をNSNumber形式に変換 Dim numb As Ptr = NSClassFromString("NSNumber") Declare Function numberWithInteger Lib "Cocoa" Selector "numberWithInteger:" (receiver As Ptr, path As Integer) As Ptr Dim vv As Integer if attKind.kind="StrikeS" then vv = 1 // Single else vv = 9 // Double end if numb = numberWithInteger(numb, vv) setObjectForKey(dict1, numb, "NSStrikethrough") // 線の種別 else // 属性を削除 removeObjectForKey(dict1, "NSStrikethrough") end if case "KExtend" , "KCondens" , "KDefault" , "KNone" // カーニング if not (attKind.kind="KNone") then // カーニングあり Dim vv As CGFloat = 0.0 // 初期値 = デフォルト if attKind.kind="KExtend" or attKind.kind="KCondens" then // 現在の値を読み込み Declare Function objectForKey Lib "Cocoa" Selector "objectForKey:" (receiver As Ptr, key As CFStringRef) As Ptr Dim ptrk As Ptr = objectForKey(dict1, "NSKern") if ptrk<>nil then Declare Function floatValue Lib "Cocoa" Selector "floatValue" (receiver As Ptr) As CGFloat vv = floatValue(ptrk) end if // 値の増減 if attKind.kind="KExtend" then vv=vv+4.0 else // KCondens vv=vv-4.0 end if end if // 実数をNSNumber形式に変換 Dim numb As Ptr = NSClassFromString("NSNumber") numb = numberWithFloat(numb, vv) setObjectForKey(dict1, numb, "NSKern") // 間隔 else // 属性を削除 removeObjectForKey(dict1, "NSKern") end if end select // 属性をセット Declare Sub setTypingAttributes Lib "Cocoa" Selector "setTypingAttributes:" (receiver As Ptr, dict As Ptr) setTypingAttributes(pnt1, dict1) End Sub
- 以下をWindow1にペースト
Protected Sub SetFormatMenu(ef As TextArea) // TextAreaの取得 declare function documentView lib "Cocoa" selector "documentView" (obj_id as Integer) as Ptr // Return NSTextView* Dim pnt1 As Ptr = documentView(ef.Handle) // 現在のキャレット位置のアトリビュート(NSDictionary形式)を取得 Declare Function typingAttributes Lib "Cocoa" Selector "typingAttributes" (receiver As Ptr) As Ptr Dim dict As Ptr = typingAttributes(pnt1) // 含まれる全てのキーを取得 Dim key As String Dim oline, shadow As Boolean Dim j, strike As Integer Dim pnt2 As Ptr Declare Function count Lib "Cocoa" Selector "count" (receiver As Ptr) As Integer Dim jmax As Integer = count(dict) // 要素数 Declare Function allKeys Lib "Cocoa" Selector "allKeys" (receiver As Ptr) As Ptr Dim keys As Ptr = allKeys(dict) // キーのリスト Declare Function objectAtIndexString Lib "Cocoa" Selector "objectAtIndex:" (receiver As Ptr, idx As Integer) As CFStringRef Declare Function objectForKey Lib "Cocoa" Selector "objectForKey:" (receiver As Ptr, key As CFStringRef) As Ptr Declare Function floatValue Lib "Cocoa" Selector "floatValue" (receiver As Ptr) As CGFloat for j=0 to jmax-1 key = objectAtIndexString(keys, j) // 個々のキー取得 if key="NSStrokeWidth" then // アウトライン oline=true end if if key="NSShadow" then // シャドウ shadow=true end if if key="NSStrikethrough" then // 取り消し線 strike=0 pnt2 = objectForKey(dict, "NSStrikethrough") if pnt2<>nil then // 設定済なら strike = floatValue(pnt2) // 現在の値を読み込み end if end if next // 当該メニューのチェックマーク操作と値セット if oline then FormatOutline.Checked=true attKind.Outline=true else FormatOutline.Checked=false attKind.Outline=false end if if shadow then FormatShadow.Checked=true attKind.Shadow=true else FormatShadow.Checked=false attKind.Shadow=false end if if strike=1 then FormatStrikeSingle.Checked=true FormatStrikeDouble.Checked=false attKind.Strike=1 elseif strike=9 then FormatStrikeSingle.Checked=false FormatStrikeDouble.Checked=true attKind.Strike=2 else FormatStrikeSingle.Checked=false FormatStrikeDouble.Checked=false attKind.Strike=0 end if End Sub
- 以下をWindow1にペースト(できなければプロパティに、名前:attKind、データ型:GFAttrOption、を追加)
Protected Property attKind as GFAttrOption
- 以下をWindow1(または適当なモジュール)の構造体(Structures)に追加
Structure GFAttrOption kind As String*8 Outline As Boolean Shadow As Boolean Strike As UInt8 End Structure
おわりに
数値は固定としましたが、より細かいチューニングができるように、オプションパネルを追加する等した方がいいかもしれません。
シャドウに関しては、フォントパネルに任せてしまう手もあるかも。
あと、(2)のケースでの「既存のアトリビュートを無視して追加のみ」については、更に検証が必要かもしれません。(既存に追加または置き換え、の方が無難かも。)
なお、OS9以前のEditFieldにあった「袋文字、影、文字間拡張/縮小(SelOutline, SelShadow, SelExtend, SelCondense)」はOSXでは使えなくなっていますが、それらの互換性維持にも使えるかもしれません。(TextAreaのサブクラス作って、計算型プロパティで実装すればいけるのかな?)
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):NSAttributedStringを使ってみる | Tea Leaves
更新履歴
2018.07.20 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]