(原) 再次学习fyne

原创文章,请后转载,并注明出处。

寻找一款好用的GUI是每个Go开发者的追求。之前也学习过fyne,没有使用它的主要原因是对中文的支持(能显示,不能输入)。

今天再看的时候,发现它已经支持中文输入了。我们毕竟应该用发展的眼光来看所有问题。继续学习…

官网:https://fyne.io/

相关文档:https://developer.fyne.io/

API:https://developer.fyne.io/api/v2.0/

安装:go get fyne.io/fyne/v2

以前网友的文章,或许是针对早前版本的:

https://studygolang.com/articles/29369?fr=sidebar

https://learnku.com/articles/43181

这里有一个样式生成器:github.com\lusingander\fyne-theme-generator

go build -ldflags -H=windowsgui main.go   windows下没有命令窗编译
os.Setenv("FYNE_FONT", "./msyh.ttf")   // 通过环境变量设置中文字库

可以运行一下示例 go get fyne.io/fyne/v2/cmd/fyne_demo

FYNE_THEME=light 通过环境变量,改变外观


组件

Accordion 手风琴组件

Button 按钮

Card 卡片组件

Check 检查组件

Entry 输入组件

FileIcon ?

Form 表单

Hyperlink 超链组件:当点击时,通过默认浏览器打开

Icon 图标组件:可以加载其资源以配合主题。

Label 标签组件

Progress bar 进度条组件

ProgressBarInfinite 组件创建了一个水平面板,表示无限期的等待。一个无限期的进度条在0%->100%之间反复循环,直到调用Stop()。

RadioGroup 单选组件

Select 下拉列表选择组件

SelectEntry 可输入选择组件

Separator 分隔线组件

Slider 滑块组件

TextGrid TextGrid是一个单空格的字符网格。这是为文本编辑器、代码预览或终端模拟器所设计的。

Toolbar 工具栏组件

List 列表组件

Table

Tree

AppTabs 标签切换内容

Scroll 滚动

Split 分割

布局

Horizontal Box (HBox) 垂直布局

Vertical Box (VBox) 水平布局

Center 居中

Form 表单

表单布局将项目排列成对,其中第一列是最小宽度。这通常适用于为表单中的元素贴标签,标签在第一列,它所描述的项目在第二列。你应该总是向表单布局中添加偶数的元素。

Grid 表格

网格布局在可用的空间内平均安排项目。指定一个列的数量,对象被水平放置,直到达到列的数量,这时开始新的一行。所有对象都有相同的尺寸,即宽度除以列总数,高度是总高度除以所需行数。减去填充物。

GridWrap 表格自适

GridWrap布局安排所有项目沿着一行流动,如果没有足够的空间,则包裹到一个新的行。所有对象将被设置为相同的尺寸,也就是传递给布局的尺寸。这个布局可能不尊重项目的最小尺寸来管理这个统一的布局。通常用于文件管理器或图像缩略图列表。

Border 边框布局

边框布局支持将项目定位在可用空间的外部。边框被传递给对象的指针为(上、左、下、右)。容器中所有没有被定位在边框上的项目将填充剩余的空间。

Max 最大布局

最大布局将所有的容器元素定位为填充可用空间。这些对象都将是全尺寸的,并按照它们被添加到容器中的顺序绘制(最后的在上面)。

Padded 填充式布局

填充式布局将所有的容器元素定位为填充可用空间,但在外面有一个小的填充。填充物的大小是由主题决定的。这些对象将按照它们被添加到容器中的顺序绘制(最后一个在上面)。

Combining Layouts 自定义布局

通过使用多个布局,可以建立起更复杂的应用结构。多个容器都有自己的布局,可以通过嵌套来创建完整的用户界面安排,只需使用上面列出的标准布局。例如,一个水平的盒子作为标题,一个垂直的盒子作为左侧的文件面板,在内容区采用网格包装布局–所有这些都在一个使用边框布局的容器内,可以建立如下图所示的结果。

对话框

Color 颜色对话框

Confirm 确定对话框

FileOpen

Form

在一个对话框中获取各种输入元素,并进行验证。

Information

Custom


fyne package -os windows -icon app.png
编译成windows系统,且包含图标。这是它附带的一个工具:go get fyne.io/fyne/v2/cmd/fyne

还可以编译为其它系统,例fyne package -os linux -icon myapp.png

fyne install -icon myapp.png 这两个有点什么差别,没仔细看。

不知道和普通的编译,除了多一个图标,有什么不同。(go build -ldflags="-s -w -H windowsgui" -o fyne.exe main.go)文件大了10MB。

关于交叉编译,暂没使用,以后再看:https://developer.fyne.io/started/cross-compiling


Fyne项目被分成许多包,每个包都提供不同类型的功能。它们的内容如下。

