ホームページ開発ツール>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形式では実用的なサイズに収まっている。

 サンプルの仕様

 以上を踏まえて今回は、縦書きの編集機能と、内容のプレビュー表示およびファイル出力するものを試してみることにしました。
 仕様は以下の通りとしました。

 Xojoでの実装
【ソースコードのコピー&ペーストについて】
ソースコード(グレー背景部分の全文)をコピーし、指定のウィンドウ/クラスにペーストすると、(新規作成して名前等を個別にコピー&ペーストしなくても)復元されます。
ただし、この方法は、メソッドでは問題ないようですが、イベント/アクション/プロパティでは不安定?なので、ペーストできない場合は、各項目のカッコ内を適用して下さい。
  1. 前回プロジェクトをベースとする
  2. 以下をWindow1のOpenイベント内に追加(setRulerVisibleの直後)
      // インスペクターバー追加による高さの増分がウィンドウ高に反映されないことへの対策(これでうまくいくようだ。)
      me.Height=me.Height
    
  3. Toolbar1に、以下のアイテムを追加。
    ToolItem6(スペース)/ToolItem7(縦書き Style:Toggle Button)/ToolItem8(スペース)/ToolItem9(プレビュー)/ToolItem10(ファイル書き出し)
    注)アイコンは適当な画像を別途用意します。(なくても、機能の確認はできます。)
  4. 以下をWindow1のToolbar11のActionイベント内に追加(end selectの直前)
      case "ToolItem7"  // 縦書き(トグル)
        ToggleVtext()
        
      case "ToolItem9"  // プレビュー
        ShowPreviewDlg()
        
      case "ToolItem10"  // ファイル書き出し
        PNGtoFile()
    
  5. 以下を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
    
  6. 以下を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
    
  7. 以下を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
    
  8. 以下を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
    
  9. 以下を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
    
  10. 以下をWindow1にペースト
    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
    
    注)1バイトずつコピーの代替法については、こちらを参照。(2019.03.26)
  11. 以下を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
    
  12. 以下を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
    
  13. 以下をWindow1にペースト(できなければプロパティに、名前:pic、データ型:Picture、を追加)
    Public Property pic as Picture
    
  14. 以下をWindow1にペースト(できなければプロパティに、名前:vFlg、データ型:Boolean、を追加)
    Protected Property vFlg as Boolean = false
    
  15. 新規ウィンドウを作成(名前は、ここではデフォルトの「Window2」とした。)
  16. Window2にCanvas(名前:Canvas1)とPushButton(名前:PushButton1)をドラッグ
  17. 以下を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
    
  18. 以下をPushButton1にペースト(できなければ、Sub - Endの間をActionイベントに記述)
    Sub Action() Handles Action
      Hide
    End Sub
    
  19. 他に、NSMakeRange/NSMakeRect(メソッド)、NSRange/NSRect(構造体)が必要ですが、それらはmacoslibからコピーさせて頂きました。(上記Window1か、汎用で使いたい場合は適当なモジュールに、コピーする。)
 実行してみたところ、縦書きが機能することを確認しました。
S Shot1


 おわりに

 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]