Go言語(golang)でTUIアプリを作ろう ( その6 Listの画面コントロール )

ページング処理を追加しよう。


今回は、前回のプログラムにページコントロールを入れて、全件表示ができるよう変更していきます。新たなWidgetの追加はありません。処理は極めて簡単で、ページング対応ロジックの追加が中心になります。


画面イメージ

まずは、最終画面イメージを示します。

Header部に、ページングボタンを2つ配置しています。このボタンがクリックされることで、ページング表示を行っていくという形となるわけです。


プログラムの分割

その前にプログラムを整理しましょう。
今までは、1つのプログラムに全ての処理を記述していましたが、だいぶ長くなってきたので、下記の2ファイルに分割し、ページングに対応した処理を追加していきます。

(1) Widget関係(mywidget.go)
(2) メイン部(list.go)

それでは、個々のスクリプトを見ていきましょう。


(1) mywidget

tviewの基本Widgetを拡張している部分を抜き出します。

package main  
  
import (  
	"fmt"  
	"github.com/gdamore/tcell/v2"  
	"github.com/rivo/tview"  
)  
  
var cols, rows = GetScreenSize()  
  
func GetScreenSize() (int, int) {  
	s, _ := tcell.NewScreen()  
	s.Init()  
	cols, rows := s.Size()  
	s.Fini()  
	return cols, rows  
}  
  
var listData = createListData()  
  
func createListData() []string {  
	var listData []string  
	var s string  
	for i := 0; i < 100; i++ {  
		s = "テストデータ" + fmt.Sprintf("%d", i)  
		for j := len(s); j < cols; j++ {  
			s = s + " "  
		}  
		listData = append(listData, s)  
	}  
	return listData  
}  
  
func getListData(from int) []string {  
	return listData[from-1 : from+rows-2]  
}  
  
// -----------------------------------------------  
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 myButton(label string) *tview.Button {  
	button := tview.NewButton(label)  
	button.Box = tview.NewBox().SetBackgroundColor(tcell.ColorBlack)  
	button.SetLabelColor(tcell.ColorYellow).SetLabelColorActivated(tcell.ColorBlack).SetBackgroundColorActivated(tcell.ColorYellow)  
	return button  
}  
  

(2) list

一覧処理なので、Listという名称に変更しました。

1) ページングボタンの追加

headerにページングボタンを追加します。<N>ボタンで次ページ、<P>ボタンで前ページ画面に移行することとします。

2) ボタンの処理

表示開始レコードをページングボタンの動作に合わせて増減することで画面ページングを行います。今回は、1ページめの<P>ボタンを、最終ページの<N>ボタン表示の抑制には手を抜いて、そのままにしてあります。

下記にコードを示します。

package main  
  
import (  
	"github.com/gdamore/tcell/v2"  
	"github.com/rivo/tview"  
	"strconv"  
)  
  
func setList(list *tview.List, from int) *tview.List {  
	list.Clear()  
	item := getListData(from)  
	for i := 0; i < rows-2; i++ {  
		list.AddItem(item[i], "", 0, nil)  
	}  
	return list  
}  
  
func nextPage(list *tview.List, from int) (*tview.List, int) {  
	if from+(rows-2) < len(listData) {  
		from = from + (rows - 2)  
		list = setList(list, from)  
	}  
	return list, from  
}  
  
func priorPage(list *tview.List, from int) (*tview.List, int) {  
	if from > 1 {  
		from = from - (rows - 2)  
		list = setList(list, from)  
	}  
	return list, from  
}  
  
func doformat(app *tview.Application, from int) tview.Primitive {  
	list := tview.NewList().ShowSecondaryText(false).SetSelectedTextColor(tcell.ColorWhite).SetSelectedBackgroundColor(tcell.ColorAqua).SetSelectedFocusOnly(true)  
	list = setList(list, from)  
  
	pages := tview.NewPages()  
	footer := tview.NewTextView().SetText("これはフッター")  
	header := tview.NewFlex()  
	btnN := myButton("<N>").SetSelectedFunc(func() {  
		list, from = nextPage(list, from)  
	})  
	btnP := myButton("<P>").SetSelectedFunc(func() {  
		list, from = priorPage(list, from)  
	})  
	btnQ := myButton("<Q>").SetSelectedFunc(func() {  
		app.Stop()  
	})  
	buttons := []tview.Primitive{  
		btnN,  
		btnP,  
		btnQ,  
	}  
	header.AddItem(btnN, 0, 1, true)  
	header.AddItem(btnP, 0, 1, true)  
	header.AddItem(btnQ, 0, 1, true)  
  
	body := tview.NewFlex().SetDirection(tview.FlexRow).AddItem(list, 0, 1, true)  
  
	main := tview.NewFlex().SetDirection(tview.FlexRow).  
		AddItem(header, 1, 0, true).  
		AddItem(body, 0, 1, true).  
		AddItem(footer, 1, 0, false)  
	pages.AddPage("main", main, true, 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.KeyDown:  
			app.SetFocus(list)  
			return nil  
		}  
		return event  
	})  
	body.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {  
		switch event.Key() {  
		case tcell.KeyUp:  
			if list.GetCurrentItem() == 0 {  
				mySetFocus(app, buttons, true)  
				return nil  
			}  
		}  
		return event  
	})  
	pages.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {  
		switch event.Key() {  
		case tcell.KeyRune:  
			switch event.Rune() {  
			case 'q':  
				app.Stop()  
			case 'n':  
				list, from = nextPage(list, from)  
			case 'p':  
				list, from = priorPage(list, from)  
			}  
		}  
		return event  
	})  
  
	list.SetSelectedFunc(func(index int, s string, secondary string, code rune) {  
		footer.SetText("pos:" + strconv.Itoa(index) + " data:" + s)  
	})  
  
	return pages  
}  
  
func main() {  
	app := tview.NewApplication()  
	pages := doformat(app, 1)  
	if err := app.SetRoot(pages, true).EnableMouse(true).Run(); err != nil {  
		panic(err)  
	}  
}  

動作確認

では、実際に動かしてみましょう。mywidget.golist.goの2ファイルを同一ディレクトリに入れておきます。
今回以降、モジュール名はlistdbgに統一します。

go mod init listdbg  
go mod tidy  

環境を設定したら、実行します。

go run list.go mywidget.go  

先に示した画面が表示されたでしょうか。
<N>ボタン、<P>ボタンでページング処理が行われます。また、データをEnterキーで選択すると、レコードの内容がfotter部に表示されることも確認してください。

これで、tviewListを使用した一覧リスト画面の雛形は完成です。
次回以降、この画面から選択されたレコードの詳細(Detail)を表示するプログラムを作成していくことになりますが、その前に「プログラム全体の構造を見直したい」と考えています。


ソースコードについて

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