Go言語(golang)でTUIアプリを作ろう ( その8 画面間データ連携について )

tviewで、画面の遷移をサポートしてみよう。


今回は、新たな画面を追加して画面間の遷移処理を組み込んでみましょう。
これまでの画面がデータを一覧表示(List)するものでしたから、次の画面は、選択された1件分を表示する詳細画面(Detail)となります。


[1] 画面遷移を行うには

Go言語(golang)でTUIアプリを作ろう(その3 tviewの基本構造)で、tviewの基本オブジェクト構造を、概念図にしてみました。

この図にありますように、tviewの中心にあるオブジェクトは、Applicationであり、画面を構成するPrimitiveは、すべてこのApplicationに紐付けられています。したがって、tviewで複数画面を連携するには、このApplicationオブジェクトを共有する必要があります。

アプリケーションの起動形態を、下記の2つに分けて考えてみるとよいでしょう。

(1) 初期起動の場合

Applicationオブジェクトは、まだ存在しない状態(nil)ですので、Applicationオブジェクトの生成から開始します。

(2) 他のクラスから起動された場合

  • 例えば、ListからDetailに画面遷移する場合は、Listクラスで生成されたApplicationオブジェクトを、Detailに引き渡します。
  • Detailでは、画面作成処理で生成したContainerをApplicationのSetRootメソッドにセットして、新たな画面を表示します。

Applicationオブジェクトを、上記の手順で再利用することになるわけです。このあたりのロジックについては、すでに前回示していますが、下記のようなコードになります。

func (self *MyApplication) run(app *tview.Application, primitive tview.Primitive) {  
	if app == nil {  
		self.app = tview.NewApplication()  
		if err := self.app.SetRoot(primitive, true).EnableMouse(true).Run(); err != nil {  
			panic(err)  
		}  
	} else {  
		self.app = app  
		self.display(primitive)  
	}  
}  

他の画面を起動するコードは下記となります。この例では、List画面からDetail画面を呼び出しています。

	NewDetail().run(self.app, common)  
  

[2] 詳細画面(Detail)の作成

さて、画面遷移を行う手法が明確になったところで、実際の画面を作成していきましょう。

上の画面イメージを作成していきます。新たに使用するPrimitiveは、表示フィールド、入力フィールド、それを格納するコンテナです。

(1) 表示フィールド

tviewにおける表示フィールドは、TextViewで定義します。NewTextView()メソッドでインスタンスを生成し、Setterで適当な属性を定義していきます。
今回は、下記のような共通的な表示フィールドをmyLabelとして設定しています。

func myLabel(label string) *tview.TextView {  
	return tview.NewTextView().SetTextColor(tcell.ColorAqua).SetText(label).SetTextAlign(tview.AlignLeft)  
}  

(2) 入力フィールド

入力用のフィールドは、InputFieldで定義します。InputFieldにfocusが移動した場合、文字入力はもちろんですが、矢印キーでの移動、Delキー、Insキーでの文字の削除、追加などが標準でできるようになっています。これも、属性を付加したmyEditを定義しておきます。

func myEdit(text string, rows int) *tview.InputField {  
	return tview.NewInputField().SetFieldWidth(rows).SetText(text).SetFieldTextColor(tcell.ColorWhite).SetFieldBackgroundColor(tcell.ColorBlack).SetFieldStyle(tcell.StyleDefault.Underline(true))  
}  

先にも少し触れましたが、tviewでは、複数行入力がサポートされていません。

(3) コンテナ

Primitiveを配置するコンテナには、Flexと、Gridがありますが、今回はFlexをを使用します。
採用理由は、一方向に配置する形式のほうがわかりやすいことと、Flexには、GetItemメソッドによって、内部に格納されているPrimitiveにアクセスできるからです。(なぜ、Gridにはこのメソッドがないのでしょう。どうもこのあたりに、tviewの詰めの甘さを感じます。)

Detail部の入力画面は、下記のようなコードになります。

func (self *Detail) detailBody(pages *tview.Pages, header *tview.Flex, footer *tview.Flex, common *Common) *tview.Flex {  
	var focusPrimitives []tview.Primitive  
	body := tview.NewFlex().SetDirection(tview.FlexRow)  
	btnCategory := myButton(common.category)  
	editField01 := myEdit("フィールド01", common.cols)  
	editField02 := myEdit("フィールド02", common.cols)  
	focusPrimitives = append(focusPrimitives, btnCategory)  
	focusPrimitives = append(focusPrimitives, editField01)  
	focusPrimitives = append(focusPrimitives, editField02)  
	editNote := tview.NewTextView().SetText("FirstLine\nNextLine")  
  
	body.AddItem(myLabel("ID"), 1, 0, false)  
	body.AddItem(myLabel(fmt.Sprintf("%d", common.selectedItem-1)), 1, 0, false)  
  
	body.AddItem(myLabel("Category"), 1, 0, false)  
	body.AddItem(btnCategory, 1, 0, false)  
	body.AddItem(myLabel("Field01"), 1, 0, false)  
	body.AddItem(editField01, 1, 0, false)  
	body.AddItem(myLabel("Field02"), 1, 0, false)  
	body.AddItem(editField02, 1, 0, false)  
	body.AddItem(myLabel("Note"), 1, 0, false)  
	body.AddItem(editNote, 0, 1, true)  
  
	body.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {  
		switch event.Key() {  
  
		case tcell.KeyTab:  
			mySetFocus(self.app, focusPrimitives, false)  
			return nil  
		case tcell.KeyBacktab:  
			if btnCategory.HasFocus() {  
				myFlexFocus(self.app, header, false)  
				return nil  
			} else {  
				mySetFocus(self.app, focusPrimitives, true)  
				return nil  
			}  
		case tcell.KeyDown:  
			mySetFocus(self.app, focusPrimitives, false)  
			return nil  
		case tcell.KeyUp:  
			if btnCategory.HasFocus() {  
				myFlexFocus(self.app, header, false)  
				return nil  
			} else {  
				mySetFocus(self.app, focusPrimitives, true)  
				return nil  
			}  
		}  
		return event  
	})  
  
	// ------------------------------  
	// InputCapture on Header  
	// ------------------------------  
	header.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {  
		switch event.Key() {  
		case tcell.KeyRight, tcell.KeyTab:  
			myFlexFocus(self.app, header, false)  
			return nil  
		case tcell.KeyLeft, tcell.KeyBacktab:  
			myFlexFocus(self.app, header, true)  
			return nil  
		case tcell.KeyDown:  
			mySetFocus(self.app, focusPrimitives, false)  
			return nil  
		case tcell.KeyRune:  
			switch event.Rune() {  
			case 'q', 'Q':  
				self.exit()  
			case 'r', 'R':  
				NewMainList().run(self.app, common)  
			}  
		}  
		return event  
	})  
  
	return body  
}  

Detail部から、List部に戻る部分は、下記のコードになります。

	btnR := myButton("<R>").SetSelectedFunc(func() {  
		NewMainList().run(self.app, common)  
	})  
  

詳細はソースコードを見てください。tviewでは、Focus移動を自前で書かないといけないので、Python Urwidに比べると冗長さが目立ちますね。


[3] プログラムの実行

下記のコマンドを入力します。

go run cmd/main.go  

Listで選択されたデータが、Detailに渡っていることがわかります。

次回は、tviewの標準機能ではサポートされていないDialogを実装してみましょう。


ソースコードについて

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