2022年08月28日

wxRichTextCtrl を使う

 LuaAppMaker で書式付きテキストを扱うため、wxRichTextCtrl を使ってみようと思った。wxLua には wxRichTextCtrl が実装されていなかったので、頑張ってパッチを作りました(プルリクエスト中)。こんな風になります。なかなかのものですよね。

20220828-1.jpg

 Windows だとこんな感じ。文字がギザギザなのは気になります。VMWare Fusion 上で動かしているためかもしれません。

20220828-2.jpg

 プログラムコードは wxRichTextCtrl のサンプルをほぼそのまま使っています。「上付き・下付き」のテストをしたかったので、そこだけ追加しています。

  frame = wx.wxFrame(wx.NULL, wx.wxID_ANY, "wxRichTextCtrl Example", wx.wxDefaultPosition, wx.wxSize(480, 400), bit32.bor(wx.wxDEFAULT_FRAME_STYLE, wx.wxFULL_REPAINT_ON_RESIZE))
  r = wx.wxRichTextCtrl(frame, wx.wxID_ANY, "", wx.wxPoint(0, 0), wx.wxSize(480, 400), wx.wxVSCROLL + wx.wxHSCROLL + wx.wxNO_BORDER + wx.wxWANTS_CHARS)
  textFont = wx.wxFont(12, wx.wxROMAN, wx.wxNORMAL, wx.wxNORMAL)
  boldFont = wx.wxFont(12, wx.wxROMAN, wx.wxNORMAL, wx.wxBOLD)
  italicFont = wx.wxFont(12, wx.wxROMAN, wx.wxITALIC, wx.wxNORMAL)
  font = wx.wxFont(12, wx.wxROMAN, wx.wxNORMAL, wx.wxNORMAL)
  r:SetFont(font)
  r:BeginSuppressUndo()
  r:BeginParagraphSpacing(0, 20)
  r:BeginAlignment(wx.wxTEXT_ALIGNMENT_CENTRE)
  r:BeginBold()
  r:BeginFontSize(14)
  r:WriteText("Welcome to wxRichTextCtrl, a wxWidgets control for editing and presenting styled text and images")
  r:EndFontSize()
  r:Newline()
  r:BeginItalic()
  r:WriteText("by Julian Smart")
  r:EndItalic()
  r:EndBold()
  r:Newline()
  r:WriteImage(wx.wxBitmap("image/horse.xpm"), wx.wxBITMAP_TYPE_XPM)
  r:EndAlignment()
  r:Newline()
  r:Newline()
  r:WriteText("What can you do with this thing? ")
  r:WriteImage(wx.wxBitmap("image/smile.xpm"), wx.wxBITMAP_TYPE_XPM)
  r:WriteText(" Well, you can change text ")
  r:BeginTextColour(wx.wxColour(255, 0, 0))
  r:WriteText("colour, like this red bit.")
  r:EndTextColour()
  r:BeginTextColour(wx.wxColour(0, 0, 255))
  r:WriteText(" And this blue bit.")
  r:EndTextColour()
  r:WriteText(" Naturally you can make things ")
  r:BeginBold()
  r:WriteText("bold ")
  r:EndBold()
  r:BeginItalic()
  r:WriteText("or italic ")
  r:EndItalic()
  r:BeginUnderline()
  r:WriteText("or underlined.")
  r:EndUnderline()
  local pos1 = r:GetLastPosition()
  local attr = wx.wxRichTextAttr()
  r:WriteText("\nThis is superscript,") -- 上付き
  attr:SetTextEffects(wx.wxTEXT_ATTR_EFFECT_SUPERSCRIPT)
  attr:SetFlags(wx.wxTEXT_ATTR_EFFECTS)
  attr:SetTextEffectFlags(wx.wxTEXT_ATTR_EFFECT_SUPERSCRIPT)
  r:SetStyle(pos1 + 9, pos1 + 20, attr)
  pos1 = r:GetLastPosition()
  r:WriteText(" and this is subscript.\n") -- 下付き
  attr = wx.wxRichTextAttr()
  attr:SetTextEffects(wx.wxTEXT_ATTR_EFFECT_SUBSCRIPT)
  attr:SetFlags(wx.wxTEXT_ATTR_EFFECTS)
  attr:SetTextEffectFlags(wx.wxTEXT_ATTR_EFFECT_SUBSCRIPT)
  r:SetStyle(pos1 + 13, pos1 + 22, attr)
  r:BeginFontSize(14)
  r:WriteText(" Different font sizes on the same line is allowed, too.")
  r:EndFontSize()
  r:WriteText(" Next we'll show an indented paragraph.")
  r:BeginLeftIndent(60)
  r:Newline()
  r:WriteText("Indented paragraph.")
  r:EndLeftIndent()
  r:Newline()
  r:WriteText("Next, we'll show a first-line indent, achieved using BeginLeftIndent(100, -40).")
  r:BeginLeftIndent(100, -40)
  r:Newline()
  r:WriteText("It was in January, the most down-trodden month of an Edinburgh winter.")
  r:EndLeftIndent()
  r:Newline()
  r:WriteText("Numbered bullets are possible, again using subindents:")
  r:BeginNumberedBullet(1, 100, 60)
  r:Newline()
  r:WriteText("This is my first item. Note that wxRichTextCtrl doesn't automatically do numbering, but this will be added later.")
  r:EndNumberedBullet()
  r:BeginNumberedBullet(2, 100, 60)
  r:Newline()
  r:WriteText("This is my second item.")
  r:EndNumberedBullet()
  r:Newline()
  r:WriteText("The following paragraph is right-indented:")
  r:BeginRightIndent(200)
  r:Newline()
  r:WriteText("It was in January, the most down-trodden month of an Edinburgh winter. An attractive woman came into the cafe, which is nothing remarkable.")
  r:EndRightIndent()
  r:Newline()
  tabs = {400, 600, 800, 1000}
  attr = wx.wxTextAttr()
  attr:SetFlags(wx.wxTEXT_ATTR_TABS)
  attr:SetTabs(tabs)
  r:SetDefaultStyle(attr)
  r:WriteText("This line contains tabs:\tFirst tab\tSecond tab\tThird tab")
  r:Newline()
  r:WriteText("Other notable features of wxRichTextCtrl include:")
  r:Newline()
  r:BeginSymbolBullet("*", 100, 60)
  r:WriteText("Compatibility with wxTextCtrl API\n")
  r:EndSymbolBullet()
  r:WriteText("Note: this sample content was generated programmatically from within the MyFrame constructor in the demo. The images were loaded from inline XPMs. Enjoy wxRichTextCtrl!")
  r:EndSuppressUndo()
  frame:Show()

 wxRichTextCtrl は、ドキュメントが絶望的に不足しています。wxRichText... という名前のクラスがたくさんあるんだけど、それが何をするものなのか、さっぱりわかりません。少しずつ実験して紐解いていくしかないのかな。

 例えば、「上付き・下付きを含む文字列をユーザーが入力して、その結果を取得する」というお題を考えてみる。wxRichTextCtrl の使い方としては最も基本的なところです。「上付き」「下付き」というボタンと、入力用の wxRichTextCtrl を表示するところまでは、秒で書けます。

  frame = wx.wxFrame(wx.NULL, wx.wxID_ANY, "wxRichTextCtrl Ex.2", wx.wxDefaultPosition, wx.wxSize(400, 200), bit32.bor(wx.wxDEFAULT_FRAME_STYLE, wx.wxFULL_REPAINT_ON_RESIZE))
  btn1 = wx.wxToggleButton(frame, 1, "上付き", wx.wxPoint(10, 10), wx.wxSize(60, 20))
  btn2 = wx.wxToggleButton(frame, 2, "下付き", wx.wxPoint(80, 10), wx.wxSize(60, 20))
  r = wx.wxRichTextCtrl(frame, 3, "", wx.wxPoint(10, 40), wx.wxSize(380, 28), wx.wxTE_PROCESS_ENTER)
  r:ShowScrollbars(-1, -1)
  font = wx.wxFont(12, wx.wxROMAN, wx.wxNORMAL, wx.wxNORMAL)
  r:SetFont(font)
  frame:Show()