fyne.io/fyne/v2 这个导入提供了所有Fyne代码共有的基本定义 包括数据类型和接口。 fyne.io/fyne/v2/app app包提供了启动一个新应用程序的API。 通常你只需要app.New()或app.NewWithID()。 fyne.io/fyne/v2/canvas canvas包提供了Fyne中所有的绘图API。 完整的Fyne工具包是由这些原始图形类型组成的。 fyne.io/fyne/v2/container 容器包提供用于布局和组织应用程序的容器。 fyne.io/fyne/v2/data/binding 绑定包包含将数据源与部件绑定的方法。 fyne.io/fyne/v2/data/validation 验证包为验证部件内的数据提供收费。 fyne.io/fyne/v2/dialog 对话包包含确认、错误和文件保存/打开等对话框。 fyne.io/fyne/v2/layout 布局包提供各种布局实现,用于 布局包提供了各种布局实现,可与容器一起使用(在后面的教程中讨论)。 fyne.io/fyne/v2/storage 存储包提供存储访问和管理功能。 fyne.io/fyne/v2/test 使用测试包中的工具可以更容易地测试应用程序。 包内的工具,可以更容易地测试应用程序。 fyne.io/fyne/v2/widget 大多数图形化应用程序都是用小工具集合创建的。 Fyne中所有的部件和互动元素都在这个包中。


在Fyne中,通过App.Run或Window.ShowAndRun()来循环程序,启动界面。

App.NewWindow() 创建窗口,用Show()进行显示。ShowAndRun()为显示窗口并运行应用程序。

如要显示另一个窗口,只需Show()即可。

以下示例中还有窗口大小的设置。

package main

import (
	"time"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Hello")
	myWindow.SetContent(widget.NewLabel("Hello"))

	go showAnother(myApp)
	myWindow.ShowAndRun()
}

func showAnother(a fyne.App) {
	time.Sleep(time.Second * 5)

	win := a.NewWindow("Shown later")
	win.SetContent(widget.NewLabel("5 seconds later"))
	win.Resize(fyne.NewSize(200, 200))
	win.Show()

	time.Sleep(time.Second * 2)
	win.Close()
}

画布

在 Fyne 中,Canvas 是一个应用程序的绘制区域。每个窗口都有一个画布,你可以用Window.Canvas()来访问。

除了使用 Canvas.SetContent() 改变显示的内容外,还可以改变当前可见的内容。例如,如果您改变了一个矩形的填充颜色,您可以使用 rect.Refresh() 请求刷新这个现有的组件。

package main

import (
	"image/color"
	"time"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/theme"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Canvas")
	myCanvas := myWindow.Canvas()

	green := color.NRGBA{R: 0, G: 180, B: 0, A: 255}
	text := canvas.NewText("Text", green)
	text.TextStyle.Bold = true
	myCanvas.SetContent(text)
	go changeContent(myCanvas)

	myWindow.Resize(fyne.NewSize(100, 100))
	myWindow.ShowAndRun()
}

func changeContent(c fyne.Canvas) {
	time.Sleep(time.Second * 2)

	blue := color.NRGBA{R: 0, G: 0, B: 180, A: 255}
	c.SetContent(canvas.NewRectangle(blue))

	time.Sleep(time.Second * 2)
	c.SetContent(canvas.NewLine(color.Gray{Y: 180}))

	time.Sleep(time.Second * 2)
	red := color.NRGBA{R: 0xff, G: 0x33, B: 0x33, A: 0xff}
	circle := canvas.NewCircle(color.White)
	circle.StrokeWidth = 4
	circle.StrokeColor = red
	c.SetContent(circle)

	time.Sleep(time.Second * 2)
	c.SetContent(canvas.NewImageFromResource(theme.FyneLogo()))
}

以下示例在容器中显示两个文字。

package main

