Go言語(golang)でTUIアプリを作ろう ( その9 DialogBoxの利用 )
新たなPrimitiveとして、”Dialog"を作成してみる。
アプリケーション画面設計を行う際には、データを選択したり、条件入力を行う機能が不可欠です。一般にそのような補助入力画面は、DialogBoxという形式で実装されています。
IT用語辞典では、
ダイアログボックスとは、コンピュータの操作画面で、利用者に何らかの入力を促すために表示される矩形の領域のこと。小さなウィンドウの形で表示されることが多い。“dialog” は「対話」の意。
と定義されています。
多くのUIツールキットでは、このようなDialogBoxが標準ツールとして提供されていますが、残念ながらtviewにはないのです。
となれば、しかたがない。なんとかDialogBoxもどきを自作しないといけません。
[1] DialogBoxをどう作るか
tviewに「DialogBoxは存在しない」と述べましたが、それに近いものが実装されています。
(1) tviewのModal
それが、ModalというPrimitiveです。Modalでは、下記のような画面を作成することができます。
コードは、下記になります。
// Demo code for the Modal primitive.
package main
import (
"github.com/rivo/tview"
)
func main() {
app := tview.NewApplication()
modal := tview.NewModal().
SetText("Do you want to quit the application?").
AddButtons([]string{"Quit", "Cancel"}).
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
if buttonLabel == "Quit" {
app.Stop()
}
})
if err := app.SetRoot(modal, false).EnableMouse(true).Run(); err != nil {
panic(err)
}
}
見ていただけばわかりますが、これはいわゆるMessageBoxです。Modalで画面に表示できるのは、一行のテキストと複数のButtonのみで、汎用性のない構成になってしまっています。本来であれば、ベースとなるDialogBoxをまず提供し、その一つの要素としてMessgaeBoxが作成できるよう設計すべきなのです。
(2) Modalを改造し、DialogBoxへ
tviewのソースコードを眺めながら変更方法を模索しました。
Modalでは、Frame上にFormを配置することで画面を作成していますが、このFrame上のFormをFlexコンテナに変更してやれば、汎用化することができそうです。
(3) Primitiveの作成手法
それでは、この考え方に沿って汎用性のあるDialogBoxを作成していきましょう。
Tviewにおける部品は、すべてPrimitiveと呼ばれますが、その作成については、ここにドキュメントがあり、その中盤の「Writing Primitives」という部分に具体的な手法が書かれています。簡単にまとめれば、「新規作成には、Primitive interfaceを実装しないといけないが、それにはBoxをサブクラス化するとよい」ということのようです。Go言語(golang)でTUIアプリを作ろう(その3 tviewの基本構造)で示した概念図のとおりです。
[2] MyDialogの作成
それでは、実際にDialogBoxを作成していきましょう。
まず、新たにパッケージmytviewを設定して、そこにMyDialogとして作成、下記のメソッドを定義しました。
(1) Dialogの生成
func NewMyDialog(width int, height int, ratioX int, ratioY int) *MyDialog
この関数でDialogを生成します。
widthとheightは、表示するDialogのサイズ。
ratioXとratioYは、表示位置を指示します。画面の中央が2、1で下に、3以上で上に移動します。
(2) コンテナのセット
func (m *MyDialog) SetFlex(flex *tview.Flex) *MyDialog
表示する画面を定義したFlexコンテナをDialogにセット。
(3) コンテナから入力されたデータの取り込み
func (m *MyDialog) SetParm(label string, input string) *MyDialog
Dialogで入力されたデータを取り込みます。この情報は、下記のSetDoneFuncのパラメータとなります。
(4) 処理ルーティンの設定
func (m *MyDialog) SetDoneFunc(handler func(buttonLabel string, input string)) *MyDialog
プログラムで入力データを扱う関数の定義です。
(5) Dialogの描画
func (m *MyDialog) Draw(screen tcell.Screen)
Dialogの描画ルーティン。NewMyDialogで設定されたサイズ、及び位置に画面を描画します。
→詳細は、ソース(mydialog.go)を参照してください。
[3] MyDialogの利用
Dialog上のFlexコンテナに具体的なフィールドを設定して、実際のDialogBoxとして動作するよう、mywidgetに3つの関数を追加します。
関数のパラメータですが、width、height、ratioX、ratioYは、NewMyDialogに引き渡すもの、borderは枠線を設定するかどうかのフラグで、すべての関数共通となります。
(1) SelectBox
func MySelectBox(items []string, width int, height int, ratioX int, ratioY int, current int, border bool) *mytview.MyDialog {
itemsは、リスト表示する文字列、currentは、初期選択対象となる項目の番号。最上位は0になります。
(2) InputBox
func MyInputDialog(app *tview.Application, search string, width int, height int, ratioX int, ratioY int, border bool) *mytview.MyDialog {
searchは、初期表示する文字列です。Buttonは、OKとCancelに決め打ちしています。
(3) MessageBox
func MyMessageBox(app *tview.Application, message string, width int, height int, ratioX int, ratioY int, border bool) *mytview.MyDialog {
messageは、DialogBoxに表示するメッセージ。Buttonは、YesとNoに決め打ちしています。
ボーダー枠の問題
上の画面を見ていただくとわかりますが、DialogBoxの枠線が連続していませんね。これについては、下記に情報があります。
これによれば、下記の環境変数を指定すると良いとのこと。
export LC_CTYPE="en_US.UTF-8"
確かに、枠線がきれいに表示されます。これは文字列の描画に関わる環境変数のようですが、英語モードにある1バイト罫線や枠線との関係でしょうか。
[4] プログラムでの使用
それでは、実際にプログラムでDialogBoxを使用してみましょう。
今回は、List画面(list.go)に、3つのボタンを追加し、そこから各DialogBoxを呼び出しています。
btnT := myButton("<T>").SetSelectedFunc(func() {
self.getTable(pages, common)
})
:
btnS := myButton("<S>").SetSelectedFunc(func() {
self.getSearch(pages, common)
})
:
btnC := myButton("<C>").SetSelectedFunc(func() {
self.getYesNo(pages, common, "Yes or No")
})
:
それぞれの処理は、下記となります。
func (self *MainList) getTable(pages *tview.Pages, common *Common) {
s := strings.Split("テーブルA テーブルB テーブルC テーブルE テーブルF テーブルG テーブルH テーブルI テーブルJ テーブルK テーブルL テーブルM", " ")
tables := MySelectBox(s, 30, 20, 2, 3, 0, true).
//tables := MySelectBox(s, 30, 20, 2, 3, 0, false).
SetDoneFunc(func(buttonLabel string, inputString string) {
if buttonLabel == "OK" {
common.reset()
common.tableName = inputString
pages.RemovePage("table")
self.display(common)
}
})
tables.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyEscape:
pages.RemovePage("table")
self.display(common)
}
return event
})
// @@@ if you like full screen dialog, replace line below.
pages.AddPage("table", tables, false, true)
//self.app.SetRoot(tables, true)
// @@@@
}
func (self *MainList) getSearch(pages *tview.Pages, common *Common) {
search := MyInputDialog(self.app, common.search, 40, 7, 2, 4, true).
//search := MyInputDialog(self.app, common.search, 40, 7, 2, 4, false).
SetDoneFunc(func(buttonLabel string, inputString string) {
if buttonLabel == "OK" {
common.search = inputString
} else if buttonLabel == "Cancel" {
common.search = ""
}
pages.RemovePage("search")
common.resetPaging()
self.display(common)
})
// @@@ if you like full screen dialog, replace line below.
pages.AddPage("search", search, false, true)
//self.app.SetRoot(search, true)
// @@@@
}
func (self *MainList) getYesNo(pages *tview.Pages, common *Common, msg string) {
yesno := MyMessageBox(self.app, msg, 30, 7, 2, 3, true).
//yesno := MyMessageBox(self.app, msg, 30, 7, 2, 3, false).
SetDoneFunc(func(buttonLabel string, inputString string) {
if buttonLabel == "Yes" {
}
pages.RemovePage("yesno")
self.display(common)
})
yesno.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyEscape:
pages.RemovePage("yesno")
self.display(common)
}
return event
})
// @@@ if you like full screen dialog, replace line below.
pages.AddPage("yesno", yesno, false, true)
//self.app.SetRoot(yesno, true)
// @@@@
}
DialogBoxではなく独立した別画面として扱いたい場合は、上のコード上の@@@で囲んである行を下記のように入れ替えてください。(MessageBoxの例)
// @@@ if you like full screen dialog, replace line below.
//pages.AddPage("yesno", yesno, false, true)
self.app.SetRoot(yesno, true)
// @@@@
[5] プログラムの構造と実行
プログラムのディレクトリ構造は下記のようになります。
cmd/
main.go
tui/
application.go
common.go
list.go
mywidget.go
mytview
mydialog.go
go.mod
go.sum
実行は前回までと同様です。
go run cmd/main.go
以上で、懸案の一つであったDialogBoxを実装することが出来ました。残された課題は複数行入力ですね。次回はこれに対応しましょう。