One of the more complex widgets included in the standard Fyne library is the Table
collection widget.
It is designed to present a lot of data in an efficient manner, supporting fast scroll through large quantities of widgets (sharing a design with the List
and Tree
collections). This requires complex internal caching and a slightly more complex, callback based, API compared to simpler widgets. Due to the way it is set up a table will hold only one type of widget – but it is often desirable to show more. We will explore how to do that in this post.
Showing different widgets in table cells
It is a relatively common use-case to display different types of data in a Table (or List, which this tutorial can be applied to as well). For example we may want to show devices in a table as follows, with a name and IP address using a Label
, but with an icon or other data which is not possible using the same content widget:
As well as looking to display different widget types we have also set the third column to be narrower, which can be done with the simple call t.SetColumnWidth(2, 24)
. However the complete solution is a little more complex, we will need to display different widgets in a homogeneous setup callback – to do this we will use a container so we can manage multiple widgets.
Coding it up
We can support this switching of widget type by making use of a Max
container in each cell (using Max so that all items fill the space) and then hide / show the items according to the cell being set up in the update callback. For the illustration above we wish to display textual content (a Label) and an image (using the Icon widget), which is set up in a container as follows:
. container.NewMax(widget.NewLabel("template11"), widget.NewIcon(nil))
The container solves the creation of different cells, but to complete the display we must update the container to show the correct item based on the column ID. That is done in the update callback by checking the TableCellID
and using Hide()
and Show()
appropriately, as shown below:
l := o.(*fyne.Container).Objects[0].(*widget.Label) i := o.(*fyne.Container).Objects[1].(*widget.Icon) l.Show() i.Hide() switch id.Col { case 2: l.Hide() i.Show() i.SetResource(getIcon(id.Row)) case 0: l.SetText("hostname") }
As you can see we set the visibility of the appropriate widget, and then go ahead to set its content in the usual manner. Be sure to set the right sort of content on each column or the data set may not be visible!
The complete code for the image displayed above is set out below, combining all of the items we have discussed so far.
package main import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) func main() { a := app.New() w := a.NewWindow("Devices") w.Resize(fyne.NewSize(320, 240)) t := widget.NewTable(func() (int, int) { return 100, 4 }, func() fyne.CanvasObject { return container.NewMax(widget.NewLabel("template11"), widget.NewIcon(nil)) }, func(id widget.TableCellID, o fyne.CanvasObject) { l := o.(*fyne.Container).Objects[0].(*widget.Label) i := o.(*fyne.Container).Objects[1].(*widget.Icon) l.Show() i.Hide() switch id.Col { case 2: l.Hide() i.Show() i.SetResource(getIcon(id.Row)) case 0: l.SetText("hostname") case 1: l.SetText("127.0.0.1") case 3: l.SetText("notes...") } }) t.SetColumnWidth(2, 24) t.SetColumnWidth(3, 156) w.SetContent(t) w.ShowAndRun() } func getIcon(i int) fyne.Resource { switch i%3 { case 1: return theme.HomeIcon() case 2: return theme.MailSendIcon() default: return theme.MediaVideoIcon() } }
Using this approach you can build more complex table content, showing a richer experience for your users. This approach will continue to work well and deliver good design and performance.
Into the future
Add to this that in Fyne v2.3.0 we introduced SetRowHeight
and there are more possibilities for creating advanced tables whilst supporting the large data sizes and high performance that the API is designed to enable. Future versions may provide a way to manage this even more efficiently, but that API has yet to be designed.
widget.NewTable(
func() (int, int) {
return len(center) + 1, len(Headers)
},
func() fyne.CanvasObject {
label := widget.NewLabelWithStyle(“”, fyne.TextAlignCenter, fyne.TextStyle{})
input := widget.NewEntry()
input.SetText(“”)
bytton := widget.NewButton(“Button (text only)”, func() { fmt.Println(“tapped text button”) })
return container.NewMax(label, input, bytton)
},
func(id widget.TableCellID, o fyne.CanvasObject) {
// 根据表格坐标设置内容
l := o.(*fyne.Container).Objects[0].(*widget.Label)
i := o.(*fyne.Container).Objects[1].(*widget.Entry)
btn := o.(*fyne.Container).Objects[2].(*widget.Button)
if id.Row == 0 {
l.Show()
i.Hide()
btn.Hide()
l.SetText(Headers[id.Col])
} else {
l.Hide()
i.Hide()
btn.Hide()
switch id.Col {
case 0:
l.Show()
l.SetText(strconv.Itoa(int(center[id.Row-1].M_nagentid)))
case 1:
i.Show()
i.SetText(center[id.Row-1].M_strdbname)
i.OnChanged = func(str string) {
center[id.Row-1].M_strdbname = str
center[id.Row-1].M_bChange = isChange(center[id.Row-1], allcenter)
handleCustomEvent()
}
case 2:
i.Show()
i.SetText(center[id.Row-1].M_strdbuser)
i.OnChanged = func(str string) {
center[id.Row-1].M_strdbuser = str
center[id.Row-1].M_bChange = isChange(center[id.Row-1], allcenter)
handleCustomEvent()
}
case 3:
i.Show()
i.SetText(center[id.Row-1].M_strdbpwd)
i.OnChanged = func(str string) {
center[id.Row-1].M_strdbpwd = str
center[id.Row-1].M_bChange = isChange(center[id.Row-1], allcenter)
handleCustomEvent()
}
case 4:
i.Show()
i.SetText(center[id.Row-1].M_strdbserver)
i.OnChanged = func(str string) {
center[id.Row-1].M_strdbserver = str
center[id.Row-1].M_bChange = isChange(center[id.Row-1], allcenter)
handleCustomEvent()
}
case 5:
i.Show()
i.SetText(strconv.Itoa(int(center[id.Row-1].M_ndbport)))
i.OnChanged = func(str string) {
num, err := strconv.Atoi(str)
if err != nil {
num = 3306
}
center[id.Row-1].M_ndbport = int32(num)
center[id.Row-1].M_bChange = isChange(center[id.Row-1], allcenter)
handleCustomEvent()
}
case 6:
i.Show()
i.SetText(strconv.Itoa(int(center[id.Row-1].M_ndbconnect)))
i.OnChanged = func(str string) {
num, err := strconv.Atoi(str)
if err != nil {
num = 10
}
center[id.Row-1].M_ndbconnect = int32(num)
center[id.Row-1].M_bChange = isChange(center[id.Row-1], allcenter)
handleCustomEvent()
}
case 7:
i.Show()
i.SetText(strconv.Itoa(int(center[id.Row-1].M_ndbsqldelaythread)))
i.OnChanged = func(str string) {
num, err := strconv.Atoi(str)
if err != nil {
num = 5
}
center[id.Row-1].M_ndbsqldelaythread = int32(num)
center[id.Row-1].M_bChange = isChange(center[id.Row-1], allcenter)
handleCustomEvent()
}
case 8:
i.Show()
i.SetText(strconv.Itoa(int(center[id.Row-1].M_ndbismain)))
i.OnChanged = func(str string) {
num, err := strconv.Atoi(str)
if err != nil {
num = 1
}
center[id.Row-1].M_ndbismain = int32(num)
center[id.Row-1].M_bChange = isChange(center[id.Row-1], allcenter)
handleCustomEvent()
}
case 9:
i.Show()
i.SetText(strconv.Itoa(int(center[id.Row-1].M_ndbisuser)))
i.OnChanged = func(str string) {
num, err := strconv.Atoi(str)
if err != nil {
num = 0
}
center[id.Row-1].M_ndbisuser = int32(num)
center[id.Row-1].M_bChange = isChange(center[id.Row-1], allcenter)
handleCustomEvent()
}
case 10:
l.Show()
if center[id.Row-1].M_nstatus == 0 {
l.SetText(“正常”)
} else {
l.SetText(“释放”)
}
case 11:
btn.Show()
btn.SetText(“保存”)
if center[id.Row-1].M_bChange {
btn.Enable()
} else {
btn.Disable()
}
btn.Importance = widget.HighImportance
btn.OnTapped = func() { allcenter = dboperator.UpdateAgentDB(center[id.Row-1]); handleCustomEvent2() }
case 12:
btn.Show()
btn.SetText(“放弃”)
if center[id.Row-1].M_bChange {
btn.Enable()
} else {
btn.Disable()
}
btn.Importance = widget.HighImportance
btn.OnTapped = func() { fmt.Println(“tapped button id.Col–“, id.Col, “id.Row–“, id.Row); handleCustomEvent2() }
}
}
},
)
//Dragging the scroll bar may cause refresh issues
Hiding and then showing items may cause them to flicker, it will depend on many factors but it is good to avoid this – only make changes that you want to be visible.
So if your button is visible, and it should remain so, don’t hide it.