import (
	"image/color"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	//"fyne.io/fyne/v2/layout"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Container")
	green := color.NRGBA{R: 0, G: 180, B: 0, A: 255}

	text1 := canvas.NewText("Hello", green)
	text2 := canvas.NewText("There", green)
	text2.Move(fyne.NewPos(20, 20))
	content := container.NewWithoutLayout(text1, text2)
	// content := container.New(layout.NewGridLayout(2), text1, text2)

	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

以下示例中,通过SetContent添加部件

package main

import (
	"os"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
)

func main() {
	os.Setenv("FYNE_FONT", "./msyh.ttf")  // 不指定字库,输入框中将不能显示中文
	myApp := app.New()
	myWindow := myApp.NewWindow("Widget")

	myWindow.SetContent(widget.NewEntry())
	myWindow.ShowAndRun()
}

画布

矩形

canvas.Rectangle是Fyne中最简单的画布对象。它显示一个指定颜色的块。你也可以使用FillColor字段来设置颜色。

在这个例子中,矩形填充了窗口,因为它是唯一的内容元素。

package main

import (
	"image/color"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
)

func main() {
	myApp := app.New()
	w := myApp.NewWindow("Rectangle")

	rect := canvas.NewRectangle(color.White)
	w.SetContent(rect)

	w.Resize(fyne.NewSize(150, 100))
	w.ShowAndRun()
}

文本

canvas.Text用于Fyne内的所有文本渲染。它是通过指定文本和文本的颜色来创建的。文本使用默认字体渲染,由当前主题指定。

文本对象允许某些配置,如Alignment和TextStyle字段,如这里的例子所说明的。要使用单行字体,你可以指定fyne.TextStyle{Monospace: true}。

可以通过指定FYNE_FONT环境变量来使用另一种字体。用它来设置一个.ttf文件,以取代Fyne工具包或当前主题中提供的字体。

package main

import (
	"image/color"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
)

func main() {
	myApp := app.New()
	w := myApp.NewWindow("Text")

	text := canvas.NewText("Text Object", color.White)
	text.Alignment = fyne.TextAlignTrailing
	text.TextStyle = fyne.TextStyle{Italic: true}
	w.SetContent(text)

	w.ShowAndRun()
}

画线

我试了一下move却没有成功。

package main

import (
	"image/color"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
)

func main() {
	myApp := app.New()
	w := myApp.NewWindow("Line")

	line := canvas.NewLine(color.Black)
	line.StrokeWidth = 5
	w.SetContent(line)

	w.Resize(fyne.NewSize(100, 100))
	w.ShowAndRun()
}

画圆

package main

import (
	"image/color"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
)

func main() {
	myApp := app.New()
	w := myApp.NewWindow("Circle")

	circle := canvas.NewCircle(color.White)
	circle.StrokeColor = color.Gray{0x99}
	circle.StrokeWidth = 5
	w.SetContent(circle)

	w.Resize(fyne.NewSize(100, 100))
	w.ShowAndRun()
}

canvas.Image在Fyne中代表一个可扩展的图像资源。它可以从一个资源(如例子所示)、图像文件、包含图像的URI位置、io.Reader或内存中的Go image.Image加载。

默认的图像填充模式是canvas.ImageFillStretch,这将导致它填充指定的空间(通过Resize()或布局)。另外,你也可以使用canvas.ImageFillContain来确保长宽比得到保持,图像在范围内。除此之外,你还可以使用canvas.ImageFillOriginal(就像这里的例子中使用的那样)来确保它的最小尺寸等于原始图像的尺寸。

图像可以是基于位图的(如PNG和JPEG)或基于矢量的(如SVG)。

package main

import (
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/theme"
)

func main() {
	myApp := app.New()
	w := myApp.NewWindow("Image")

	image := canvas.NewImageFromResource(theme.FyneLogo())
	// image := canvas.NewImageFromURI(uri)
	// image := canvas.NewImageFromImage(src)
	// image := canvas.NewImageFromReader(reader, name)
	// image := canvas.NewImageFromFile(fileName)
	image.FillMode = canvas.ImageFillOriginal
	w.SetContent(image)

	w.ShowAndRun()
}

光栅

canvas.Raster就像一个图像,但在屏幕上的每个像素都会准确地画出一个点。这意味着随着用户界面的缩放或图像大小的调整,将要求更多的像素来填充空间。为了做到这一点,我们使用一个生成器函数,如本例所示–它将被用来返回每个像素的颜色。

生成器函数可以是基于像素的(如本例中,我们为每个像素生成一个新的随机颜色)或基于完整的图像。生成完整的图像(用canvas.NewRaster())效率更高,但有时直接控制像素会更方便。

如果你的像素数据存储在一个图像中,你可以通过NewRasterFromImage()函数加载它,它将加载图像,在屏幕上完美显示像素。

package main

import (
	"image/color"
	"math/rand"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
)

func main() {
	myApp := app.New()
	w := myApp.NewWindow("Raster")

	raster := canvas.NewRasterWithPixels(
		func(_, _, w, h int) color.Color {
			return color.RGBA{uint8(rand.Intn(255)),
				uint8(rand.Intn(255)),
				uint8(rand.Intn(255)), 0xff}
		})
	// raster := canvas.NewRasterFromImage()
	w.SetContent(raster)
	w.Resize(fyne.NewSize(120, 100))
	w.ShowAndRun()
}

渐变

最后一个画布基元类型是渐变,有canvas.LinearGradient和canvas.RadialGradient两种类型,用于绘制从一种颜色到另一种颜色的各种模式的梯度。你可以使用NewHorizontalGradient(), NewVerticalGradient()或NewRadialGradient()创建渐变。

要创建一个渐变,你需要一个开始和结束的颜色–中间的每一个颜色都由画布来计算。在这个例子中,我们使用color.Transparent来展示梯度(或任何其他类型)如何使用一个alpha值来对后面的内容进行半透明。

package main

import (
	"image/color"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
)

func main() {
	myApp := app.New()
	w := myApp.NewWindow("Gradient")

	gradient := canvas.NewHorizontalGradient(color.Black, color.Transparent)
	//gradient := canvas.NewRadialGradient(color.White, color.Transparent)
	w.SetContent(gradient)

	w.Resize(fyne.NewSize(100, 100))
	w.ShowAndRun()
}

Box Layout 盒子的布局

正如在容器和布局中讨论的那样,容器中的元素可以使用布局来安排。本节探讨了内置的布局以及如何使用它们。

最常用的布局是 layout.BoxLayout,它有两种变体:水平和垂直。一个盒式布局将所有的元素安排在一个单一的行或列中,并有可选的空间来帮助对齐。

用layout.NewHBoxLayout()创建的水平盒式布局在单行中创建一个项目的排列。盒子里的每个项目的宽度将被设置为MinSize().Width,高度对所有项目来说是相等的,是所有MinSize().Height值中最大的一个。该布局可以在一个容器中使用,或者你可以使用盒子部件widget.NewHBox()。

一个垂直的盒子布局是类似的,但它将项目安排在一列。每个项目的高度将被设置为最小值,所有的宽度将是相等的,被设置为最小宽度中最大的一个。

为了在元素之间创建一个扩展空间(例如,使一些元素向左对齐,而其他元素向右对齐),添加一个 layout.NewSpacer() 作为其中一个项目。间隔符将扩展以填充所有可用空间。在一个垂直盒式布局的开头添加一个间隔符将导致所有项目都是底部对齐的。你可以在水平布局的开头和结尾添加一个,以创建一个中心对齐。

package main

import (
	"image/color"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/layout"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Box Layout")

	text1 := canvas.NewText("Hello", color.Black)
	text2 := canvas.NewText("There", color.Black)
	text3 := canvas.NewText("(right)", color.Black)
	content := container.New(layout.NewHBoxLayout(), text1, text2, layout.NewSpacer(), text3)

	text4 := canvas.NewText("centered", color.Black)
	centered := container.New(layout.NewHBoxLayout(), layout.NewSpacer(), text4, layout.NewSpacer())
	myWindow.SetContent(container.New(layout.NewVBoxLayout(), content, centered))
	myWindow.ShowAndRun()
}

