Go言語(golang)でTUIアプリを作ろう ( その4 Buttonを配置する )
tviewのButtonオブジェクトとイベントを理解しよう。
それでは、最初のWidgetとしてButtonを使ってみましょう。ユーザーインタフェースにおいて、Buttonは、特に説明する必要もない一般的な構成部品です。
[1] Buttonオブジェクトの生成
tviewのButtonは、下記のように生成します。
func NewButton(label string) *Button
// 使用例
button := tview.NewButton(label).SetSelectedFunc(func() {
footer.SetText("Pushed T")
})
labelは、Button上に表示するテキストになります。SetSelectedFuncで、押されたときの処理を記述できます。
また、カラーなどの属性も指定することが出来ます。今回は、下記のようなButton作成処理を定義しておきます。tviewでは、メソッドチェーン形式を採用していますので、連続して属性を設定することが出来ます。
func myButton(label string) *tview.Button {
button := tview.NewButton(label)
button.SetBackgroundColor(tcell.ColorBlack)
button.SetLabelColor(tcell.ColorYellow).SetLabelColorActivated(tcell.ColorBlack).SetBackgroundColorActivated(tcell.ColorYellow)
return button
}
なぜ、"button.SetBackgroundColor(tcell.ColorBlack)” だけを独立させているかは、最後に説明しましょう。
[2] 画面に配置してみる。
それでは、Header部(1行を想定)にButtonを3つ配置してみます。こんな画面を作成していきます。
labelは、画面幅が小さくなっても1行に入るよう、1文字に限定してみました。
同時に、ButtonのEventに対応するルーティンもそれぞれ用意しておきます。Button生成とEventは、こんなコーディングになるでしょう。
btnT := myButton("<T>").SetSelectedFunc(func() {
footer.SetText("Pushed T")
})
btnS := myButton("<S>").SetSelectedFunc(func() {
footer.SetText("Pushed S")
})
btnQ := myButton("<Q>").SetSelectedFunc(func() {
app.Stop()
})
(1) Header上のコンテナにButtonを配置する。
それでは、3つのButoonをHeader部に追加していきます。
今回の処理では、コンテナとして”Flex”を使用することとします。Flexでは縦、あるいは横の方向を選択して、Primitiveを配置していきます。
func NewFlex() *Flex
Flexコンテナ上には、ButtonをAddItemメソッドで載せていきます。
func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) *Flex
// 使用例
header := tview.NewFlex()
header.AddItem(btnT, 0, 1, true)
header.AddItem(btnS, 0, 1, true)
header.AddItem(btnQ, 0, 1, true)
- “fixedSize” は、表示幅を指定します。"0” を指定した場合、次のパラメータに従い、tviewが決定します。
- “proportion” は、重みです。上の例では、すべて “1” ですので、画面の幅を三等分するようにButtonが表示されます。
- 最後の “focus” は言うまでもなく、選択可能という意味です。
Flexの表示方向は、Defaultで横(FlexColumn)ですので、今回は指定しませんが、縦に表示したい場合は、
func (f *Flex) SetDirection(direction int) *Flex
で、 “FlexRow” を指定します。
// 使用例
body := tview.NewFlex().SetDirection(tview.FlexRow)
(2) Footer部にメッセージ表示する。
Frameの下部(1行を想定)にFooterを用意し、Buttonが押されたときには「“Pushed ‘T’"」のようなメッセージを表示することとしましょう。
以上をまとめると、下記のコードとなります。
//
// s04_01.go
//
package main
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
func myButton(label string) *tview.Button {
button := tview.NewButton(label)
button.SetBackgroundColor(tcell.ColorBlack)
button.SetLabelColor(tcell.ColorYellow).SetLabelColorActivated(tcell.ColorBlack).SetBackgroundColorActivated(tcell.ColorYellow)
return button
}
func doformat() {
app := tview.NewApplication()
header := tview.NewFlex()
footer := tview.NewTextView().SetText("これはフッター")
body := tview.NewTextView().SetText("Buttonテスト").SetTextAlign(tview.AlignCenter)
btnT := myButton("<T>").SetSelectedFunc(func() {
footer.SetText("Pushed T")
})
btnS := myButton("<S>").SetSelectedFunc(func() {
footer.SetText("Pushed S")
})
btnQ := myButton("<Q>").SetSelectedFunc(func() {
app.Stop()
})
header.AddItem(btnT, 0, 1, true)
header.AddItem(btnS, 0, 1, true)
header.AddItem(btnQ, 0, 1, true)
main := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(header, 1, 0, true).
AddItem(body, 0, 1, false).
AddItem(footer, 1, 0, false)
if err := app.SetRoot(main, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}
func main() {
doformat()
}
では、実際に動かしてみましょう。
go run s04_01.go
あれれ、矢印キーやTabでButtonのFocusが移りませんね。
“<Q>” を選択できないので、終了することも出来ません。仕方がないので、 “CTRL+C” で処理を中断してください。
実は、tviewでは「Focusの移動はコーディングが必要」なのです。
Python Urwidでは自動で出来るので、tviewでも出来るものと思いこんでいたのですが、これには意表を突かれました。また、ひとつtviewの問題点が出てしまいましたね。Auto focusが効かないのは辛いなあ(笑)。
(3) Focus設定メソッドを追加する。
しかたがないので、コードを追加しましょう。focusを移動するメソッド “mySetFocus“と、矢印キーの入力処理になります。
func mySetFocus(app *tview.Application, elements []tview.Primitive, reverse bool)
elementsには、Focus対象オブジェクトの配列を渡します。
reverseは、Focusの移動方向です。"false“で進み、”true”で戻るわけです。
Buttonが配置されてるコンテナ(ここではheaderオブジェクト)のSetInputCaptureメソッドに、矢印キーによるFocus移動処理を記述します。
header.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyRight:
mySetFocus(app, buttons, false)
case tcell.KeyLeft:
mySetFocus(app, buttons, true)
:
:
最終的なコードを下記に示します。
//
// s04_02.go
//
package main
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
func myButton(label string) *tview.Button {
button := tview.NewButton(label)
button.SetBackgroundColor(tcell.ColorBlack)
button.SetLabelColor(tcell.ColorYellow).SetLabelColorActivated(tcell.ColorBlack).SetBackgroundColorActivated(tcell.ColorYellow)
return button
}
func mySetFocus(app *tview.Application, elements []tview.Primitive, reverse bool) {
for i, el := range elements {
if !el.HasFocus() {
continue
}
if reverse {
i = i - 1
if i < 0 {
i = len(elements) - 1
}
} else {
i = i + 1
i = i % len(elements)
}
app.SetFocus(elements[i])
return
}
app.SetFocus(elements[0])
}
func doformat() {
app := tview.NewApplication()
header := tview.NewFlex()
footer := tview.NewTextView().SetText("これはフッター")
body := tview.NewTextView().SetText("Buttonテスト").SetTextAlign(tview.AlignCenter)
btnT := myButton("<T>").SetSelectedFunc(func() {
footer.SetText("Pushed T")
})
btnS := myButton("<S>").SetSelectedFunc(func() {
footer.SetText("Pushed S")
})
btnQ := myButton("<Q>").SetSelectedFunc(func() {
app.Stop()
})
buttons := []tview.Primitive{
btnT,
btnS,
btnQ,
}
header.AddItem(btnT, 0, 1, true)
header.AddItem(btnS, 0, 1, true)
header.AddItem(btnQ, 0, 1, true)
header.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyRight:
mySetFocus(app, buttons, false)
case tcell.KeyLeft:
mySetFocus(app, buttons, true)
case tcell.KeyRune:
switch event.Rune() {
case 'q', 'Q':
app.Stop()
}
}
body.SetText(string(event.Rune()))
return event
})
main := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(header, 1, 0, true).
AddItem(body, 0, 1, false).
AddItem(footer, 1, 0, false)
if err := app.SetRoot(main, true).EnableMouse(true).Run(); err != nil {
panic(err)
}
}
func main() {
doformat()
}
では、再度動かしてみましょう。
go run s04_02.go
左右矢印キーで、フォーカスを移動してみましょう。また、Enterキーを叩くとFooter部にメッセージが表示されることも確認しておきます。
これで、所定の動作になりましたね。
[3] golangの継承について
先にButton生成メソッドを下記のように記述していました。
func myButton(label string) *tview.Button {
button := tview.NewButton(label)
button.SetBackgroundColor(tcell.ColorBlack)
button.SetLabelColor(tcell.ColorYellow).SetLabelColorActivated(tcell.ColorBlack).SetBackgroundColorActivated(tcell.ColorYellow)
return button
}
上記をメソッドチェーンで1つにまとめると、どうなるでしょうか。
func myButton(label string) *tview.Button {
button := tview.NewButton(label).SetBackgroundColor(tcell.ColorBlack).SetLabelColor(tcell.ColorYellow).SetLabelColorActivated(tcell.ColorBlack).SetBackgroundColorActivated(tcell.ColorYellow)
return button
}
実行してみましょう。
$ go run s04_02.go
# command-line-arguments
./s04_02.go:16:71: tview.NewButton(label).Box.SetBackgroundColor(tcell.ColorBlack).SetLabelColor undefined (type *tview.Box has no field or method SetLabelColor)
エラーになりますね。
先に、tviewの基本構造を説明した際、基本的にすべてのオブジェクトは、Boxを継承していると述べました。
実は、 “SetBackgroundColor” というメソッドは、Boxのメソッドなのです。このSetterは、Boxオブジェクトを返しますから、次の「"SetLabelColor”なんてメソッドは、Boxにないぞ」と言っているわけです。このあたりは、「golangが純粋のオブジェクト指向ではない」所以ですね。
これで、Buttonオブジェクトの説明はおしまい。次回は、tviewのListについて説明しましょう。