LuaAppMaker で書式付きテキストを扱うため、wxRichTextCtrl
を使ってみようと思った。wxLua には wxRichTextCtrl
が実装されていなかったので、頑張ってパッチを作りました(プルリクエスト中)。こんな風になります。なかなかのものですよね。
Windows だとこんな感じ。文字がギザギザなのは気になります。VMWare Fusion 上で動かしているためかもしれません。
プログラムコードは 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()
問題はここからです。「上付き」ボタンを押したとき、何をすればいい? 「次に入力する文字のスタイルを指定する」のだから 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()
おーいけてるやん。単に「これから入力する文字属性」を指定するだけでなく、カーソルを動かしても「その位置の文字属性」がちゃんと反映される。つまり、上の図でカーソルを左に戻して、"super" の直後に文字を入力すると、それは上付きになる。
そうすると、トグルボタンは「これから入力する文字属性」が上付きかどうかを表示するようにしないといけないな。「現在のカーソル位置」が変更されたことを捕まえるにはどうすればいい? 試してみたところ、wxEVT_KEY_UP
と wxEVT_LEFT_UP
を捕まえればよさそう。これらのイベントは、キー押下やマウスボタン押下のイベントを処理した「後」に発生するので、「現在のカーソル位置」を調べるタイミングとして適している。
カーソル位置がわかったところで、「これから入力する文字属性」はどうやって調べたらいいんだ? ドキュメントを探してもわからず、結局ソースを読んだ。wxWidgets はこういうところがね……正解は次の通り。
local pos = r:GetAdjustedCaretPosition(r:GetCaretPosition())
local flag, attr = r:GetUncombinedStyle(pos)
GetCaretPosition()
は「現在のキャレットの位置」を表す。「キャレットの位置」は、「カーソルのある位置の直前」を表している。例えば、カーソルが文字列の先頭にあれば、「キャレットの位置」は -1
になる。また、GetAdjustedCaretPosition()
は、キャレットが段落の先頭にある場合に、文字属性は「前の文字」ではなく「段落の先頭」で得るべきなので、それを補正するためのもの。こんなの、ドキュメントを何回読んでも絶対わからない。src/richtext/richtextctrl.cpp
の SetDefaultStyleFromCursorStyle()
関数の実装を見て初めてわかった。ハードル高いな!
結局こうなりました。
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