Grid Layout 网格布局

网格布局以固定列数的网格模式布置容器中的元素。项目将填满单行,直到达到列数,之后将创建一个新的行。垂直空间将被平均分配给每一行的对象。

你使用 layout.NewGridLayout(cols) 创建一个网格布局,其中 cols 是你希望在每行中拥有的项目(列)的数量。然后这个布局被作为第一个参数传递给container.New(…)。

如果你调整了容器的大小,那么每个单元格都会平均调整大小以分享可用空间。

package main

import (
	"image/color"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/layout"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Grid Layout")

	text1 := canvas.NewText("1", color.Black)
	text2 := canvas.NewText("2", color.Black)
	text3 := canvas.NewText("3", color.Black)
	grid := container.New(layout.NewGridLayout(2), text1, text2, text3)
	myWindow.SetContent(grid)
	myWindow.ShowAndRun()
}

Grid Wrap Layout

网格包络布局

像之前的网格布局一样,网格包装布局在网格模式中创建一个元素的排列。但是这个网格没有设定的列数,相反,它为每个单元格使用一个固定的尺寸,然后根据显示项目的需要,将内容流向多少行。

你使用 layout.NewGridWrapLayout(size) 创建一个网格包装布局,其中size指定了应用于所有子元素的尺寸。然后这个布局被作为第一个参数传递给container.New(…)。列和行的数量将根据容器的当前大小来计算。

最初,一个网格包裹的布局将有一个单列,如果你调整它的大小(如右边的代码注释所示),它将重新排列子元素以填补空间。

package main

import (
	"image/color"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/layout"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Grid Wrap Layout")

	text1 := canvas.NewText("1", color.Black)
	text2 := canvas.NewText("2", color.Black)
	text3 := canvas.NewText("3", color.Black)
	grid := container.New(layout.NewGridWrapLayout(fyne.NewSize(50, 50)),
		text1, text2, text3)
	myWindow.SetContent(grid)

	// myWindow.Resize(fyne.NewSize(180, 75))
	myWindow.ShowAndRun()
}

Border Layout 边框布局

边框布局可能是用于构建用户界面的最广泛的布局,因为它允许将项目定位在一个中心元素周围,该元素将扩展以填充空间。要创建一个边框布局,你需要将应该被定位在边框位置的fyne.CanvasObjects传递给布局(以及像往常一样的容器)。这个语法与其他布局有些不同,但基本上只是 layout.NewBorderLayout(top, bottom, left, right) ,如右边的例子所示。

任何传递给容器的项目,如果没有出现在特定的边框位置,将被定位到中央区域,并将展开以填补可用空间。你也可以将nil传递给你希望留空的边框参数。

请注意,中心区域的所有项目都将扩展以填充空间(就像它们在一个layout.MaxLayout容器中一样)。为了自己管理这个区域,你可以创建一个新的fyne.Container(使用container.New())并使用任何你希望的布局。

package main

import (
	"image/color"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/layout"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Border Layout")

	top := canvas.NewText("top bar", color.White)
	left := canvas.NewText("left", color.White)
	middle := canvas.NewText("content", color.White)
	content := container.New(layout.NewBorderLayout(top, nil, left, nil),
		top, left, middle)
	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

Form Layout 表单布局

layout.FormLayout就像一个2列的网格布局,但经过调整后可以在一个应用程序中布置表单。每个项目的高度将是每行中两个最小高度中较大的一个。左边项目的宽度将是第一列中所有项目的最大最小宽度,而每行的第二个项目将扩展以填补空间。

这种布局更多的是在widget.Form中使用(用于验证、提交和取消按钮等),但它也可以直接用layout.NewFormLayout()传递给container.New(…)的第一个参数。

