ホームページ>開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareでリッチテキストを扱う・縦書きする
Xojo / Real Studio Trial and Error
目次
CocoaのDeclareでリッチテキストを扱う・縦書きする
はじめに
以下は、Xojo Cocoaビルドについての話題です。
リッチテキストの縦書きについて試してみました。
なお検証には、Xojo 2016 Release 3を用いています。(Mac mini mid 2010 + macOS 10.13.3 High Sierra)
NSTextView/NSViewの縦書き
Cocoaでの縦書きについては、以下のサイトに全て載っています。
記載の内容をDeclareで記述するだけでできます。
参考サイト(1):のっけうしの徒然なる開発日誌: Cocoaでの縦書きの実現について
Cocoaの描画結果を画像データに変換する
注)本機能は、リッチテキストに限定されるものではありませんが、話題の流れ上、ここに置いています。縦書きの結果を、画像として扱いたい場合もあるかと思います。
そのための基本情報も、参考サイト(1)に記載されているので、原則その通りでいいのですが、今回はウィンドウに描画する訳ではないので、変換元としてはNSViewではなく、NSBitmapImageRepを用いることにします。
NSViewではdrawRect:をオーバーライドすることでグラフィックコンテキストが得られるが、NSBitmapImageRepの場合はインスタンス生成後、graphicsContextWithBitmapImageRep:を用いてグラフィックコンテキストを取得する。以降の(縦書きも含めた)描画方法は共通。回転の詳細については、(ここだけ説明がないので)別途調べた結果、CGContextTranslateCTMはCGContextRef(NSGraphicsContextからCGContext:で取得)に対して実行すればいいことが分かりました。
なお、マトリックス操作では、回転だけではフレーム外に出てしまうので、スライドも併せて行います。
追記:上記方法では、画像を含む場合に、画像が回転してしまう等の問題点が確認されています。解決法はこちらをご覧下さい。(2019.08.22)さて、画像の変換先としては、二つ考えられます。
一つは、汎用の画像フォーマットです。
ファイルに保存したい場合は、これができると便利です。
画像フォーマットには様々ありますが、ここではPNGとしました。(理由の一つは次項参照。)
もう一つは、XojoのPicture形式です。
これができれば、Xojo側での処理が色々と楽になります。
とはいえ、XojoのPictureは標準の機能でCGImageRefに変換できますが、(確認できた範囲内では)その逆は用意されていないようです。
その代わり、といえるのか、FromDataメソッドが使えそうな気がしないでもありません。
というのも、メモリーブロック内の詳細説明は見つけられませんでしたが、ペアとなるGetDataの入力が汎用の画像フォーマットなので、それと同等なものを作って渡してあげれば、いけそうだからです。
そこで、NSBitmapImageRepをPNG形式に変換してからバイト列を抽出し、MemoryBlockにコピーしてFromDataの入力としたところ…、うまくいきました。
TIFFRepresentation(即ちTIFF形式フォーマット)からもバイト列を抽出してみたが、なぜかサイズがえらく大きくなってしまった。一方、PNG形式では実用的なサイズに収まっている。
サンプルの仕様
以上を踏まえて今回は、縦書きの編集機能と、内容のプレビュー表示およびファイル出力するものを試してみることにしました。
仕様は以下の通りとしました。
- リッチテキストのプロジェクトをベースとする
- 縦書きはツールバーのボタンから選択する。(トグル動作)
- ファイル出力時の画像フォーマットは(前述の通り)PNGとする。
- プレビューはダイアログに表示する。この時、縦横比の維持は考えないこととする。
- 画像の範囲は、現在のテキストエリアのサイズで先頭からとし、スクロールについては考えないこととする。
Xojoでの実装
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
実行してみたところ、縦書きが機能することを確認しました。
- 前回プロジェクトをベースとする
- 以下をWindow1のOpenイベント内に追加(setRulerVisibleの直後)
// インスペクターバー追加による高さの増分がウィンドウ高に反映されないことへの対策(これでうまくいくようだ。) me.Height=me.Height
- Toolbar1に、以下のアイテムを追加。
ToolItem6(スペース)/ToolItem7(縦書き Style:Toggle Button)/ToolItem8(スペース)/ToolItem9(プレビュー)/ToolItem10(ファイル書き出し)
注)アイコンは適当な画像を別途用意します。(なくても、機能の確認はできます。)- 以下をWindow1のToolbar11のActionイベント内に追加(end selectの直前)
case "ToolItem7" // 縦書き(トグル) ToggleVtext() case "ToolItem9" // プレビュー ShowPreviewDlg() case "ToolItem10" // ファイル書き出し PNGtoFile()
- 以下をWindow1にペースト
Protected Function AttrStringSetVertical(attr As Ptr, flg As Boolean) as Ptr // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr Declare Function length Lib "Cocoa" Selector "length" (receiver As Ptr) As Integer Dim ll As Integer = length(attr) // 文字列長 Declare Sub beginEditing Lib "Cocoa" Selector "beginEditing" (receiver As Ptr) beginEditing(attr) // 編集開始 // 縦書きオプションの設定 Dim numB As Ptr = NSClassFromString("NSNumber") Declare Function numberWithBool Lib "Cocoa" Selector "numberWithBool:" (receiver As Ptr, num As Boolean) As Ptr numB = numberWithBool(numB, flg) Declare Sub addAttribute Lib "Cocoa" Selector "addAttribute:value:range:" (receiver As Ptr, nane As CFStringRef, val As Ptr, range As NSRange) addAttribute(attr, "CTVerticalForms", numB, NSMakeRange(0, ll)) Declare Sub endEditing Lib "Cocoa" Selector "endEditing" (receiver As Ptr) endEditing(attr) // 編集終了 // AttributedStringを返す return attr End Function
- 以下をWindow1にペースト
Protected Function ConvCtoP(clrC As Color) as Ptr Dim r, g, b As CGFloat // 0〜255 を 0.0〜1.0 にマッピング r=clrC.Red/255 g=clrC.Green/255 b=clrC.Blue/255 // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr // カラーの取得 Dim clr As Ptr = NSClassFromString("NSColor") // RGB値からカラーを生成 Declare Function colorWithCalibrate Lib "Cocoa" Selector "colorWithCalibratedRed:green:blue:alpha:" (receiver As Ptr, red As CGFloat, green As CGFloat, blue As CGFloat, alpha As CGFloat) As Ptr clr = colorWithCalibrate(clr, r, g, b, 1.0) // alpha値は常に1.0 // カラーを返す return clr End Function
- 以下をWindow1にペースト
Private Function PNGfromAttrString() as Ptr // TextAreaの取得 declare function documentView lib "Cocoa" selector "documentView" (obj_id as Integer) as Ptr // Return NSTextView* Dim pnt1 As Ptr = documentView(TextArea1.Handle) // ef.Handle = NSScrollView* // NSTextStorageの取得 declare function textStorage lib "Cocoa" selector "textStorage" (obj_id as Ptr) As Ptr // Return NSTextStorage* Dim pnt2 As Ptr = textStorage(pnt1) // サイズ調整 (スクロールバー、ルーラーの領域分をそれぞれ引いている。数値は現物合わせのため、誤差を含む可能性あり。) Dim w, h As Integer if vFlg=false then // 横 w=TextArea1.Width-16 h=TextArea1.Height-32 else // 縦 w=TextArea1.Width-30 h=TextArea1.Height-16 end if // サイズのセット Dim rect As NSRect = NSMakeRect(0,0,w,h) // テキストの描画結果をPNG形式で取得 Dim pngData As Ptr = PNGfromAttrString2(rect,ConvCtoP(RGB(255,255,255)),pnt2,vFlg) // PNGを返す return pngData End Function
- 以下をWindow1にペースト
Private Function PNGfromAttrString2(rect As NSRect, bkClr As Ptr, attr As Ptr, flg As Boolean) as Ptr // 文字列を指定してクラスオブジェクトを取得する。最初に一回宣言しておけばよい。 Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr // NSBitmapImageRep初期化 Dim bitmapImageRep As Ptr = NSClassFromString("NSBitmapImageRep") Declare Function alloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr bitmapImageRep = alloc(bitmapImageRep) Declare Function initWithBitmapDataPlanes Lib "Cocoa" Selector "initWithBitmapDataPlanes:pixelsWide:pixelsHigh:bitsPerSample:samplesPerPixel:hasAlpha:isPlanar:colorSpaceName:bitmapFormat:bytesPerRow:bitsPerPixel:" _ (receiver As Ptr, planes As Ptr, w As Integer, h As Integer, bps As Integer, spp As Integer, alpha As Boolean, isPlanar As Boolean, colorSpaceName As CFStringRef, bfmt As Integer, rBytes As Integer, pBits As Integer) As Ptr bitmapImageRep = initWithBitmapDataPlanes(bitmapImageRep, nil, rect.w, rect.h, 8, 4, true, false, "NSDeviceRGBColorSpace", 0, 0, 0) // bitmapImageRepからグラフィックコンテキストを取得 Dim context As Ptr = NSClassFromString("NSGraphicsContext") Declare Function graphicsContextWithBitmapImageRep Lib "Cocoa" Selector "graphicsContextWithBitmapImageRep:" (receiver As Ptr, irep As Ptr) As Ptr context = graphicsContextWithBitmapImageRep(context, bitmapImageRep) if context=nil then return nil end if // 現在のグラフィックコンテキストを退避 Dim context1 As Ptr = NSClassFromString("NSGraphicsContext") Declare Sub saveGraphicsState Lib "Cocoa" Selector "saveGraphicsState" (receiver As Ptr) saveGraphicsState(context1) // 現在のグラフィックコンテキストにセット Dim context2 As Ptr = NSClassFromString("NSGraphicsContext") Declare Sub setCurrentContext Lib "Cocoa" Selector "setCurrentContext:" (receiver As Ptr, cntx As Ptr) setCurrentContext(context2, context) // 縦書きが指定されたら if flg then // 回転を考慮したRectに変換(幅と高さを入れ替える) rect = NSMakeRect(rect.x,rect.y,rect.h,rect.w) // 回転用のコンテキストの取得 Declare Function CGContext Lib "Cocoa" Selector "CGContext" (receiver As Ptr) As Ptr Dim contextCG As Ptr = CGContext(context) // まず水平移動 Declare Sub CGContextTranslateCTM Lib "Carbon" (context As Ptr, height As CGFloat, width As CGFloat) CGContextTranslateCTM(contextCG, 0, rect.w) // 次に回転(右に90°) Declare Sub CGContextRotateCTM Lib "Carbon" (context As Ptr, rotate As CGFloat) CGContextRotateCTM(contextCG, -3.1416 / 2) end if // 背景色のセット Declare Sub setFill Lib "Cocoa" selector "setFill" (class_id As Ptr) setFill(bkclr) // rectを塗りつぶす Declare Sub NSRectFill Lib "AppKit" (aRect As NSRect) NSRectFill(rect) // 本文の描画 Declare Sub drawInRect Lib "Cocoa" selector "drawInRect:" (class_id As Ptr, rect As NSRect) drawInRect(attr, rect) // 退避したグラフィックコンテキストを戻す Dim context3 As Ptr = NSClassFromString("NSGraphicsContext") Declare Sub restoreGraphicsState Lib "Cocoa" Selector "restoreGraphicsState" (receiver As Ptr) restoreGraphicsState(context3) // falseをNSNumber形式に変換 Dim numb As Ptr = NSClassFromString("NSNumber") Declare Function numberWithBool Lib "Cocoa" Selector "numberWithBool:" (receiver As Ptr, path As Boolean) As Ptr numb = numberWithBool(numb, false) // PNG用オプションのセット Dim dict As Ptr = NSClassFromString("NSDictionary") Declare Function dictionaryWithObject Lib "Cocoa" Selector "dictionaryWithObject:forKey:" (receiver As Ptr, objt As Ptr, key As CFStringRef) As Ptr dict = dictionaryWithObject(dict, numb, "NSImageInterlaced") // PNG出力(NSPNGFileType = 4) Declare Function representationUsingType Lib "Cocoa" Selector "representationUsingType:properties:" (receiver As Ptr, type As Integer, prop As Ptr) As Ptr Dim pngData As Ptr = representationUsingType(bitmapImageRep, 4, dict) // clean up Declare Sub release Lib "Cocoa" Selector "release" (receiver As Ptr) release(bitmapImageRep) // PNGを返す return pngData End Function
- 以下をWindow1にペースト
Private Sub PNGtoFile() // テキストの描画結果をPNG形式で取得 Dim pngData As Ptr = PNGfromAttrString() // ファイル出力(ここではDesktopにtest.pngという名前で出力している。必要なら変更して下さい。) Declare Function writeToFile Lib "Cocoa" Selector "writeToFile:atomically:" (receiver As Ptr, path As CFStringRef, atm As Boolean) As Boolean Dim ret As Boolean = writeToFile(pngData, SpecialFolder.Desktop.Child("test.png").NativePath, true) End Sub
- 以下をWindow1にペースト
注)1バイトずつコピーの代替法については、こちらを参照。(2019.03.26)Private Function PNGtoPicture(pngData As Ptr) as Picture // PNGデータの長さを取得 Declare Function length Lib "Cocoa" Selector "length" (receiver As Ptr) As Integer Dim lng As Integer = length(pngData) // バイト列の抽出 Declare Function bytes Lib "Cocoa" Selector "bytes" (receiver As Ptr) As Ptr Dim bstream As Ptr = bytes(pngData) // バイト列をMemoryBlockに1バイトずつコピー Dim i As Integer Dim mb As new MemoryBlock(lng) for i=0 to lng-1 mb.Byte(i) = bstream.Byte(i) next // MemoryBlockを使ってPictureを生成 Dim pic As Picture = Picture.FromData(mb) // Pictureを返す return pic End Function
- 以下をWindow1にペースト
Private Sub ShowPreviewDlg() Dim dlg As new Window2 // テキストの描画結果をPNG形式で取得 Dim pngData As Ptr = PNGfromAttrString() // PNGの中身をPictureにコピー pic=PNGtoPicture(pngData) // ダイアログ表示 dlg.ShowModalWithin self // ダイアログ破棄 dlg.Close End Sub
- 以下をWindow1にペースト
Protected Sub ToggleVtext() // トグル動作 Dim flg As Integer if vFlg=false then vFlg=true flg=1 else vFlg=false flg=0 end if // NSTextViewの取得 declare function documentView lib "Cocoa" selector "documentView" (obj_id as Integer) as Ptr // Return NSTextView* Dim pnt1 As Ptr = documentView(TextArea1.Handle) // NSTextViewに対する縦書き属性の設定(NSTextLayoutOrientationVertical = 1) declare sub setLayoutOrientation lib "Cocoa" selector "setLayoutOrientation:" (receiver as Ptr, flag as Integer) setLayoutOrientation(pnt1, flg) // NSTextStorageの取得 declare function textStorage lib "Cocoa" selector "textStorage" (obj_id as Ptr) As Ptr // Return NSTextStorage* Dim pnt2 As Ptr = textStorage(pnt1) // NSTextStorageに対する縦書き属性の設定 pnt2 = AttrStringSetVertical(pnt2,vFlg) End Sub
- 以下をWindow1にペースト(できなければプロパティに、名前:pic、データ型:Picture、を追加)
Public Property pic as Picture
- 以下をWindow1にペースト(できなければプロパティに、名前:vFlg、データ型:Boolean、を追加)
Protected Property vFlg as Boolean = false
- 新規ウィンドウを作成(名前は、ここではデフォルトの「Window2」とした。)
- Window2にCanvas(名前:Canvas1)とPushButton(名前:PushButton1)をドラッグ
- 以下をCanvas1にペースト(できなければ、Sub - Endの間をPaintイベントに記述)
Sub Paint(g As Graphics, areas() As REALbasic.Rect) Handles Paint // 以下の指定では縦横比は維持されません。必要なら修正してください。 g.DrawPicture(Window1.pic,0,0,me.Width,me.Height,0,0,Window1.pic.Width,Window1.pic.Height) End Sub
- 以下をPushButton1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
Sub Action() Handles Action Hide End Sub
- 他に、NSMakeRange/NSMakeRect(メソッド)、NSRange/NSRect(構造体)が必要ですが、それらはmacoslibからコピーさせて頂きました。(上記Window1か、汎用で使いたい場合は適当なモジュールに、コピーする。)
おわりに
1バイトずつコピーしているので、サイズが大きくなった時の実用性は未知数です。一括してコピーする方法は今のところ確認できていません。
(MemoryBlockを別のMemoryBlockにコピーする際は、cのmemcpyが使えるようなのですが、新たにMemoryBlockを作る場合は、うまくいかない?)
(上図ではちょっと分かりづらいですが)アンダーラインがTextAreaでは右側につくのに対し、プレビューでは左についてしまうのは参考サイト(1)の指摘通りです。
また、画像化の際はサイズをTextAreaと一致させている筈なのですが、長文を入力すると、改行位置や行数が微妙に異なってしまいます。マージンや間隔が異なるのか、設定不足やミスがあるのか、この辺りは今後の課題と言えます。
追記:ToggleVtext()内でAttrStringSetVertical()を呼び出していますが、文字入力だけならこの処理は不要です。本来はPNGfromAttrString()辺りで呼び出すべきものですが、そうするとプレビュー後、文字が横を向くという現象が発生するため、暫定的にこうしています。また、AttrStringSetVertical()に戻り値がありますが、試行錯誤の名残で、なくても問題ありません。
お世話になったサイト
貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)
参考サイト(1):のっけうしの徒然なる開発日誌: Cocoaでの縦書きの実現について
更新履歴
2019.08.22 Cocoaの描画結果を画像データに変換する、に追記を追加
2019.03.26 1バイトずつコピーの代替法へのリンクを追加
2018.04.02 おわりに、に追記を追加
2018.03.22 新規作成
[Home] [MacSoft] [Donation] [History] [Privacy Policy] [Affiliate Policy]