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について説明しましょう。


ソースコードについて

GitHubに登録しました。今回のコードは、Section04となります。