package main

import (
	"image/color"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/layout"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Form Layout")

	label1 := canvas.NewText("Label 1", color.Black)
	value1 := canvas.NewText("Value", color.White)
	label2 := canvas.NewText("Label 2", color.Black)
	value2 := canvas.NewText("Something", color.White)
	grid := container.New(layout.NewFormLayout(), label1, value1, label2, value2)
	myWindow.SetContent(grid)
	myWindow.ShowAndRun()
}

Center Layout 中心布局

layout.CenterLayout将其容器中的所有项目组织到可用空间的中央。对象将按照传递给容器的顺序被绘制,最后一个被绘制在最上面。

中心布局使所有项目保持最小尺寸,如果你希望扩展项目以填充空间,请参见layout.MaxLayout。

package main

import (
	"image/color"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/layout"
	"fyne.io/fyne/v2/theme"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Center Layout")

	img := canvas.NewImageFromResource(theme.FyneLogo())
	img.FillMode = canvas.ImageFillOriginal
	text := canvas.NewText("Overlay", color.Black)
	content := container.New(layout.NewCenterLayout(), img, text)

	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

Max Layout 最大布局

layout.MaxLayout是最简单的布局,它将容器中的所有项目都设置为与容器相同的大小。这在一般的容器中并不经常有用,但在组成小部件时可能很合适。

最大布局将把容器扩大到至少是最大项的最小尺寸。对象将按照传递到容器中的顺序绘制,最后一个将被绘制在最上面。

现在我们知道了如何布局一个用户界面,我们将转向容器包,它简化了布局,让你以更多的方式布局对象。

package main

import (
	"image/color"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/layout"
	"fyne.io/fyne/v2/theme"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Max Layout")

	img := canvas.NewImageFromResource(theme.FyneLogo())
	text := canvas.NewText("Overlay", color.Black)
	content := container.New(layout.NewMaxLayout(), img, text)

	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

布局

应用标签(AppTabs)

AppTabs容器是用来让用户在不同的内容面板之间切换。标签要么只是文本,要么是文本和一个图标。建议不要把一些有图标的标签和一些没有图标的标签混在一起。使用container.NewAppTabs(…)和传递container.TabItem项目(可以使用container.NewTabItem(…)创建)来创建一个标签容器。

可以通过设置标签的位置来配置标签容器,容器.TabLocationTop、容器.TabLocationBottom、容器.TabLocationLeading和容器.TabLocationTrailing其中之一。默认位置是顶部。

界面样式对源代码作了修改。

package main

import (
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	//"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("TabContainer Widget")

	tabs := container.NewAppTabs(
		container.NewTabItem("Tab 1", widget.NewLabel("Hello")),
		container.NewTabItem("Tab 2", widget.NewLabel("World!")),
	)

	//tabs.Append(container.NewTabItemWithIcon("Home", theme.HomeIcon(), widget.NewLabel("Home tab")))

	tabs.SetTabLocation(container.TabLocationLeading)

	myWindow.SetContent(tabs)
	myWindow.ShowAndRun()
}

Box

盒子

盒子部件是一个简单的水平或垂直容器,它使用盒子布局来布置子组件。你可以在container.NewHBox()或container.NewVBox()构造函数中传递要包含的对象。

也可以在盒子部件创建后使用Add()向其添加项目(到现有内容之后)或使用Remove()删除项目。

package main

import (
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Entry Widget")

	content := container.NewVBox(
		widget.NewLabel("The top row of the VBox"),
		container.NewHBox(
			widget.NewLabel("Label 1"),
			widget.NewLabel("Label 2"),
		),
	)

	content.Add(widget.NewButton("Add more items", func() {
		content.Add(widget.NewLabel("Added"))
	}))

	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

组件

Label

标签小组件是其中最简单的–它向用户展示文本。与canvas.Text不同,它可以处理一些简单的格式化(如\n)和包装(通过设置包装字段)。你可以通过调用widget.NewLabel(“some text”)来创建一个标签,其结果可以分配给一个变量或直接传入一个容器。

package main

import (
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Label Widget")

	content := widget.NewLabel("text")

	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

Button

按钮部件可以包含文本、图标或两者,构造函数是widget.NewButton()和widget.NewButtonWithIcon()。要创建一个文本按钮,只有2个参数,字符串内容和一个0参数func(),当按钮被点击时将被调用。关于如何创建,请看例子。

带有图标的按钮构造函数包括一个额外的参数,即包含图标数据的fyne.Resource。主题包中的内置图标都能适当地适应主题的变化。你可以传入你自己的图片,如果它是以资源的形式加载的–诸如fyne.LoadResourceFromPath()这样的助手可以提供帮助,不过我们建议尽可能地捆绑资源。

要创建一个只有图标的按钮,你应该把"“作为标签参数传给widget.NewButtonWithIcon()。

package main

import (
	"log"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
	//"fyne.io/fyne/v2/theme"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Button Widget")

	content := widget.NewButton("click me", func() {
		log.Println("tapped")
	})

	//content := widget.NewButtonWithIcon("Home", theme.HomeIcon(), func() {
	//	log.Println("tapped home")
	//})

	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

Entry

条目小组件用于用户输入简单的文本内容。一个条目可以用一个简单的widget.NewEntry()构造函数来创建。当你创建小组件时,保留一个引用,以便你以后可以访问其文本字段。也可以使用OnChanged回调函数,在每次内容改变时得到通知。

条目小组件也可以有验证功能,以验证输入到它的文本输入。这可以通过将验证器字段设置为fyne.StringValidator来实现。你也可以设置一个PlaceHolder文本,也可以将条目设置为MultiLine,以接受超过一行的文本。

你也可以使用NewPasswordEntry()函数创建一个密码条目(其中的内容被遮挡)。

package main

import (
	"log"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Entry Widget")

	input := widget.NewEntry()
	input.SetPlaceHolder("Enter text...")

	content := container.NewVBox(input, widget.NewButton("Save", func() {
		log.Println("Content was:", input.Text)
	}))

	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

Choices

有各种小部件可用于向用户提供选择,这些小部件包括复选框、单选组和选择弹窗。

widget.Check提供了一个简单的是/否选择,并使用一个字符串标签创建。这些部件中的每一个都需要一个 “改变的 “func(…),其中参数是适当的类型。widget.NewCheck(…)因此需要一个字符串参数作为标签和一个func(bool)参数作为改变处理程序。你也可以使用Checked字段来获得布尔值。

单选部件也是类似的,但第一个参数是代表每个选项的字符串片。改变函数这次期望一个字符串参数来返回当前选择的值。调用widget.NewRadioGroup(…)来构造广播组部件,你可以在以后使用这个引用来读取选择字段,而不是使用改变回调。

选择小组件在构造函数签名中与无线电小组件是相同的。调用widget.NewSelect(…)将显示一个按钮,当点击时显示一个弹出窗口,用户可以从中做出选择。这对长的选项列表来说是更好的。

package main

import (
	"log"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Choice Widgets")

	check := widget.NewCheck("Optional", func(value bool) {
		log.Println("Check set to", value)
	})
	radio := widget.NewRadioGroup([]string{"Option 1", "Option 2"}, func(value string) {
		log.Println("Radio set to", value)
	})
	combo := widget.NewSelect([]string{"Option 1", "Option 2"}, func(value string) {
		log.Println("Select set to", value)
	})

	myWindow.SetContent(container.NewVBox(check, radio, combo))
	myWindow.ShowAndRun()
}

Form

表单部件用于布置许多带有标签和可选的取消和提交按钮的输入字段。在其最原始的形式下,它将标签排列在每个输入小部件的左边。通过设置OnCancel或OnSubmit,表单将添加一个按钮栏,在适当的时候调用指定的处理程序。

小组件可以用widget.NewForm(…)创建,传递一个widget.FormItems的列表,或者使用例子中说明的&widget.Form{}语法。还有一个有用的Form.Append(label, widget),可以用于替代语法。

在这个例子中,我们创建了两个条目,其中一个是一个 “多行”(像HTML TextArea),用来保存数值。有一个OnSubmit处理程序,在关闭窗口(也就是关闭应用程序)之前打印信息。

package main

import (
	"log"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Form Widget")

	entry := widget.NewEntry()
	textArea := widget.NewMultiLineEntry()

	form := &widget.Form{
		Items: []*widget.FormItem{ // we can specify items in the constructor
			{Text: "Entry", Widget: entry}},
		OnSubmit: func() { // optional, handle form submission
			log.Println("Form submitted:", entry.Text)
			log.Println("multiline:", textArea.Text)
			myWindow.Close()
		},
	}

	// we can also append items
	form.Append("Text", textArea)

	myWindow.SetContent(form)
	myWindow.ShowAndRun()
}

ProgressBar

进度条部件有两种形式,标准的进度条显示用户已经达到了哪个值,从最小到最大。默认的最小值是0.0,最大值默认为1.0。要使用默认值,只需调用widget.NewProgressBar()。创建后,你可以设置Value字段。

要设置一个自定义范围,你可以手动设置最小和最大字段。标签将始终显示完成百分比。

进度部件的另一种形式是无限的进度条。这个版本只是通过将条形图的一段从左到右再移动来显示一些活动正在进行。你用widget.NewProgressBarInfinite()来创建它,它将在显示时开始动画。

package main

import (
	"time"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("ProgressBar Widget")

	progress := widget.NewProgressBar()
	infinite := widget.NewProgressBarInfinite()

	go func() {
		for i := 0.0; i <= 1.0; i += 0.1 {
			time.Sleep(time.Millisecond * 250)
			progress.SetValue(i)
		}
	}()

	myWindow.SetContent(container.NewVBox(progress, infinite))
	myWindow.ShowAndRun()
}

Toolbar

工具栏部件创建了一排行动按钮,使用图标来代表每个按钮。widget.NewToolbar(…)构造函数接收一个widget.ToolbarItem参数的列表。工具栏项目的内置类型是动作、分隔器和间隔器。

最常用的项目是使用widget.NewToolbarItemAction(..)函数创建的动作。一个动作需要两个参数,第一个是要绘制的图标资源,第二个是点击时要调用的函数()。这将创建一个标准的工具条按钮。

你可以使用widget.NewToolbarSeparator()来在工具栏的项目之间创建一个小的分隔线(通常是一条细的垂直线)。最后,你可以使用widget.NewToolbarSpacer()来在元素之间创建一个灵活的空间。这对右对齐列在间隔条之后的工具栏项目最有用。

工具栏应该总是在内容区的顶部,所以使用layout.BorderLayout将其添加到fyne.Container中,使其在其他内容上方对齐是正常的。

package main

import (
	"log"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Toolbar Widget")

	toolbar := widget.NewToolbar(
		widget.NewToolbarAction(theme.DocumentCreateIcon(), func() {
			log.Println("New document")
		}),
		widget.NewToolbarSeparator(),
		widget.NewToolbarAction(theme.ContentCutIcon(), func() {}),
		widget.NewToolbarAction(theme.ContentCopyIcon(), func() {}),
		widget.NewToolbarAction(theme.ContentPasteIcon(), func() {}),
		widget.NewToolbarSpacer(),
		widget.NewToolbarAction(theme.HelpIcon(), func() {
			log.Println("Display help")
		}),
	)

	content := container.NewBorder(toolbar, nil, nil, nil, widget.NewLabel("Content"))
	myWindow.SetContent(content)
	myWindow.ShowAndRun()
}

List

列表小组件是工具包的集合小组件之一。这些部件的设计是为了在呈现大量数据时帮助建立真正的高性能界面。你还可以看到表和树形部件,它们有类似的API。由于这种设计,它们的使用就比较复杂了。

列表使用回调函数在需要的时候询问数据。有3个主要的回调,Length, CreateItem 和 UpdateItem。长度回调(首先传递)是最简单的,它返回要呈现的数据中有多少个项目。其他的与模板有关–图形元素如何被创建、缓存和重复使用。

CreateItem回调会返回一个新的模板对象。当小组件被展示时,这将被重新使用真实的数据。这个对象的MinSize将影响List的最小尺寸。最后,UpdateItem被调用来应用一个数据项到一个缓存的模板。使用这个来设置准备显示的内容。

package main

import (
	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
)

var data = []string{"a", "string", "list"}

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("List Widget")

	list := widget.NewList(
		func() int {
			return len(data)
		},
		func() fyne.CanvasObject {
			return widget.NewLabel("template")
		},
		func(i widget.ListItemID, o fyne.CanvasObject) {
			o.(*widget.Label).SetText(data[i])
		})

	myWindow.SetContent(list)
	myWindow.ShowAndRun()
}

Table

表组件与列表组件(工具包的另一个集合小组件)一样,有一个二维索引。与List一样,它的设计是为了在展示大量数据时帮助建立真正的高性能界面。正因为如此,这个小组件在创建时并没有嵌入所有的数据,而是在需要时调用数据源。

表使用回调函数在需要时请求数据。有3个主要的回调,Length, CreateCell 和 UpdateCell。长度回调(首先传递)是最简单的,它返回要呈现的数据中有多少个项目,它返回的两个ints代表行和列的数量。另外两个与内容模板有关。

CreateItem回调会返回一个新的模板对象,就像list一样。不同的是,MinSize将定义每个单元格的标准尺寸,以及表格的最小尺寸(它至少显示一个单元格)。和以前一样,UpdateItem被调用来应用数据到一个单元格模板。传入的索引是相同的(row, col)int对。

package main

import (
	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/widget"
)

var data = [][]string{[]string{"top left", "top right"},
	[]string{"bottom left", "bottom right"}}

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Table Widget")

	list := widget.NewTable(
		func() (int, int) {
			return len(data), len(data[0])
		},
		func() fyne.CanvasObject {
			return widget.NewLabel("wide content")
		},
		func(i widget.TableCellID, o fyne.CanvasObject) {
			o.(*widget.Label).SetText(data[i.Row][i.Col])
		})

	myWindow.SetContent(list)
	myWindow.ShowAndRun()
}

Data Binding

数据绑定是Fyne工具包的一个强大的新功能,在v2.0.0版本中引入。 通过使用数据绑定,我们可以避免手动管理许多标准对象,如标签、按钮和列表。

内置的绑定支持许多原始类型(如Int, String, Float等),列表(如StringList, BoolList)以及Map和Struct绑定。这些类型中的每一个都可以通过一个简单的构造函数来创建。例如,要创建一个新的零值的字符串绑定,你可以使用 binding.NewString()。你可以使用Get和Set方法来获取或设置大多数数据绑定的值。

也可以使用类似的函数来绑定一个现有的值,这些函数的名字以Bind开头,它们都接受一个指向绑定类型的指针。要绑定到一个现有的int,我们可以使用binding.BindInt(&myInt)。通过保留对绑定值的引用而不是原始变量,我们可以配置部件和函数来自动响应任何变化。如果你直接改变了外部数据,一定要调用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)
}

绑定简单的小部件

绑定一个widget的最简单的方法是把一个绑定的项目作为一个值传递给它,而不是一个静态值。许多小组件提供了一个WithData构造函数,它将接受一个类型的数据绑定项目。要设置绑定,你需要做的就是把正确的类型传进去。

虽然这在最初的代码中可能看起来没有什么好处,但你可以看到它是如何确保显示的内容总是与数据的来源同步的。你会注意到,我们不需要在Label widget上调用Refresh(),甚至不需要保留对它的引用,但它却能适当地更新。

在下一步,我们看看如何通过双向绑定来设置编辑值的部件。

package main

import (
	"time"

	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/data/binding"
	"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)
	w.SetContent(text)

	go func() {
		time.Sleep(time.Second * 2)
		str.Set("A new string")
	}()

	w.ShowAndRun()
}

双向绑定 到目前为止,我们已经将数据绑定视为保持用户界面元素最新的一种方式。然而,更常见的是需要从UI部件更新值,并在任何地方保持数据最新。 谢天谢地,Fyne中提供的绑定是“双向”的,这意味着可以将值推入其中并读取。数据更改将传达给所有连接的代码,无需任何附加代码。 要查看此操作,我们可以更新上一个测试应用程序,以显示绑定到相同值的标签和条目。通过设置,您可以看到通过条目编辑值也会更新标签中的文本。无需调用刷新或引用代码中的小部件,这一切都是可能的。 通过移动应用程序使用数据绑定,您可以停止保存指向所有小部件的指针。通过将数据捕获为一组绑定值,您的用户界面可以是完全独立的代码。更易于阅读和管理。 接下来,我们将了解如何在数据中添加转换。

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")

	str := binding.NewString()
	str.Set("Hi!")

	w.SetContent(container.NewVBox(
		widget.NewLabelWithData(str),
		widget.NewEntryWithData(str),
	))

	w.ShowAndRun()
}

数据转换 到目前为止,我们已经使用了数据绑定,其中数据类型与输出类型匹配(例如字符串和标签或条目)。通常情况下,需要提供尚未采用正确格式的数据。为此,绑定包提供了许多有用的转换函数。 最常见的是,这将用于将不同类型的数据转换为字符串,以便在标签或条目小部件中显示。请参见代码中如何使用binding.FloatToString将浮点转换为字符串。可以通过移动滑块编辑原始值。每次数据更改时,它都会运行转换代码并更新任何连接的小部件。 还可以使用格式字符串为用户界面添加更自然的输出。您可以看到,我们的短绑定也在将浮点转换为字符串,但是通过使用WithFormat助手,我们可以传递一个格式字符串(类似于fmt包)以提供自定义输出。 最后,在本节中,我们将查看列表数据。

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("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.ShowAndRun()
}

列表数据 为了演示如何连接更复杂的类型,我们将查看列表小部件以及数据绑定如何使其更易于使用。首先,我们创建一个StringList数据绑定,它是一个字符串数据类型的列表。一旦我们有了列表类型的数据,我们就可以将其连接到标准列表小部件。为此,我们使用widget.NewListWithData构造函数,与其他小部件非常相似。 将此代码与列表教程进行比较,您将看到两个主要更改,第一个是我们将数据类型作为第一个参数而不是长度回调函数传递。第二个更改是最后一个参数,即UpdateItem回调。修订版采用binding.DataItem值,而不是widget.ListIndexID。当使用这个回调结构时,我们应该绑定到模板标签小部件,而不是调用SetText。这意味着,如果数据源中的任何字符串发生更改,表中每个受影响的行都将刷新。 在我们的演示代码中有一个“Append”按钮,点击它将向数据源追加一个新值。这样做将自动触发数据更改处理程序,并展开列表小部件以显示新数据。

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 Data")

	data := binding.BindStringList(
		&[]string{"Item 1", "Item 2", "Item 3"},
	)

	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("Append", func() {
		val := fmt.Sprintf("Item %d", data.Length()+1)
		data.Append(val)
	})
	myWindow.SetContent(container.NewBorder(nil, add, nil, nil, list))
	myWindow.ShowAndRun()
}

扩展小部件

标准Fyne小部件提供最低限度的功能和定制,以支持大多数用例。在某些时候,它可能需要具有更高级的功能。与其让开发人员构建自己的小部件,还可以扩展现有的小部件。 例如,我们将扩展图标小部件以支持点击。为此,我们声明了一个嵌入widget.Icon类型的新结构。我们创建一个构造函数来调用重要的ExtendBasweWidget函数。

以下示例演示了一个可以点击的icon。

package main

import (
	"log"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

type tappableIcon struct {
	widget.Icon
}

func newTappableIcon(res fyne.Resource) *tappableIcon {
	icon := &tappableIcon{}
	icon.ExtendBaseWidget(icon)
	icon.SetResource(res)

	return icon
}
func (t *tappableIcon) Tapped(_ *fyne.PointEvent) {
	log.Println("I have been tapped")
}

func (t *tappableIcon) TappedSecondary(_ *fyne.PointEvent) {
}

func main() {
	a := app.New()
	w := a.NewWindow("Tappable")
	w.SetContent(newTappableIcon(theme.FyneLogo()))
	w.ShowAndRun()
}

相关文章