20220828-3.jpg

 問題はここからです。「上付き」ボタンを押したとき、何をすればいい? 「次に入力する文字のスタイルを指定する」のだから wxRichTextCtrl:SetDefaultStyle() かな? スタイルを覚えておく wxRichTextAttr オブジェクトを用意して、ボタンが押されたらそれを更新して wxRichTextCtrl:SetDefaultStyle() を呼んでみる。

bit = require("bit")
attr = wx.wxRichTextAttr()

function main()
  frame = wx.wxFrame(wx.NULL, wx.wxID_ANY, "wxRichTextCtrl Ex.2", wx.wxDefaultPosition, wx.wxSize(400, 200), bit32.bor(wx.wxDEFAULT_FRAME_STYLE, wx.wxFULL_REPAINT_ON_RESIZE))
  btn1 = wx.wxToggleButton(frame, 1, "上付き", wx.wxPoint(10, 10), wx.wxSize(60, 20))
  btn2 = wx.wxToggleButton(frame, 2, "下付き", wx.wxPoint(80, 10), wx.wxSize(60, 20))
  btn1:Connect(wx.wxEVT_TOGGLEBUTTON, function (event) DoToggleButton(btn1, event) end)
  r = wx.wxRichTextCtrl(frame, 3, "", wx.wxPoint(10, 40), wx.wxSize(380, 28), wx.wxTE_PROCESS_ENTER)
  r:ShowScrollbars(-1, -1)
  font = wx.wxFont(12, wx.wxROMAN, wx.wxNORMAL, wx.wxNORMAL)
  attr:SetFont(font)
  attr:SetFlags(wx.wxTEXT_ATTR_EFFECTS)
  attr:SetTextEffectFlags(wx.wxTEXT_ATTR_EFFECT_SUPERSCRIPT + wx.wxTEXT_ATTR_EFFECT_SUBSCRIPT)
  r:SetDefaultStyle(attr)
  frame:Show()
