入门
数据绑定是在Fyne工具包v2.0.0版本中引入的的一个强大功能。
通过使用数据绑定,我们可以避免手动管理许多标准对象,如标签、按钮和列表等,的数据内容。
Fyne内置绑定支持许多基本数据类型(如Int、String、Float等),还有列表(如StringList、BoolList)以及Map和Struct绑定。这些类型中的每一种都可以使用一个简单的构造函数来创建。例如,要创建一个零值的新字符串绑定,可以使用绑定binding.NewString()。您可以使用Get和Set方法获取或设置大多数数据绑定的值。
也可以使用类似的函数绑定到现有值,这些函数的名称以Bind开头,并且它们都接受指向绑定类型的指针。例如,要绑定到现有的int,我们可以使用binding.BindInt(&myInt)。
通过保留对绑定值的引用而不是原始变量,我们可以配置小部件和函数来自动响应任何更改!有经验的朋友可以把Fyne框架中的绑定方案与Web开发前端框架Vue或者React中的绑定进行简单对比。
但是:如果想直接更改外部数据的话,请务必调用Reload()
方法,以确保绑定系统读取最新的值。请参考下面基础实例代码(本例中没有涉及外部数据问题):
package main
import (
"log"
"fyne.io/fyne/v2/data/binding"
)
func main() {
boundString := binding.NewString()
s, _ := boundString.Get()
log.Printf("Bound = '%s'", s)
myInt := 5
boundInt := binding.BindInt(&myInt)
i, _ := boundInt.Get()
log.Printf("Source = %d, bound = %d", myInt, i)
}
接下来,我们把上述绑定技巧与Fyne GUI组件结合,来了解如何使用绑定简化小部件相关的数据管理问题。
绑定简单的GUI小部件(Gadgets)
绑定小部件的最简单形式是:将绑定项作为值而不是静态值传递给此小部件。许多小部件提供了一个WithData构造函数,该构造函数将接受类型化的数据绑定项。要设置绑定,只需传入正确的类型即可。
尽管这在最初的代码中看起来没有多大好处,但您可以看到它是如何确保显示的内容始终与数据源保持同步的。您会注意到,我们不需要在Label小部件上调用Refresh(),甚至不需要保留对它的引用,但它会适当地更新。请参考下面的基本示例来体会小部件Label如何与其数据源始终保持同步!
package main
import (
"crypto/rand"
"encoding/base64"
"fmt"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
w := myApp.NewWindow("Simple")
str := binding.NewString()
str.Set("Initial value")
text := widget.NewLabelWithData(str)
button := widget.NewButton("click me", func() {
randomString, err := generateRandomString(10)
if err != nil {
fmt.Println("Failed to generate random string:", err)
return
}
str.Set(randomString)
})
content := container.New(layout.NewHBoxLayout(), text, layout.NewSpacer(), button)
w.SetContent(content)
w.ShowAndRun()
}
func generateRandomString(length int) (string, error) {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
charsetBytes := make([]byte, length)
for i := range charsetBytes {
_, err := rand.Read(charsetBytes[i : i+1])
if err != nil {
return "", err
}
}
return base64.URLEncoding.EncodeToString(charsetBytes), nil
}
双向数据绑定问题
到目前为止,我们已经将数据绑定视为保持用户界面元素及时更新的一种方式。然而,更常见的应用需求是:更新UI小部件中的值的同时,使数据处处保持最新。值得庆幸的是,Fyne中提供的绑定是“双向”的,这意味着不仅可以将值推入其中,而且也可以读取绑定对象中的值。这样一来,绑定对象中的数据的变化将在没有任何附加代码的情况下传达给所有相关连的代码。
为了展示这一点,我们可以编写一个简单的测试应用程序,以显示Label和Entry绑定到相同的值。通过操作,您可以看到通过Entry编辑值也会更新标签中的文本。这一切都如我们所期望的,而且无需调用刷新或引用我们代码中的小部件。
通过使应用程序进行数据绑定,您不需要使用保存指向所有小部件的指针操作。相反,通过将数据捕获为一组绑定值,用户界面就可以成为完全独立的代码。这样的方案显然更易于阅读、管理及数据维护。
package main
import (
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
w := myApp.NewWindow("Two Way Binding Test")
str := binding.NewString()
str.Set("Initial Text!")
w.SetContent(container.NewVBox(
widget.NewLabelWithData(str),
widget.NewEntryWithData(str),
))
w.ShowAndRun()
}
程序启动后,在下部文本框中输入新的内容,上面标签控件中的内容进行同步更新。这就是双向绑定的特征与优势!
数据转换问题
到目前为止,我们已经使用了数据绑定,其中数据类型与输出类型是匹配的(例如String和Label或Entry)。然而,实际开发中通常需要呈现尚未采用正确格式的数据。为此,绑定包binding提供了许多有用的转换函数。
最常见的是,转换函数用于将不同类型的数据转换为字符串,以便在Label或Entry窗口小部件中显示。请参阅下面代码中如何使用binding.FloatToString将Float转换为String。本例中,你可以通过移动滑块来编辑原始值。每次数据更改时,它都会运行转换代码并更新任何与之连接的小部件。
还可以使用格式字符串为用户界面添加更自然的输出。您可以看到,我们的短绑定也将Float转换为String,但通过使用WithFormat帮助程序,我们可以传递一个格式字符串(类似于fmt包)来提供自定义输出。
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
w := myApp.NewWindow("Conversion")
f := binding.NewFloat()
str := binding.FloatToString(f)
short := binding.FloatToStringWithFormat(f, "%0.0f%%")
f.Set(25.0)
w.SetContent(container.NewVBox(
widget.NewSliderWithData(0, 100.0, f),
widget.NewLabelWithData(str),
widget.NewLabelWithData(short),
))
w.Resize(fyne.NewSize(480, 360))
w.ShowAndRun()
}
上面小例中,我们实现了数据的定制格式化显示。由于数据双向绑定的作用,简单拖动一个水平滚动条,可得到如下图所示的显示效果。
List数据绑定实例
为了演示如何使用双向绑定组件来连接更复杂的Fyne类型,我们构造一个稍微复杂的例子,使用List小部件,观察数据绑定如何使其更易于使用。首先,我们创建一个StringList数据绑定,它是String数据类型的列表。一旦我们有了列表类型的数据,我们就可以将其连接到标准的列表小部件。为此,我们使用widget.NewListWithData构造函数,与其他小部件非常相似。
将下面代码与最基本的列表示例进行比较,您将看到两个主要更改:第一个是我们将数据类型作为第一个参数而不是长度回调函数进行传递;第二个更改是最后一个参数,即UpdateItem回调。下面的修订版本使用了binding.DataItem而不是widget.ListIndexID。当使用这个回调结构时,我们应该绑定到模板标签小部件,而不是调用SetText函数。这意味着,如果数据源中的任何字符串发生更改,则表中每个受影响的行都将刷新。
package main
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.New()
myWindow := myApp.NewWindow("List数据绑定示例")
t := SetMyTheme()
myApp.Settings().SetTheme(t)
data := binding.BindStringList(
&[]string{"条目 1", "条目 2", "条目 3", "条目 4", "条目 5", "条目 6", "条目 7"},
)
list := widget.NewListWithData(data,
func() fyne.CanvasObject {
return widget.NewLabel("template")
},
func(i binding.DataItem, o fyne.CanvasObject) {
o.(*widget.Label).Bind(i.(binding.String))
})
add := widget.NewButton("添加", func() {
val := fmt.Sprintf("条目 %d", data.Length()+1)
data.Append(val)
})
myWindow.SetContent(container.NewBorder(nil, add, nil, nil, list))
myWindow.Resize(fyne.NewSize(480, 360))
myWindow.ShowAndRun()
}
其中,SetMyTheme方法在文件custom_theme.go中定义,内容如下(在此先不作解释,有兴趣的读者可以参考官网中的自定义主题):
package main
import (
"image/color"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)
type myTheme struct {
regular, bold, italic, boldItalic, monospace fyne.Resource
}
func (t *myTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
return theme.DefaultTheme().Color(name, variant)
}
func (t *myTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(name)
}
func (m *myTheme) Font(style fyne.TextStyle) fyne.Resource {
if style.Monospace {
return m.monospace
}
if style.Bold {
if style.Italic {
return m.boldItalic
}
return m.bold
}
if style.Italic {
return m.italic
}
return m.regular
}
func (m *myTheme) Size(name fyne.ThemeSizeName) float32 {
return theme.DefaultTheme().Size(name)
}
func (t *myTheme) SetFonts(regularFontPath string, monoFontPath string) {
t.regular = theme.TextFont()
t.bold = theme.TextBoldFont()
t.italic = theme.TextItalicFont()
t.boldItalic = theme.TextBoldItalicFont()
t.monospace = theme.TextMonospaceFont()
if regularFontPath != "" {
t.regular = loadCustomFont(regularFontPath, "Regular", t.regular)
t.bold = loadCustomFont(regularFontPath, "Bold", t.bold)
t.italic = loadCustomFont(regularFontPath, "Italic", t.italic)
t.boldItalic = loadCustomFont(regularFontPath, "BoldItalic", t.boldItalic)
}
if monoFontPath != "" {
t.monospace = loadCustomFont(monoFontPath, "Regular", t.monospace)
} else {
t.monospace = t.regular
}
}
func loadCustomFont(env, variant string, fallback fyne.Resource) fyne.Resource {
variantPath := strings.Replace(env, "Regular", variant, -1)
res, err := fyne.LoadResourceFromPath(variantPath)
if err != nil {
fyne.LogError("Error loading specified font", err)
return fallback
}
return res
}
func SetMyTheme() *myTheme {
t := &myTheme{}
t.SetFonts("./assets/ArialUnicode.ttf", "")
return t
}
在我们的演示代码中有一个“添加”按钮,当点击它时,它将向数据源附加一个新值。这样做将自动触发数据更改处理程序,并展开List小部件以显示新数据。
小结
本文介绍了Fyne框架中数据绑定组件binding在数据与GUI展示组件中的重要作用,并给出了几个基本类型的实例展示。但是,对于binding组件内在实现逻辑并没有作过多和细致解释,后面将撰文作专门探讨,敬请期待。
标签:widget,fyne,GUI,绑定,binding,v2,跨平台,io,Fyne From: https://blog.51cto.com/zhuxianzhong/8556813