end

function DoToggleButton(btn, event)
  if btn == btn1 then
    if btn:GetValue() then
      attr:SetTextEffects(bit.bor(attr:GetTextEffects(), wx.wxTEXT_ATTR_EFFECT_SUPERSCRIPT))
    else
      attr:SetTextEffects(bit.band(attr:GetTextEffects(), bit.bnot(wx.wxTEXT_ATTR_EFFECT_SUPERSCRIPT)))
    end
  end
  r:SetDefaultStyle(attr)
end

main()

20220828-4.jpg

 おーいけてるやん。単に「これから入力する文字属性」を指定するだけでなく、カーソルを動かしても「その位置の文字属性」がちゃんと反映される。つまり、上の図でカーソルを左に戻して、"super" の直後に文字を入力すると、それは上付きになる。

 そうすると、トグルボタンは「これから入力する文字属性」が上付きかどうかを表示するようにしないといけないな。「現在のカーソル位置」が変更されたことを捕まえるにはどうすればいい? 試してみたところ、wxEVT_KEY_UPwxEVT_LEFT_UP を捕まえればよさそう。これらのイベントは、キー押下やマウスボタン押下のイベントを処理した「後」に発生するので、「現在のカーソル位置」を調べるタイミングとして適している。

 カーソル位置がわかったところで、「これから入力する文字属性」はどうやって調べたらいいんだ? ドキュメントを探してもわからず、結局ソースを読んだ。wxWidgets はこういうところがね……正解は次の通り。

  local pos = r:GetAdjustedCaretPosition(r:GetCaretPosition())
  local flag, attr = r:GetUncombinedStyle(pos)

 GetCaretPosition() は「現在のキャレットの位置」を表す。「キャレットの位置」は、「カーソルのある位置の直前」を表している。例えば、カーソルが文字列の先頭にあれば、「キャレットの位置」は -1 になる。また、GetAdjustedCaretPosition() は、キャレットが段落の先頭にある場合に、文字属性は「前の文字」ではなく「段落の先頭」で得るべきなので、それを補正するためのもの。こんなの、ドキュメントを何回読んでも絶対わからない。src/richtext/richtextctrl.cppSetDefaultStyleFromCursorStyle() 関数の実装を見て初めてわかった。ハードル高いな!

 結局こうなりました。

function main()
  -- 中略 --
  r:Connect(wx.wxEVT_KEY_UP, function (event) DoRichTextEvent(r, event) end)
  r:Connect(wx.wxEVT_LEFT_UP, function (event) DoRichTextEvent(r, event) end)
  -- 中略 --
end

function DoRichTextEvent(r, event)
  local t = event:GetEventType()
  local pos = r:GetAdjustedCaretPosition(r:GetCaretPosition())
  local flag, a = r:GetUncombinedStyle(pos)  -- 次に入力する位置の属性
  if flag then
    local ef = a:GetTextEffects()
    if bit.band(ef, wx.wxTEXT_ATTR_EFFECT_SUPERSCRIPT) ~= 0 then
      btn1:SetValue(true)  -- トグルボタン ON
    else
      btn1:SetValue(false) -- トグルボタン OFF
    end
  end
  event:Skip()
end

20220828-5.jpg

Posted at 2022年08月28日 22:14:30
email.png