Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(中)
5.10 表格 (Table)
Fyne v2.8.0 推荐使用 widget.NewTableWithHeaders 创建带固化表头的表格,这是 v2.4 版本引入的重大改进。表头行和表头列不参与内容滚动。
5.10.1 基本用法(NewTableWithHeaders)
go
// 来源: fyne demo (v2.8.0) --- tutorials/collection.go makeTableTab()
// NewTableWithHeaders 自动创建独立的表头行和表头列(固化,不随着内容滚动)
func makeTableTab(_ fyne.Window) fyne.CanvasObject {
t := widget.NewTableWithHeaders(
// Length: 返回数据区的行数和列数(不含表头)
func() (int, int) { return 500, 150 },
// CreateCell: 单元格模板
func() fyne.CanvasObject {
return widget.NewLabel("Cell 000, 000")
},
// UpdateCell: 渲染第 id.Row 行、第 id.Col 列的数据单元格
func(id widget.TableCellID, cell fyne.CanvasObject) {
label := cell.(*widget.Label)
switch id.Col {
case 0:
label.SetText("A longer cell")
default:
label.SetText(fmt.Sprintf("Cell %d, %d", id.Row+1, id.Col+1))
}
},
)
// 设置第 0 列宽度为 102 像素
t.SetColumnWidth(0, 102)
// 设置第 2 行(数据行)高度为 50 像素
t.SetRowHeight(2, 50)
return t
}
核心区别 :
NewTableWithHeaders内部自动设置ShowHeaderRow=true和ShowHeaderColumn=true,数据区索引从 (0,0) 开始,表头由框架自动管理。
5.10.2 自定义表头内容
如果需要自定义表头显示的文本(默认是 A、B、C... 和 1、2、3...),可覆写 CreateHeader 和 UpdateHeader:
go
// 来源: fyne v2.8.0 widget/table.go Table struct 字段
// 自定义表头:显示中文列名和行号
t := widget.NewTable(
func() (int, int) { return 100, 4 },
func() fyne.CanvasObject {
return widget.NewLabel("")
},
func(id widget.TableCellID, cell fyne.CanvasObject) {
cell.(*widget.Label).SetText(
fmt.Sprintf("Row %d, Col %d", id.Row, id.Col),
)
},
)
// 开启表头显示
t.ShowHeaderRow = true
t.ShowHeaderColumn = true
// 自定义表头单元格模板和渲染
t.CreateHeader = func() fyne.CanvasObject {
return widget.NewLabelWithStyle("", fyne.TextAlignCenter, fyne.TextStyle{Bold: true})
}
t.UpdateHeader = func(id widget.TableCellID, cell fyne.CanvasObject) {
lbl := cell.(*widget.Label)
if id.Row == -1 && id.Col >= 0 {
// 列头:id.Row = -1,id.Col 是列号
lbl.SetText([]string{"序号", "姓名", "成绩", "班级"}[id.Col])
} else if id.Col == -1 && id.Row >= 0 {
// 行头:id.Col = -1,id.Row 是行号
lbl.SetText(fmt.Sprintf("No.%d", id.Row+1))
}
}
UpdateHeader 的
id参数规则:
id.Row == -1, id.Col >= 0→ 列头(顶部水平表头),id.Col为列索引。id.Col == -1, id.Row >= 0→ 行头(左侧垂直表头),id.Row为行索引。id.Row == -1, id.Col == -1→ 左上角交叉单元格。
5.10.3 选择事件
go
// 单元格被选中
t.OnSelected = func(id widget.TableCellID) {
fmt.Printf("选中了第 %d 行, 第 %d 列\n", id.Row, id.Col)
}
// 单元格取消选中
t.OnUnselected = func(id widget.TableCellID) {
fmt.Printf("取消选中第 %d 行, 第 %d 列\n", id.Row, id.Col)
}
// 编程方式选中第 3 行第 1 列
t.Select(widget.TableCellID{Row: 3, Col: 1})
// 取消所有选中
t.UnselectAll()
5.10.4 固化行/列(Sticky)
go
// 固化前 2 行数据,使其在垂直滚动时保持可见
t.StickyRowCount = 2
// 固化前 1 列数据,使其在水平滚动时保持可见
t.StickyColumnCount = 1
// 注意:StickyRowCount 的数据行位于表头行下方(如果 ShowHeaderRow=true)
5.10.5 列宽与行高控制
go
// 设置第 0 列宽度
t.SetColumnWidth(0, 120)
// 设置第 3 行高度
t.SetRowHeight(3, 48)
// SetColumnWidth(-1, w) 可设置所有未指定列的默认宽度
t.SetColumnWidth(-1, 80)
// SetRowHeight(-1, h) 可设置所有未指定行的默认高度
t.SetRowHeight(-1, 32)
5.10.6 滚动控制
go
// 滚动到指定单元格(使其进入可视区域)
t.ScrollTo(widget.TableCellID{Row: 42, Col: 3})
// 滚动到顶部
t.ScrollToTop()
// 滚动到底部
t.ScrollToBottom()
// 滚动到最左侧
t.ScrollToLeading()
// 滚动到最右侧
t.ScrollToTrailing()
// 滚动到指定偏移量
t.ScrollToOffset(fyne.NewPos(100, 200))
5.10.7 样式控制
go
// 隐藏单元格之间的分隔线(v2.5+)
t.HideSeparators = true
// 刷新指定单元格(数据变化后局部更新)
t.RefreshItem(widget.TableCellID{Row: 5, Col: 2})
// 键盘导航支持(方向键移动高亮,Enter/Space 触发选择)
Table API 速查(v2.8.0):
方法/字段 说明 版本 NewTable(len, create, update)基础表格(无内置表头) v1.0 NewTableWithHeaders(len, create, update)带固化表头的表格 v2.4 ShowHeaderRow/ShowHeaderColumn是否显示表头行/列 v2.4 CreateHeader/UpdateHeader自定义表头模板和渲染 v2.4 StickyRowCount/StickyColumnCount固化前 N 行/列 v2.4 HideSeparators隐藏分隔线 v2.5 OnSelected/OnUnselected选择/取消选择回调 v1.0 Select(id)/Unselect(id)/UnselectAll()选中/取消选中控制 v1.0 SetColumnWidth(col, w)设置列宽(col=-1 为默认) v1.0 SetRowHeight(row, h)设置行高(row=-1 为默认) v1.0 ScrollTo(id)/ScrollToTop()/ScrollToBottom()滚动到指定位置 v2.0 RefreshItem(id)局部刷新单元格 v2.0 Length/CreateCell/UpdateCell核心三回调(公开字段) v1.0
5.11 滑块 (Slider)
go
// 来源: fynewh/widgetSlider.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func HBoxSlider() *fyne.Container {
lblMsg := widget.NewLabel("")
// 创建滑块:参数为最小值和最大值
sld := widget.NewSlider(0, 100)
sld.Step = 1 // 步长
sld.OnChanged = func(f float64) {
lblMsg.SetText(fmt.Sprintf("Slider: %.0f", f))
}
// 设置默认值
sld.Value = 50
// 步长滑块
sld1 := widget.NewSlider(0, 100)
sld1.Step = 5
sld1.OnChanged = func(f float64) {
lblMsg.SetText(fmt.Sprintf("Slider Step 5: %.0f", f))
}
return container.NewVBox(
sld,
sld1,
lblMsg,
)
}
说明:
NewSlider(min, max)--- 创建滑块,滑动范围[min, max]。.Step--- 步长,设为 0 时连续滑动。.OnChanged--- 值变化回调,参数类型float64。.Value--- 当前值。
5.12 进度条 (ProgressBar)
进度条有三种形态:普通百分比、自定义文本格式、无限循环加载动画,此外还支持数据绑定。
5.12.1 三种进度条 + 动画驱动
go
// 来源: fyne demo v2.8.0 --- tutorials/widget.go
import (
"fmt"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func makeProgressTab(_ fyne.Window) fyne.CanvasObject {
// 类型1:普通百分比进度条(默认显示百分比)
percentBar := widget.NewProgressBar()
// 类型2:自定义文本格式进度条
formattedBar := widget.NewProgressBar()
formattedBar.TextFormatter = func() string {
return fmt.Sprintf("%.2f out of %.2f", formattedBar.Value, formattedBar.Max)
}
// 类型3:无限循环进度条(加载动画)
infiniteBar := widget.NewProgressBarInfinite()
// 使用 Fyne Animation API 驱动进度变化(0 → 100%,时长 10 秒)
progressAnim := fyne.Animation{
Curve: fyne.AnimationLinear,
Duration: 10 * time.Second,
Tick: func(percentage float32) {
value := float64(percentage)
percentBar.SetValue(value)
formattedBar.SetValue(value)
},
}
progressAnim.Start()
return container.NewVBox(
widget.NewLabel("Percent"), percentBar,
widget.NewLabel("Formatted"), formattedBar,
widget.NewLabel("Infinite"), infiniteBar,
)
}
5.12.2 数据绑定进度条
go
// 来源: fyne demo v2.8.0 --- tutorials/bind.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
)
func bindingProgressBarDemo() *fyne.Container {
f := 0.2
data := binding.BindFloat(&f) // 绑定 float64 数据源
// 数据绑定的滑块 + 进度条,共享同一个数据源
slide := widget.NewSliderWithData(0, 1, data)
slide.Step = 0.01
bar := widget.NewProgressBarWithData(data) // 进度条自动跟随 data 变化
// 按钮直接修改数据源,进度条和滑块同步变化
buttons := container.NewGridWithColumns(4,
widget.NewButton("0%", func() { data.Set(0) }),
widget.NewButton("30%", func() { data.Set(0.3) }),
widget.NewButton("70%", func() { data.Set(0.7) }),
widget.NewButton("100%", func() { data.Set(1) }),
)
return container.NewVBox(slide, bar, buttons)
}
API 说明:
构造函数 说明 widget.NewProgressBar()有限进度条, Value范围0.0 ~ 1.0,Max默认为1.0widget.NewProgressBarInfinite()无限循环进度条(加载中动画),调用 .Start()开始、.Stop()停止widget.NewProgressBarWithData(data)数据绑定进度条,传入 binding.Float,自动双向同步
属性/方法 说明 .Value/.Max当前值 / 最大值,修改后自动重绘 .SetValue(v)设置进度值 0 ~ Max.TextFormatter自定义进度文本函数,签名 func() string,返回显示文字
5.13 折叠面板 (Accordion)
go
// 来源: fynewh/widgetAccordion.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func VBoxAccordion(w fyne.Window) *fyne.Container {
// 创建折叠面板
acc := widget.NewAccordion(
// 每个 AccordionItem 是一个可展开/折叠的区域
widget.NewAccordionItem("面板1", widget.NewLabel("内容1")),
widget.NewAccordionItem("面板2", widget.NewLabel("内容2")),
)
// 打开指定面板(索引从 0 开始)
acc.Open(0)
// 允许多个面板同时打开
acc.MultiOpen = true
return container.NewVBox(acc)
}
说明:
NewAccordion(items...)--- 创建折叠面板。NewAccordionItem("标题", content)--- 一个可折叠的项。.Open(index)--- 展开指定索引的面板。.CloseAll()--- 关闭所有面板(v2.2+)。.MultiOpen = true--- 允许多面板同时展开。acc.Items--- 获取[]*AccordionItem列表,可遍历/修改。
5.14 富文本 (RichText)
go
// 来源: fynewh/widgetRichText.go, fyne demo v2.8.0 tutorials/welcome.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
// 方式1:使用预定义样式段
s1 := &widget.TextSegment{
Style: widget.RichTextStyleHeading, // 标题样式
Text: "Hello World 1\n",
}
s2 := &widget.TextSegment{
Style: widget.RichTextStyleCodeBlock, // 代码块样式
Text: "Hello World 2\n",
}
rt := widget.NewRichText(s1, s2)
// 方式2:从文本创建(自动解析 Markdown 风格标记)
rtt := widget.NewRichTextWithText(
"# 标题一\n ## 标题二\n ```go\npackage main\n```\n")
// 方式3:从 Markdown 字符串创建(推荐)
rtmd := widget.NewRichTextFromMarkdown(
"# title1 \n ## title2 \n ```\n <html>\n</html>\n```\n")
// 方式4:在 RichText 中嵌入超链接(v2.8.0)
welcomeText := widget.NewRichTextFromMarkdown("# Welcome")
welcomeText.Wrapping = fyne.TextWrapWord // 自动换行
welcomeText.Scroll = container.ScrollNone // 禁止滚动
// 遍历 Segments,找到并处理超链接
for i, seg := range welcomeText.Segments {
if link, ok := seg.(*widget.HyperlinkSegment); ok {
// 为新打开的 URL 添加跳转行为
welcomeText.Segments[i] = &widget.HyperlinkSegment{
Alignment: link.Alignment,
Text: link.Text,
URL: link.URL,
}
}
}
说明:
NewRichText(segments...)--- 手动组装富文本。NewRichTextWithText(text)--- 从文本创建(支持#标题和 ``````````` 代码块)。NewRichTextFromMarkdown(md)--- 从 Markdown 字符串创建(推荐方式)。.Wrapping--- 文本换行模式:fyne.TextWrapWord、fyne.TextWrapOff、fyne.TextTruncate。.Segments--- 可遍历的富文本段列表,支持类型断言处理特定段类型(如*widget.HyperlinkSegment)。- 内置样式:
RichTextStyleHeading(标题)、RichTextStyleCodeBlock(代码块)、RichTextStyleEmphasis(斜体强调)、RichTextStyleStrong(加粗)、RichTextStyleParagraph(段落)、RichTextStyleBlockquote(引用)。
5.15 树控件 (Tree)
go
// 来源: fynewh/widgetTree.go(核心用法提取)
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func VBoxTree(w fyne.Window) *fyne.Container {
// 数据:父子关系的字符串映射
parent := map[string][]string{
"": {"动物", "植物"}, // 根节点
"动物": {"哺乳动物", "鸟类"}, // 动物子节点
"哺乳动物": {"猫", "狗", "老虎"}, // 哺乳动物子节点
"鸟类": {"麻雀", "鹦鹉"},
}
tree := widget.NewTreeWithStrings(parent)
// 展开所有节点
tree.OpenAllBranches()
// 选中回调
tree.OnSelected = func(id widget.TreeNodeID) {
fmt.Println("选中的节点:", id)
}
return container.NewVBox(tree)
}
说明:
NewTreeWithStrings(map)--- 用字符串映射创建树。key 为空字符串""表示根节点。.OpenBranch(id)--- 展开指定节点。.CloseBranch(id)--- 折叠指定节点。.OnSelected--- 选择节点回调。
5.16 图标 (Icon)
go
// 来源: fynewh/widgetIcon.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func VBoxIcon(w fyne.Window) *fyne.Container {
// 使用内置主题图标
icon1 := widget.NewIcon(theme.FyneLogo())
// 可用的内置图标:
// theme.HomeIcon() - 首页
// theme.CancelIcon() - 取消
// theme.ConfirmIcon() - 确认
// theme.ContentAddIcon() - 添加
// ...
return container.NewVBox(icon1)
}
// 带点击事件的自定义图标
type tappableIcon struct {
widget.Icon // 继承标准 Icon
output *widget.Label // 点击后输出的标签
}
func NewTappableIcon(res fyne.Resource, output *widget.Label) *tappableIcon {
icon := &tappableIcon{}
icon.ExtendBaseWidget(icon) // 必须调用以正确绑定渲染器
icon.SetResource(res)
icon.output = output
return icon
}
// 实现 Tappable 接口 ------ 鼠标左键单击
func (t *tappableIcon) Tapped(*fyne.PointEvent) {
t.output.SetText("图标被点击")
}
// 使用带点击事件的图标
func VBoxIconClick(w fyne.Window) *fyne.Container {
output := widget.NewLabel("hello world!")
return container.NewVBox(
NewTappableIcon(theme.FyneLogo(), output),
output,
)
}
说明:
NewIcon(resource)--- 创建图标控件。theme.*Icon()--- 获取内置主题图标资源。ExtendBaseWidget(widget)--- 自定义控件必须调用此方法,将自定义类型注册到 Fyne 的渲染系统中。
5.17 超链接 (Hyperlink)
go
// 来源: fynewh/widgetHyperlink.go(核心用法提取)
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func HBoxHyperlink() *fyne.Container {
// 创建超链接:参数为文字和 URL
link, _ := url.Parse("https://www.google.com")
hyperlink := widget.NewHyperlink("打开Google", link)
// 或者从字符串创建
hyperlink2 := widget.NewHyperlinkWithStyle(
"baidu.com",
link,
fyne.TextAlignLeading, // 对齐方式
fyne.TextStyle{Bold: true}, // 字体样式
)
return container.NewVBox(hyperlink, hyperlink2)
}
说明:
NewHyperlink(text, url)--- 创建超链接,点击后在浏览器打开 URL。NewHyperlinkWithStyle(text, url, align, style)--- 带样式的超链接。
5.18 文本网格 (TextGrid)
go
// 来源: fynewh/widgetTextGrid.go
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func VBoxTextGrid(w fyne.Window) *fyne.Container {
// 从字符串创建文本网格
grid1 := widget.NewTextGridFromString(
"TextGrid\nContainer\nShowLineNumbers\nShowWhitespace")
// 设置指定范围的样式(行0列0 到 行0列7)
grid1.SetStyleRange(0, 4, 0, 7, &widget.CustomTextGridStyle{
BGColor: &color.NRGBA{R: 64, G: 64, B: 192, A: 128},
})
// 显示行号
grid1.ShowLineNumbers = true
// 显示空白字符
grid1.ShowWhitespace = true
return container.NewVBox(grid1)
}
说明:
NewTextGridFromString(text)--- 从字符串创建文本网格(适合显示日志、代码等)。.SetStyleRange(startRow, startCol, endRow, endCol, style)--- 设置单元格范围的样式。.ShowLineNumbers--- 是否显示行号列。.ShowWhitespace--- 是否显示空白字符(空格、制表符等)。
5.19 选项卡 (Tab)
go
// 来源: fynewh/containerAppTab.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func VBoxContainerAppTab(w fyne.Window) *fyne.Container {
var tabs *container.AppTabs
// 创建选项卡内容
tab1 := container.NewHBox(widget.NewLabel("内容1"))
tab2 := container.NewHBox(widget.NewLabel("内容2"))
// 创建选项卡容器
tabs = container.NewAppTabs(
container.NewTabItem("标签1", tab1),
container.NewTabItem("标签2", tab2),
)
// 动态添加选项卡
btnAddTab := widget.NewButton("添加Tab", func() {
tabs.Append(container.NewTabItem("新标签",
container.NewHBox(widget.NewLabel("新内容"))))
// 选中最后一个标签
tabs.SelectIndex(len(tabs.Items) - 1)
// 设置标签图标
tabs.Items[len(tabs.Items)-1].Icon = theme.ContentAddIcon()
})
// 移除当前选项卡
btnRemoveTab := widget.NewButton("移除Tab", func() {
tabs.RemoveIndex(tabs.SelectedIndex())
})
return container.NewVBox(btnAddTab, btnRemoveTab, tabs)
}
// DocTabs ------ 带关闭按钮的选项卡
func VBoxContainerDocTab(w fyne.Window) *fyne.Container {
// 创建 DocTabs,用法与 AppTabs 相同,但标签上有关闭按钮
tabs := container.NewDocTabs(
container.NewTabItem("标签1", widget.NewLabel("内容1")),
container.NewTabItem("标签2", widget.NewLabel("内容2")),
)
// 点击关闭按钮时触发
tabs.OnClose = func(item *container.TabItem) {
fmt.Println("关闭了:", item.Text)
}
return container.NewVBox(tabs)
}
说明:
NewAppTabs(items...)--- 创建标准选项卡,标签在顶部。NewDocTabs(items...)--- 创建带关闭按钮的选项卡。.Append(item)--- 动态添加标签。.RemoveIndex(i)--- 移除标签。.SelectIndex(i)--- 选中指定索引的标签。.SelectedIndex()--- 获取当前选中索引。
5.20 弹出菜单 (PopUpMenu)
go
// 来源: fynewh/widgetPopUpMenu.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func VBoxPopUpMenu(w fyne.Window) *fyne.Container {
lblMsg := widget.NewLabel("")
// 创建弹出菜单按钮
btn := widget.NewButton("右键菜单", func() {
// 创建弹出菜单
menu := widget.NewPopUpMenu(
fyne.NewMenu("",
fyne.NewMenuItem("剪切", func() {
lblMsg.SetText("剪切")
}),
fyne.NewMenuItem("复制", func() {
lblMsg.SetText("复制")
}),
fyne.NewMenuItemSeparator(), // 分割线
fyne.NewMenuItem("粘贴", func() {
lblMsg.SetText("粘贴")
}),
),
w.Canvas(),
)
// 在按钮右下角位置显示菜单
menu.ShowAtPosition(fyne.NewPos(
btn.Position().X,
btn.Position().Y+btn.Size().Height,
))
})
return container.NewVBox(btn, lblMsg)
}
说明:
NewPopUpMenu(menu, canvas)--- 创建弹出菜单,需要传入w.Canvas()。.ShowAtPosition(pos)--- 在指定坐标显示菜单。fyne.NewMenuItemSeparator()--- 添加分割线。
5.21 工具栏 (Toolbar)
go
// 来源: fynewh/widgetToolbar.go(核心用法提取)
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func VBoxToolBar(w fyne.Window) *fyne.Container {
lblMsg := widget.NewLabel("")
// 创建工具栏
toolbar := widget.NewToolbar(
widget.NewToolbarAction(theme.HomeIcon(), func() {
lblMsg.SetText("首页")
}),
widget.NewToolbarAction(theme.ContentAddIcon(), func() {
lblMsg.SetText("添加")
}),
widget.NewToolbarSeparator(), // 分隔符
widget.NewToolbarAction(theme.CancelIcon(), func() {
lblMsg.SetText("取消")
}),
)
return container.NewVBox(toolbar, lblMsg)
}
说明:
NewToolbar(items...)--- 创建工具栏。NewToolbarAction(icon, callback)--- 工具栏按钮(动作项)。NewToolbarSeparator()--- 工具栏分隔线。
5.22 菜单栏 (Menu)
go
// 来源: fynewh/widgetMenu.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func HBoxMenu(w fyne.Window) *fyne.Container {
lblMsg := widget.NewLabel("")
// 创建文件菜单
fileMenu := fyne.NewMenu("fileMenu",
fyne.NewMenuItem("Open", func() {
lblMsg.SetText("Open")
}),
fyne.NewMenuItem("Save", func() {
lblMsg.SetText("Save")
}),
fyne.NewMenuItem("Close", func() {
lblMsg.SetText("Close")
}),
)
// 创建编辑菜单
editMenu := fyne.NewMenu("编辑",
fyne.NewMenuItem("撤消", func() {
lblMsg.SetText("撤消")
}),
fyne.NewMenuItem("恢复", func() {
lblMsg.SetText("恢复")
}),
fyne.NewMenuItemSeparator(),
fyne.NewMenuItem("剪切", func() {
lblMsg.SetText("剪切")
}),
)
// 设置窗口主菜单
mainMenu := fyne.NewMainMenu(fileMenu, editMenu)
w.SetMainMenu(mainMenu)
return container.NewHBox(widget.NewSeparator(), lblMsg)
}
说明:
fyne.NewMenu("菜单名", items...)--- 创建一个下拉菜单。fyne.NewMainMenu(menus...)--- 创建菜单栏。w.SetMainMenu(mainMenu)--- 将菜单栏设置到窗口上。fyne.NewMenuItemSeparator()--- 菜单分隔线。
5.23 网格环绕 (GridWrap)
GridWrap (v2.4+) 是 List 的网格版本,根据容器宽度自动换行排列。适合图库、图标网格等场景:
go
// 来源: fyne demo v2.8.0 --- tutorials/collection.go makeGridWrap()
import (
"strconv"
"fyne.io/fyne/v2/widget"
)
// NewGridWrapWithData --- 数据驱动方式创建 GridWrap
gridWrap := widget.NewGridWrapWithData(
// data: 任意类型切片(绑定后自动监听数据变化)
binding.BindStringList(
&[]string{"Item 0", "Item 1", "Item 2", "Item 3"},
),
// createItem: 单元格模板
func() fyne.CanvasObject {
return widget.NewLabel("template")
},
// updateItem: 数据绑定到单元格
func(di binding.DataItem, co fyne.CanvasObject) {
co.(*widget.Label).Bind(di.(binding.String))
},
)
// 手动方式(不使用数据绑定)
gridWrap := widget.NewGridWrap(
func() int { return 50 }, // 总项数
func() fyne.CanvasObject { // 单元格模板
return widget.NewLabel("template")
},
func(id widget.GridWrapItemID, obj fyne.CanvasObject) { // 渲染
obj.(*widget.Label).SetText("Item " + strconv.Itoa(id))
},
)
// 设置每项的最小尺寸(宽度决定每行放几个)
gridWrap.SetItemMinSize(fyne.NewSize(100, 80))
// 选择事件
gridWrap.OnSelected = func(id widget.GridWrapItemID) {
fmt.Printf("选中了第 %d 项\n", id)
}
说明:
NewGridWrapWithData(data, create, update)--- 数据驱动方式,数据变化自动刷新。SetItemMinSize(size)--- 设置每项最小尺寸,宽度决定列数。- 与 List 区别:GridWrap 根据容器宽度自动换行排列,适合网格/画廊视图。
5.24 卡片 (Card)
widget.Card 用于展示包含标题、副标题、图片和内容的信息卡片:
go
// 来源: fyne demo v2.8.0 --- tutorials/widget.go makeCard()
import "fyne.io/fyne/v2/widget"
card := widget.NewCard(
"标题文字", // title
"副标题文字", // subtitle
widget.NewLabel("这是卡片内容区域,可以放任意控件"),
)
// 设置卡片首部图片(如头像、缩略图)
card.SetImage(canvas.NewImageFromResource(theme.FileIcon()))
说明:
widget.NewCard(title, subtitle, content)--- 创建卡片,标题和副标题可为空字符串。
5.25 活动指示器 (Activity)
go
// 来源: fyne demo v2.8.0 --- tutorials/widget.go
import "fyne.io/fyne/v2/widget"
// 有限活动指示器(显示完成百分比)
progressBar := widget.NewProgressBar()
progressBar.SetValue(0.65) // 65%
// 无限旋转指示器(加载中动画)
spinner := widget.NewActivity()
spinner.Start() // 开始旋转
spinner.Stop() // 停止旋转
5.26 分隔线 (Separator)
go
// 来源: fyne demo v2.8.0
import "fyne.io/fyne/v2/widget"
// 水平分隔线(在 VBox 中)
hv := widget.NewSeparator()
// 垂直分隔线(在 HBox 中)
vv := widget.NewSeparator()
6. 布局管理 (Layout)
6.1 盒子布局 (HBox / VBox)
Fyne 中最常用的布局方式,使用 container.NewVBox() 和 container.NewHBox()。
go
// 来源: fynewh/layoutBox.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func VBoxBoxLayout(w fyne.Window) *fyne.Container {
lblMsg := widget.NewLabel("")
// HBox ------ 水平排列子控件
hbox := container.NewHBox(
widget.NewButton("btn1", func() { lblMsg.SetText("btn11") }),
widget.NewButton("btn2", func() { lblMsg.SetText("btn22") }),
)
// VBox ------ 垂直排列子控件
vboxContainer := container.NewVBox(
widget.NewButton("btn1", func() { lblMsg.SetText("btn11") }),
widget.NewButton("btn2", func() { lblMsg.SetText("btn22") }),
)
return container.NewVBox(lblMsg, hbox, vboxContainer)
}
说明:
NewHBox(children...)--- 水平排列,子控件从左到右依次排列。NewVBox(children...)--- 垂直排列,子控件从上到下依次排列。- 盒子布局会按子控件最小尺寸分配空间,剩余空间填充空白。
6.2 边框布局 (Border)
go
// 来源: fynewh/layoutBorder.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func VBoxBorderLayout(w fyne.Window) *fyne.Container {
// 边框布局:按上、下、左、右、中 五个区域排列
cLayout := container.NewBorder(
widget.NewLabel("Top"), // 上
widget.NewLabel("Bottom"), // 下
widget.NewLabel("Left"), // 左
widget.NewLabel("Right"), // 右
widget.NewLabel("Center"), // 中 ------ 占据剩余全部空间
)
return container.NewVBox(cLayout)
}
说明:
NewBorder(top, bottom, left, right, center)--- 边框布局。- top/bottom 占据顶部/底部,宽度撑满,高度由内容决定。
- left/right 占据左侧/右侧,高度由剩余空间决定,宽度由内容决定。
- center 占据剩余全部空间。
6.3 网格布局 (Grid)
go
// 来源: fynewh/layoutGrid.go
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
)
func VBoxGridLayout(w fyne.Window) *fyne.Container {
// 表格布局 ------ 每行固定列数
text1 := canvas.NewText("11111", color.Black)
text2 := canvas.NewText("22222", color.Black)
text3 := canvas.NewText("33333", color.Black)
image := canvas.NewImageFromFile("images/201.png")
// NewGridLayout(2) ------ 每行 2 列
grid1 := container.New(layout.NewGridLayout(2),
text1, text2, image, text3)
return container.NewVBox(grid1)
}
func VBoxGridWrapLayout(w fyne.Window) *fyne.Container {
text1 := canvas.NewText("11111", color.Black)
text2 := canvas.NewText("22222", color.Black)
text3 := canvas.NewText("33333", color.Black)
// NewGridWrapLayout ------ 自适应换行网格,每个控件固定大小
grid1 := layout.NewGridWrapLayout(fyne.NewSize(300, 120))
cGrid1 := container.New(grid1, text1, text2, text3)
// 也可以直接使用 container.New 同时指定布局
cGrid2 := container.New(
layout.NewGridWrapLayout(fyne.NewSize(100, 40)),
widget.NewButton("btn1", nil),
widget.NewButton("btn2", nil),
)
return container.NewVBox(cGrid1, cGrid2)
}
说明:
NewGridLayout(n)--- 每行固定 n 列的网格布局。NewGridWrapLayout(size)--- 自适应换行网格,每个单元格固定大小,超出宽度自动换行。
6.4 表单布局 (Form)
go
// 来源: fynewh/widgetForm.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func VBoxForm(w fyne.Window) *fyne.Container {
input := widget.NewEntry()
passWord := widget.NewPasswordEntry()
lblMsg := widget.NewLabel("")
// 创建表单 ------ 标签和控件成对排列
form := widget.NewForm(
&widget.FormItem{Text: "Name", Widget: input, HintText: "请输入用户名"},
&widget.FormItem{Text: "Password", Widget: passWord, HintText: "请输入密码"},
)
// 提交回调
form.OnSubmit = func() {
lblMsg.SetText(fmt.Sprintf("Name:%s\nPassword:%s\n",
input.Text, passWord.Text))
}
// 取消回调
form.OnCancel = func() {
w.Close()
}
return container.NewVBox(form, lblMsg)
}
说明:
NewForm(items...)--- 创建表单,自动处理标签和控件的对齐。FormItem---Text(标签文字)、Widget(控件)、HintText(提示文字)。.OnSubmit--- 用户按提交键时的回调(表单中回车或点击 Submit 按钮)。.OnCancel--- 取消回调。
6.5 分割条 (Split)
go
// 来源: fynewh/containerSplit.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func VBoxSplit(w fyne.Window) *fyne.Container {
lblTop := widget.NewLabel("Top")
lblBottom := widget.NewLabel("Bottom")
lblLeft := widget.NewLabel("Left")
lblRight := widget.NewLabel("Right")
// 垂直分割 ------ 上半部分和下半部分
vSplit := container.NewVSplit(lblTop, lblBottom)
// 设置分割条位置比例(0.2 表示上半占 20%)
vSplit.SetOffset(0.2)
// 水平分割 ------ 左部分和右部分
hSplit := container.NewHSplit(lblLeft, lblRight)
// 设置分割条位置比例(0.3 表示左边占 30%)
hSplit.SetOffset(0.3)
return container.NewVBox(vSplit, widget.NewSeparator(), hSplit)
}
说明:
NewVSplit(top, bottom)--- 垂直分割容器,用户可拖拽分割条调整比例。NewHSplit(left, right)--- 水平分割容器。.SetOffset(0~1)--- 设置分割条位置比例。
6.6 居中布局 (Center)
go
// 来源: fynewh/demoMainShow.go(提取)
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func centeredContent() *fyne.Container {
title := widget.NewLabel("标题文字")
// 将控件居中
head := container.NewCenter(title)
// 组合使用
btns := container.NewHBox(
widget.NewButton("确定", nil),
widget.NewButton("取消", nil),
)
// 将按钮组居中
btnCenter := container.NewCenter(btns)
return container.NewVBox(head, btnCenter)
}
说明:
container.NewCenter(obj)--- 将控件在其容器中水平居中显示。
6.7 滚动容器 (Scroll)
go
// 来源: fynewh/containerScroll.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func VBoxScroll(w fyne.Window) *fyne.Container {
// 水平滚动
longContent := widget.NewLabel("这是一段非常非常长的文字内容...")
hscroll := container.NewHScroll(longContent)
// 垂直滚动
manyItems := container.NewVBox(
widget.NewLabel("项1"),
widget.NewLabel("项2"),
// ... 很多项
)
vscroll := container.NewVScroll(manyItems)
// 设置滚动区域的最小尺寸
vscroll.SetMinSize(fyne.Size{Width: 300, Height: 200})
// 双向滚动(水平和垂直)
tableContent := widget.NewLabel("大表格内容...")
biScroll := container.NewScroll(tableContent)
return container.NewVBox(hscroll, vscroll, biScroll)
}
说明:
NewHScroll(content)--- 水平滚动容器。NewVScroll(content)--- 垂直滚动容器,必须设置SetMinSize。NewScroll(content)--- 双向滚动容器。.SetMinSize(size)--- 设置滚动区域的最小可见尺寸。
6.8 实际项目中的布局组合实例
go
// 来源: fynewh/demoMainShow.go ------ 真实业务布局
func MainShow(w fyne.Window) *fyne.Container {
// 标题栏
title := widget.NewLabel("近场自动化程序")
head := container.NewCenter(title) // 居中
// 文件选择行(边框布局实现 label + entry + button)
entry1 := widget.NewEntry()
dia1 := widget.NewButton("打开", func() {
// 文件选择对话框
})
v1 := container.NewBorder(
layout.NewSpacer(), layout.NewSpacer(),
widget.NewLabel("文件夹路径:"), dia1, entry1) // 左标签, 右按钮, 中输入框
// 选项行
combox1 := widget.NewSelect([]string{"最大值切面", "固定倾角切面"}, nil)
v2 := container.NewHBox(widget.NewLabel("切面方式:"), combox1)
// 按钮行
bt3 := widget.NewButton("生成脚本", nil)
bt4 := widget.NewButton("汇总结果", nil)
v5 := container.NewHBox(bt3, bt4)
v5Center := container.NewCenter(v5) // 按钮居中
// 底部输出
text := widget.NewMultiLineEntry()
text.Disable()
// 整体布局
vbox := container.NewVBox(head, v1, v2, v5Center, text)
return vbox
}
说明 :这是一个典型的 Fyne 业务界面布局模式。使用
NewBorder实现标签-输入框-按钮的紧密排列,NewCenter实现按钮居中,NewVBox将各部分垂直堆叠。
6.9 网格列布局 (GridWithColumns)
GridWithColumns (v2.4+) 按固定列数自动排列子元素,适合自适应宽度的网格界面:
go
// 来源: fyne demo v2.8.0 --- tutorials/container.go
import "fyne.io/fyne/v2/container"
// 3 列网格布局(每列等宽,自动换行)
grid := container.NewGridWithColumns(3,
widget.NewLabel("A"), widget.NewLabel("B"), widget.NewLabel("C"),
widget.NewLabel("D"), widget.NewLabel("E"),
)
说明 :
NewGridWithColumns(cols, items...)--- 按指定列数排列,超出自动换行。与NewGridWithRows(rows, items...)对应。
6.10 弹性占位 (Spacer)
Spacer 填充剩余可用空间,常用于实现一行左对齐+右对齐的混合布局:
go
// 来源: fyne demo v2.8.0 --- tutorials/welcome.go
import "fyne.io/fyne/v2/layout"
// 左侧文本 + 弹性占位 + 右侧版本号(实际项目中常见的混合对齐模式)
welcome := container.NewVBox(
widget.NewLabel("Welcome"),
layout.NewSpacer(),
container.NewHBox(
widget.NewLabel("v2.8.0"),
layout.NewSpacer(), // 推到右边
),
)
说明 :
layout.NewSpacer()在盒式布局中占据所有剩余可用空间,常用于实现"左对齐+右对齐"混合排版。
7. 容器 (Container)
7.1 无布局容器
go
// 来源: fynewh/canvasCircle.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
)
// NewWithoutLayout ------ 子控件不自动布局,需手动设置位置和大小
func overlayExample() *fyne.Container {
bg := canvas.NewCircle(color.RGBA{R: 255, A: 255})
bar := canvas.NewRectangle(color.White)
// 无布局容器:子控件叠加排列(先添加的在底层)
c := container.NewWithoutLayout(bg, bar)
// 手动设置每个控件的位置和大小
bg.Resize(fyne.NewSize(100, 100))
bg.Move(fyne.NewPos(10, 10))
bar.Resize(fyne.NewSize(80, 20))
bar.Move(fyne.NewPos(20, 50))
return c
}
说明:
NewWithoutLayout(children...)--- 无自动布局容器,子控件可以自由叠加。- 每个子控件需要手动
Move(pos)和Resize(size)。
7.2 添加分隔线
go
// 来源: fynewh/main.go(多处使用)
import "fyne.io/fyne/v2/widget"
// 在容器中插入分隔线
vbox := container.NewVBox(
widget.NewLabel("第一组"),
widget.NewSeparator(), // 水平分隔线
widget.NewLabel("第二组"),
)
说明 :
widget.NewSeparator()创建一条分割线,在 VBox 中是水平线,在 HBox 中是垂直线。
7.3 堆叠容器 (Stack)
Stack (v2.x+) 将多个子元素层叠放置,后面的元素覆盖在前面的上面。适合背景+内容叠加、badge 角标、水印等场景:
go
// 来源: fyne demo v2.8.0 --- tutorials/welcome.go makeLogo()
import "fyne.io/fyne/v2/container"
import "fyne.io/fyne/v2/canvas"
// 背景图片上叠加前景文字
stack := container.NewStack(
canvas.NewImageFromResource(theme.FyneLogo()), // 底层:背景图
widget.NewLabel("brand"), // 上层:文字标签
)
说明:
NewStack(items...)--- 后声明的元素在上层。- 常见用途:背景图+前景内容、遮罩层、角标显示。
7.4 内边距容器 (Padded)
go
// 来源: fyne demo v2.8.0
// 为标准主题边距创建内边距
padded := container.NewPadded(widget.NewLabel("带内边距的文字"))
7.5 Tab 操作 API
go
// AppTabs 和 DocTabs 支持动态操作
tabs := container.NewAppTabs(
container.NewTabItem("Tab1", widget.NewLabel("Content 1")),
container.NewTabItem("Tab2", widget.NewLabel("Content 2")),
)
tabs.Append(container.NewTabItem("Tab3", widget.NewLabel("Content 3"))) // 添加标签
tabs.RemoveIndex(1) // 移除第 2 个标签
tabs.SelectIndex(0) // 选中第 1 个标签
tabs.DisableIndex(2) // 禁用第 3 个标签(v2.5+)
7.6 滚动条控制
go
// Scroll.Direction 支持禁用滚动
scroll := container.NewScroll(widget.NewLabel("内容"))
scroll.Direction = container.ScrollNone // 禁止所有方向滚动(v2.5+)
scroll.Direction = container.ScrollVerticalOnly
scroll.Direction = container.ScrollHorizontalOnly
scroll.Direction = container.ScrollBoth
// 滚动回调(v2.5+)
scroll.OnScrolled = func(pos fyne.Position) {
fmt.Printf("滚动到 offset: %v\n", pos)
}
8. Canvas 画布绘图
8.1 画布文本
go
// 来源: fynewh/canvasText.go
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"golang.org/x/image/colornames"
)
func VBoxCanvasText(w fyne.Window) *fyne.Container {
// 创建画布文本
text := canvas.NewText("Text Object", color.Black)
// 设置对齐方式
text.Alignment = fyne.TextAlignTrailing
// 设置字体样式(加粗、斜体)
text.TextStyle = fyne.TextStyle{Bold: true, Italic: true}
// 设置字体大小
text.TextSize = 14
text1 := canvas.NewText("Red Object", colornames.Red)
text1.Alignment = fyne.TextAlignCenter
text1.TextSize = 18
// 对齐方式:
// fyne.TextAlignLeading - 左对齐
// fyne.TextAlignCenter - 居中对齐
// fyne.TextAlignTrailing - 右对齐
return container.NewVBox(text, text1)
}
说明:
canvas.NewText(text, color)--- 创建画布文本。.Alignment--- 对齐方式:TextAlignLeading(左)、TextAlignCenter(中)、TextAlignTrailing(右)。.TextStyle--- 字体样式:Bold(加粗)、Italic(斜体)、Monospace(等宽)。.TextSize--- 字体大小(单位:pt)。
8.2 画布矩形
go
// 来源: fynewh/canvasRectangle.go
import (
"image/color"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"golang.org/x/image/colornames"
)
func VBoxCanvasRectangle(w fyne.Window) *fyne.Container {
r1 := canvas.NewRectangle(colornames.Black)
r2 := canvas.NewRectangle(colornames.Blue)
cR1 := container.New(layout.NewGridWrapLayout(fyne.NewSize(100, 100)), r1)
cR2 := container.New(layout.NewGridWrapLayout(fyne.NewSize(100, 100)), r2)
// 颜色动画 ------ 从红色渐变到蓝色
go func() {
for range time.Tick(time.Second) {
red := colornames.Red
blue := colornames.Blue
c1 := canvas.NewColorRGBAAnimation(red, blue, time.Second*1,
func(c color.Color) {
r2.FillColor = c // 更新颜色
canvas.Refresh(cR2) // 刷新画布
})
c1.Start()
// 位置动画 ------ 从 (0,0) 移动到 (200,0)
move := canvas.NewPositionAnimation(
fyne.NewPos(0, 0), fyne.NewPos(200, 0),
time.Second, r1.Move)
move.AutoReverse = true // 自动往返
move.Start()
}
}()
return container.NewVBox(cR1, cR2)
}
说明:
canvas.NewRectangle(color)--- 创建矩形。canvas.NewColorRGBAAnimation(from, to, duration, callback)--- 颜色渐变动画。canvas.NewPositionAnimation(from, to, duration, moveFunc)--- 位置移动动画。canvas.Refresh(obj)--- 刷新画布对象。
8.3 画布圆形
go
// 来源: fynewh/canvasCircle.go
import (
"image/color"
com "fynewh/components"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
)
func VBoxCanvasCircle(w fyne.Window) *fyne.Container {
// 创建红色圆形(无填充只描边)
bg := canvas.NewCircle(color.NRGBA{R: 255, A: 255})
bg.StrokeColor = color.White // 描边颜色
bg.StrokeWidth = 5 // 描边宽度
// 创建白色矩形
bar := canvas.NewRectangle(color.White)
// 无布局容器 ------ 叠加显示(圆形在底层,矩形在上层)
c := container.NewWithoutLayout(bg, bar)
// 手动布局:圆形居中
bg.Resize(fyne.NewSize(100, 100))
bg.Move(fyne.NewPos(10, 10))
// 矩形在圆形中间
bar.Resize(fyne.NewSize(80, 20))
bar.Move(fyne.NewPos(20, 50))
// 圆形+图片叠加
bg1 := canvas.NewCircle(color.NRGBA{B: 255, A: 255})
bg1.StrokeColor = color.White
bg1.StrokeWidth = 5
img1 := canvas.NewImageFromFile("images/108.png")
c1 := container.NewWithoutLayout(bg1, img1)
bg1.Resize(fyne.NewSize(120, 100))
bg1.Move(fyne.NewPos(0, 0))
img1.Resize(fyne.NewSize(80, 80))
img1.Move(fyne.NewPos(10, 10))
return container.NewHBox(c, c1)
}
说明:
canvas.NewCircle(color)--- 创建圆形/椭圆。.StrokeColor--- 描边颜色。.StrokeWidth--- 描边宽度。canvas.NewImageFromFile(path)--- 从文件加载图片。
8.4 画布线条
go
// 来源: fynewh/canvasLine.go
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
)
func VBoxCanvasLine(w fyne.Window) *fyne.Container {
// 创建线条
line1 := canvas.NewLine(color.Black)
line1.StrokeWidth = 2
line1.Resize(fyne.Size{Width: 0, Height: 100}) // 通过 Resize 控制长度
cLine1 := container.New(layout.NewGridWrapLayout(fyne.NewSize(500, 120)), line1)
return container.NewVBox(cLine1)
}
说明:
canvas.NewLine(color)--- 创建画布线条。.StrokeWidth--- 线条宽度。- 线条的延伸方向取决于 ExposeLayout 上下文中该对象的 size:宽度控制水平延伸量,高度控制垂直延伸量。
8.5 画布图片
go
// 来源: fynewh/canvasCircle.go, fynewh/layoutGrid.go
import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/theme"
)
func imageExamples() {
// 从文件加载图片
img1 := canvas.NewImageFromFile("images/201.png")
img1.Resize(fyne.NewSize(80, 80))
// 从资源加载图片(如主题图标)
img2 := canvas.NewImageFromResource(theme.FyneLogo())
img2.Resize(fyne.NewSize(100, 100))
// 设置图片填充模式
img1.FillMode = canvas.ImageFillContain // 按比例填充(保持宽高比)
// canvas.ImageFillStretch - 拉伸填充(不保持宽高比)
// canvas.ImageFillOriginal - 原始大小
// canvas.ImageFillContain - 按比例缩放(默认)
}
说明:
NewImageFromFile(path)--- 从文件路径加载图片。NewImageFromResource(resource)--- 从资源对象加载图片。.FillMode--- 图片填充模式。.Resize(size)--- 设置图片显示尺寸。
8.6 动画 (Animation)
Fyne v2.8.0 提供丰富的动画 API,支持颜色渐变、位置移动、尺寸变化等:
go
// 来源: fyne demo v2.8.0 --- tutorials/animation.go
import "fyne.io/fyne/v2/canvas"
import "fyne.io/fyne/v2"
// 创建目标对象
rect := canvas.NewRectangle(color.RGBA{255, 0, 0, 255})
rect.Resize(fyne.NewSize(100, 100))
// 颜色动画:RGBA 渐变(1 秒)
colorAnim := canvas.NewColorRGBAAnimation(
color.RGBA{0, 255, 0, 255}, // 起始颜色
color.RGBA{0, 0, 255, 255}, // 结束颜色
1000, // 持续时间(毫秒)
func(c color.Color) { // 每帧回调
rect.FillColor = c
},
)
colorAnim.AutoReverse = true // 自动反向播放(形成来回效果)
colorAnim.RepeatCount = fyne.AnimationRepeatForever // 无限循环
colorAnim.Curve = fyne.AnimationEaseInOut // 缓动曲线
// 启动动画
colorAnim.Start()
// 位置/尺寸动画(v2.8.0)
posAnim := canvas.NewPositionAnimation(
fyne.NewPos(0, 0), // 起始位置
fyne.NewPos(200, 100), // 结束位置
500, // 持续时间(毫秒)
func(p fyne.Position) { // 每帧回调
rect.Move(p)
},
)
posAnim.AutoReverse = true
posAnim.Start()
动画速查:
方法/字段 说明 canvas.NewColorRGBAAnimation(s, e, d, fn)颜色动画 canvas.NewPositionAnimation(s, e, d, fn)位置动画(v2.8.0) .AutoReverse自动反向播放 .RepeatCount重复次数( fyne.AnimationRepeatForever无限).Curve缓动曲线: AnimationLinear,AnimationEaseInOut,AnimationEaseOut等.Start()/.Stop()启动/停止动画
9. 数据绑定 (Data Binding)
9.1 字符串绑定
go
// 来源: fynewh/dataBindString.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
)
func HboxDataBind() *fyne.Container {
// 1. 创建字符串数据源
db := binding.NewString()
// 2. 将数据源绑定到控件(自动同步)
lbl := widget.NewLabelWithData(db) // 标签绑定
entry := widget.NewEntryWithData(db) // 输入框绑定
// 3. 设置默认值
db.Set("dataBinding")
// 4. 添加数据变化监听器
lbl1 := widget.NewLabel("")
db.AddListener(binding.NewDataListener(func() {
str, _ := db.Get() // 获取当前值
lbl1.SetText(str)
if str == "red" {
btn.SetText("color red")
}
}))
return container.NewHBox(entry, lbl, lbl1)
}
说明:
binding.NewString()--- 创建字符串数据源。NewLabelWithData(binding)--- 标签绑定数据源,数据变化时自动更新显示。NewEntryWithData(binding)--- 输入框绑定数据源,输入变化时自动更新数据。.Set(value)--- 设置数据值。.Get()--- 获取数据值。.AddListener(listener)--- 添加数据变化监听。
9.2 列表数据绑定
go
// 来源: fynewh/dataBindList.go
import (
"strconv"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
)
func VBoxDataBindList() *fyne.Container {
// 1. 创建字符串列表数据源
datalist := binding.NewStringList()
// 2. 将列表控件绑定到数据源
lst := widget.NewListWithData(datalist,
// 创建模板
func() fyne.CanvasObject {
return widget.NewLabel("")
},
// 绑定数据项
func(di binding.DataItem, co fyne.CanvasObject) {
item := di.(binding.String) // 类型断言
txt, _ := item.Get()
lbl := co.(*widget.Label)
lbl.SetText(txt)
})
// 3. 动态添加数据
i := 0
btn := widget.NewButton("add item", func() {
datalist.Append("red" + strconv.Itoa(i)) // 追加数据
i++
})
return container.NewVBox(btn, lst)
}
说明:
binding.NewStringList()--- 创建字符串列表数据源。NewListWithData(binding, createFn, updateFn)--- 列表控件绑定数据源。updateFn中di.(binding.String)类型断言将通用DataItem转为具体类型。.Append(value)--- 追加数据项。
9.3 结构体数据绑定
go
// 来源: fynewh/dataBindStruct.go
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/widget"
)
// 定义数据结构体
type person struct {
Name string
Age int
}
func VBoxDataBindStruct() *fyne.Container {
p1 := &person{"tom", 11}
// 将结构体包装为数据绑定
bs := binding.BindStruct(p1)
lblMsg := widget.NewLabel("")
btn := widget.NewButton("struct[person]->info", func() {
// 修改结构体字段值
bs.SetValue("Age", 18)
// 获取字段值
name, _ := bs.GetValue("Name")
n := name.(string) // 类型断言
age, _ := bs.GetValue("Age")
a := age.(int)
lblMsg.SetText(fmt.Sprintf("{%s, %d}", n, a))
})
return container.NewVBox(btn, lblMsg)
}
说明:
binding.BindStruct(&struct)--- 将结构体包装为数据绑定,支持双向绑定。.SetValue("field", value)--- 设置结构体字段值。.GetValue("field")--- 获取结构体字段值(返回interface{},需类型断言)。.Keys()--- 获取所有字段名。
9.4 数据转换绑定
go
// 来源: fynewh/dataBindConvert.go(核心用法提取)
import (
"fyne.io/fyne/v2/data/binding"
)
func convertExample() {
// Int 绑定
intBind := binding.NewInt()
intBind.Set(100)
// Int 转 String
strFromInt := binding.IntToString(intBind)
// Float 绑定
floatBind := binding.NewFloat()
floatBind.Set(3.14)
// Float 转 String
strFromFloat := binding.FloatToString(floatBind)
// Bool 绑定
boolBind := binding.NewBool()
boolBind.Set(true)
}
说明:
binding.IntToString(intBind)--- 整数转字符串绑定。binding.FloatToString(floatBind)--- 浮点数转字符串绑定。binding.BoolToString(boolBind)--- 布尔转字符串绑定。
9.5 数据驱动控件
v2.4+ 支持将数据绑定直接注入原控件,数据变化时控件自动更新,无需手动 Reload:
go
// 来源: fyne demo v2.8.0 --- tutorials/bind.go
import "fyne.io/fyne/v2/data/binding"
// 基础类型绑定
strData := binding.NewString()
strData.Set("初始值")
floatData := binding.NewFloat()
floatData.Set(0.75)
boolData := binding.NewBool()
boolData.Set(true)
// 方式1:使用 WithData 系列构造函数
entry := widget.NewEntryWithData(strData) // Entry ↔ String 双向绑定
label := widget.NewLabelWithData(strData) // Label ↔ String 单向绑定
slider := widget.NewSliderWithData(-10, 10) // Slider ↔ Float
slider.Bind(floatData) // 手动绑定
progress := widget.NewProgressBarWithData(floatData) // ProgressBar ↔ Float
check := widget.NewCheckWithData("启用", boolData) // Check ↔ Bool
// 方式2:List 数据绑定
itemsBinding := binding.BindStringList(
&[]string{"Item A", "Item B", "Item C"},
)
list := widget.NewListWithData(
itemsBinding,
func() fyne.CanvasObject { return widget.NewLabel("") },
func(di binding.DataItem, co fyne.CanvasObject) {
co.(*widget.Label).Bind(di.(binding.String))
},
)
// GridWrap 数据绑定(v2.4+)
gridWrap := widget.NewGridWrapWithData(
itemsBinding,
func() fyne.CanvasObject { return widget.NewLabel("") },
func(di binding.DataItem, co fyne.CanvasObject) {
co.(*widget.Label).Bind(di.(binding.String))
},
)
9.6 结构体绑定 (BindStruct)
go
// 来源: fyne demo v2.8.0 --- tutorials/bind.go
import "fyne.io/fyne/v2/data/binding"
type Person struct {
Name string
Age float64
Married bool
Avatar string // 图片路径
}
person := &Person{
Name: "Alice",
Age: 30,
Married: false,
Avatar: "avatar.png",
}
// 将结构体字段自动绑定为可监听的数据项
personBinding := binding.BindStruct(person)
// 读取各个字段的绑定项
nameBind, _ := personBinding.GetItem("Name") // → binding.String
ageBind, _ := personBinding.GetItem("Age") // → binding.Float
marriedBind, _ := personBinding.GetItem("Married") // → binding.Bool
// 直接注入到控件
nameEntry := widget.NewEntryWithData(nameBind.(binding.String))
ageSlider := widget.NewSliderWithData(0, 100)
ageSlider.Bind(ageBind.(binding.Float))
9.7 DataMap 表单模式
go
// 来源: fyne demo v2.8.0 --- tutorials/bind.go
// DataMap + Form 自动生成表单
// DataMap 存储多个 key-value 绑定项
dataMap := binding.NewDataMap()
// 添加绑定项(类型自适应)
dataMap.AddFoo("hello") // 自动推断为 String
dataMap.AddFoo(int(42)) // 自动推断为 Int
dataMap.AddFoo(false) // 自动推断为 Bool
dataMap.AddFoo(float64(3.14))// 自动推断为 Float
// 利用 DataMap 构建 Form 表单
formItems := []*widget.FormItem{}
keys := dataMap.Keys()
for _, key := range keys {
val, _ := dataMap.GetItem(key)
var entry fyne.CanvasObject
switch v := val.(type) {
case binding.String:
entry = widget.NewEntryWithData(v)
case binding.Int:
entry = widget.NewEntryWithData(binding.IntToString(v))
case binding.Bool:
entry = widget.NewCheckWithData("", v)
case binding.Float:
entry = widget.NewEntryWithData(binding.FloatToStringWithFormat(v, "%.2f"))
}
formItems = append(formItems, widget.NewFormItem(key, entry))
}
form := widget.NewForm(formItems...)
说明:
binding.FloatToStringWithFormat(bind, format)--- 浮点数转字符串并指定格式。binding.DataMap适合动态/未知字段数量的表单场景。- 所有
WithData系列构造函数(v2.4+)实现了双向绑定,代码量大幅减少。
10. 对话框 (Dialog)
10.1 文件打开对话框
go
// 来源: fynewh/dialogOpenFile.go, fynewh/demoMainShow.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
)
func fileOpenExample(w fyne.Window, entry1 *widget.Entry) {
btn := widget.NewButton("打开文件", func() {
// 创建文件打开对话框
fd := dialog.NewFileOpen(func(reader fyne.URIReadCloser, err error) {
if err != nil {
dialog.ShowError(err, w) // 显示错误对话框
return
}
if reader == nil {
return // 用户取消
}
entry1.SetText(reader.URI().Path()) // 获取选中文件路径
}, w)
// 设置文件过滤器(只显示 .xlsx 文件)
fd.SetFilter(storage.NewExtensionFileFilter([]string{".xlsx"}))
fd.Show() // 显示对话框
})
}
// 文件夹选择对话框
func folderOpenExample(w fyne.Window, entry *widget.Entry) {
btn := widget.NewButton("打开文件夹", func() {
dialog.ShowFolderOpen(func(list fyne.ListableURI, err error) {
if err != nil {
dialog.ShowError(err, w)
return
}
if list == nil {
return // 用户取消
}
entry.SetText(list.Path()) // 获取选中文件夹路径
}, w)
})
}
说明:
dialog.NewFileOpen(callback, window)--- 创建文件打开对话框。dialog.ShowFolderOpen(callback, window)--- 文件夹选择对话框。.SetFilter(filter)--- 设置文件类型过滤器。dialog.ShowError(err, window)--- 显示错误提示对话框。
10.2 信息对话框
go
// 来源: fynewh/demoMainShow.go(提取)
import (
"fyne.io/fyne/v2/dialog"
)
// 信息对话框
dialog.ShowInformation("提示", "操作成功完成", w)
// 确认对话框
dialog.ShowConfirm("确认", "确定要删除吗?", func(ok bool) {
if ok {
// 用户点击了确认
}
}, w)
// 错误对话框
dialog.ShowError(errors.New("操作失败"), w)
说明:
ShowInformation(title, message, window)--- 信息提示对话框。ShowConfirm(title, message, callback, window)--- 确认对话框,回调参数bool表示用户选择。ShowError(err, window)--- 错误提示对话框。
10.3 表单对话框
go
// 来源: fyne demo v2.8.0 --- tutorials/dialog.go
import "fyne.io/fyne/v2/dialog"
// 使用 widget.FormItem 创建输入表单对话框
nameItem := widget.NewFormItem("姓名", widget.NewEntry())
ageItem := widget.NewFormItem("年龄", widget.NewEntry())
formDialog := dialog.NewForm(
"用户信息", // 标题
"提交", // 确认按钮文字
"取消", // 取消按钮文字
[]*widget.FormItem{nameItem, ageItem}, // 表单项
func(ok bool) { // 回调函数
if ok {
name := nameItem.Widget.(*widget.Entry).Text
age := ageItem.Widget.(*widget.Entry).Text
fmt.Printf("姓名: %s, 年龄: %s\n", name, age)
}
},
w, // 父窗口
)
formDialog.Show()
10.4 自定义对话框
go
// 来源: fyne demo v2.8.0 --- tutorials/dialogs.go
// 对话框内容可以任意自定义控件
icon := widget.NewIcon(theme.ComputerIcon())
customContent := container.NewVBox(
icon,
widget.NewLabel("自定义对话框内容"),
widget.NewProgressBar(),
)
dlg := dialog.NewCustom("自定义标题", "关闭", customContent, w)
dlg.Show()
11. 主题与自定义主题 (Theme)
11.1 自定义主题
go
// 来源: fynewh/theme/themeTtf.go(核心用法)
package theme
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)
// 自定义主题结构体
type MyTheme struct{}
// 实现 Theme 接口 ------ 返回主题名
func (m MyTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
// 可以自定义各部分的颜色
switch name {
case theme.ColorNameBackground:
return color.NRGBA{R: 240, G: 240, B: 240, A: 255}
case theme.ColorNameForeground:
return color.NRGBA{R: 32, G: 32, B: 32, A: 255}
default:
return theme.DefaultTheme().Color(name, variant)
}
}
// 实现 Theme 接口 ------ 返回字体
func (m MyTheme) Font(style fyne.TextStyle) fyne.Resource {
// 返回自定义字体资源
// return theme.DefaultTheme().Font(style)
return nil // 使用系统默认
}
// 实现 Theme 接口 ------ 返回图标
func (m MyTheme) Icon(name fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(name)
}
// 实现 Theme 接口 ------ 返回尺寸
func (m MyTheme) Size(name fyne.ThemeSizeName) float32 {
return theme.DefaultTheme().Size(name)
}
// 使用自定义主题
// 来源: fynewh/main.go
func main() {
a := app.New()
mytheme := &MyTheme{}
a.Settings().SetTheme(mytheme) // 设置自定义主题
}
说明:
- 实现
fyne.Theme接口需提供Color,Font,Icon,Size四个方法。a.Settings().SetTheme(theme)--- 全局设置主题。- 内置主题:
theme.LightTheme()和theme.DarkTheme()。
11.2 颜色使用
go
// 来源: fynewh/*.go 多处
import (
"image/color"
"golang.org/x/image/colornames"
)
// 方式1:使用预定义颜色名
colornames.Red // 红色
colornames.Blue // 蓝色
colornames.Black // 黑色
colornames.White // 白色
colornames.Orange // 橙色
// 方式2:使用 NRGBA(推荐,带 Alpha 通道)
color.NRGBA{R: 255, G: 0, B: 0, A: 255} // 不透明红色
color.NRGBA{R: 64, G: 64, B: 192, A: 128} // 半透明蓝紫
// 方式3:使用 RGBA
color.RGBA{85, 165, 34, 255}
// 方式4:主题颜色
theme.PrimaryColor() // 主题主色
theme.FocusColor() // 焦点色
11.3 强制主题变体 (forcedVariant)
允许用户在应用中手动切换浅色/深色主题,而非跟随系统设置:
go
// 来源: fyne demo v2.8.0 --- main.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)
// 强制浅色主题(无论系统设置如何)
forcedLight := &forcedVariant{Theme: theme.DefaultTheme(), variant: theme.VariantLight}
app.Settings().SetTheme(forcedLight)
// 强制深色主题
forcedDark := &forcedVariant{Theme: theme.DefaultTheme(), variant: theme.VariantDark}
app.Settings().SetTheme(forcedDark)
// forcedVariant 适配器实现
type forcedVariant struct {
fyne.Theme
variant fyne.ThemeVariant
}
func (f *forcedVariant) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
return f.Theme.Color(name, f.variant) // 忽略传入的 variant,使用固定值
}
func (f *forcedVariant) Size(name fyne.ThemeSizeName) float32 {
return f.Theme.Size(name)
}
func (f *forcedVariant) Font(style fyne.TextStyle) fyne.Resource {
return f.Theme.Font(style)
}
func (f *forcedVariant) Icon(name fyne.ThemeIconName) fyne.Resource {
return f.Theme.Icon(name)
}
11.4 设置外观面板
v2.8.0 提供内置的外观设置界面,用户可自行选择主题设置:
go
// 来源: fyne demo v2.8.0 --- main.go
import "fyne.io/fyne/v2/internal/app"
// 简化版(内部 API,可能变化)
settings.NewSettings().LoadAppearanceScreen(w)
说明:
forcedVariant模式通过包裹fyne.Theme接口,覆盖Color()方法中的 variant 参数来实现强制主题。- 完整的自定义主题需实现
Color(name, variant),Size(name),Font(style),Icon(name)四个方法。
12. 自定义控件 (Custom Widget)
12.1 自定义按钮
go
// 来源: fynewh/widgetButton.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/widget"
)
// 自定义按钮结构体 ------ 继承 widget.Button
type MyButton struct {
widget.Button
CCursor desktop.Cursor // 自定义光标
}
// 构造函数
func NewMyButton(label string, tapped func()) *MyButton {
button := &MyButton{}
button.Button.Text = label // 设置按钮文字
button.Button.OnTapped = tapped // 设置点击回调
return button
}
// 实现 Cursor 接口 ------ 返回光标样式
func (b MyButton) Cursor() desktop.Cursor {
if b.CCursor != nil {
return b.CCursor
}
return desktop.CrosshairCursor
}
// 使用自定义按钮
func useCustomButton() {
button1 := NewMyButton("button1", func() {
fmt.Println("自定义按钮事件")
})
// 设置光标
button1.SetCursor(desktop.TextCursor)
}
说明:
- 自定义控件通过嵌入 Fyne 标准控件并重写方法实现。
Cursor() desktop.Cursor--- 自定义鼠标光标样式。ExtendBaseWidget(self)--- 强制绑定渲染器到自定义类型(对于完全自定义的控件必须调用)。
12.2 自定义超链接控件
go
// 来源: OnlineExamApp/components/myHyperlink.go
import (
"image/color"
"net/url"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// 自定义超链接
type Hyperlink struct {
widget.DisableableWidget // 嵌入可禁用能力
Text *canvas.Text
URL *url.URL
Alignment fyne.TextAlign
Wrapping fyne.TextWrap
TextStyle fyne.TextStyle
OnTapped func() // 点击回调
focused, hovered bool
provider *widget.RichText
}
// CreateRenderer ------ 创建自定义渲染器(所有自定义控件必须实现)
func (hl *Hyperlink) CreateRenderer() fyne.WidgetRenderer {
hl.ExtendBaseWidget(hl)
hl.provider = widget.NewRichTextWithText(hl.Text.Text)
// ...
return &hyperlinkRenderer{hl: hl, objects: [...]}
}
// Cursor ------ 鼠标悬停时变为手型
func (hl *Hyperlink) Cursor() desktop.Cursor {
return desktop.PointerCursor
}
// Tapped ------ 点击事件
func (hl *Hyperlink) Tapped(*fyne.PointEvent) {
if hl.OnTapped != nil {
hl.OnTapped()
return
}
if hl.URL != nil {
fyne.CurrentApp().OpenURL(hl.URL) // 在浏览器打开
}
}
// MouseIn / MouseOut ------ 鼠标进入/离开事件
func (hl *Hyperlink) MouseIn(*desktop.MouseEvent) {
hl.hovered = true
hl.BaseWidget.Refresh()
}
func (hl *Hyperlink) MouseOut() {
hl.hovered = false
hl.BaseWidget.Refresh()
}
// TypedKey ------ 键盘事件
func (hl *Hyperlink) TypedKey(ev *fyne.KeyEvent) {
if ev.Name == fyne.KeySpace {
hl.openURL()
}
}
// 自定义渲染器
type hyperlinkRenderer struct {
hl *Hyperlink
focus *canvas.Rectangle
under *canvas.Rectangle
objects []fyne.CanvasObject
}
func (r *hyperlinkRenderer) Layout(s fyne.Size) {
// 子控件布局
r.hl.provider.Resize(s)
r.focus.Move(fyne.NewPos(theme.Padding(), theme.Padding()))
r.focus.Resize(fyne.NewSize(s.Width-theme.Padding()*2, s.Height-theme.Padding()*2))
}
func (r *hyperlinkRenderer) MinSize() fyne.Size {
return r.hl.provider.MinSize()
}
func (r *hyperlinkRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *hyperlinkRenderer) Refresh() {
r.focus.StrokeColor = theme.FocusColor()
r.focus.Hidden = !r.hl.focused
r.under.Hidden = !r.hl.hovered
}
说明:这是 OnlineExamApp 项目中的完整自定义超链接控件,展示了自定义控件的完整模式:
- 继承
widget.DisableableWidget获取禁用状态支持。CreateRenderer()--- 必须实现,返回渲染器。MouseIn/MouseOut/MouseMoved--- 实现desktop.Hoverable接口。Tapped--- 实现fyne.Tappable接口。TypedKey--- 实现fyne.Focusable接口。- 自定义
WidgetRenderer--- 管理子控件的布局、最小尺寸、对象列表和刷新。
13. 数据库集成 (SQLite)
13.1 SQLite 连接管理
go
// 来源: OnlineExamApp/utils/sqlite.go
import (
"database/sql"
_ "github.com/mattn/go-sqlite3" // SQLite 驱动
)
var (
dbDriverName = "sqlite3"
dbName = "./data.db3"
)
var db *sql.DB
// 获取数据库连接 ------ 区分 Windows 和 Android 路径
func GetConnection() (*sql.DB, error) {
var err error
sysType := GetGoos() // 获取操作系统类型
if sysType == "windows" {
dbName = "./data.db3"
}
if sysType == "android" {
// Android 上数据文件在应用私有目录
dbName = "/data/data/com.example.onlineexamapp/files/data.db3"
}
db, err = sql.Open(dbDriverName, dbName)
return db, err
}
// 封装错误处理函数
func CheckErr(e error, lblMsg *canvas.Text) bool {
if e != nil {
lblMsg.Text = e.Error()
lblMsg.Refresh()
return true
}
return false
}
说明:
sql.Open(driver, dsn)--- 打开数据库连接。_ "github.com/mattn/go-sqlite3"--- 匿名导入驱动(只需执行init函数)。- 跨平台数据库路径适配 --- Android 上文件路径需使用应用私有目录。
13.2 数据库 CRUD 操作
go
// 来源: OnlineExamApp/service/service.go(核心用法提取)
import (
"database/sql"
)
// 创建表
func CreateTableExams(db *sql.DB) error {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS exams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
option_a TEXT,
option_b TEXT,
option_c TEXT,
option_d TEXT,
answer TEXT,
user_answer TEXT,
score INTEGER DEFAULT 0
)
`)
return err
}
// 批量插入数据
func InsertDatas(db *sql.DB, exams []model.OnLineExams, lblMsg *canvas.Text) error {
tx, err := db.Begin() // 开启事务
if err != nil { return err }
stmt, err := tx.Prepare(`
INSERT INTO exams (title, option_a, option_b, option_c, option_d, answer)
VALUES (?, ?, ?, ?, ?, ?)`)
if err != nil { return err }
defer stmt.Close()
for _, exam := range exams {
_, err = stmt.Exec(exam.Title, exam.OptionA, exam.OptionB,
exam.OptionC, exam.OptionD, exam.Answer)
if err != nil { return err }
}
return tx.Commit() // 提交事务
}
// 根据 ID 查询
func QueryDataID(db *sql.DB, id string) ([]model.OnLineExams, error) {
rows, err := db.Query("SELECT * FROM exams WHERE id = ?", id)
if err != nil { return nil, err }
defer rows.Close()
var exams []model.OnLineExams
for rows.Next() {
var exam model.OnLineExams
err = rows.Scan(&exam.Id, &exam.Title, &exam.OptionA, ...)
if err != nil { return nil, err }
exams = append(exams, exam)
}
return exams, nil
}
// 更新数据
func UpdateSelectRadio(db *sql.DB, selectRadio string, id int) (bool, error) {
_, err := db.Exec("UPDATE exams SET user_answer = ? WHERE id = ?", selectRadio, id)
if err != nil { return false, err }
return true, nil
}
// 统计行数
func QueryDataMaxRowsCount(db *sql.DB) (int, error) {
var count int
err := db.QueryRow("SELECT COUNT(*) FROM exams").Scan(&count)
if err != nil { return 0, err }
return count, nil
}
说明:
db.Exec(sql, params...)--- 执行非查询 SQL(INSERT/UPDATE/DELETE/CREATE)。db.Query(sql, params...)--- 执行查询 SQL(SELECT),返回*sql.Rows。db.QueryRow(sql, params...)--- 执行单行查询,返回*sql.Row。tx.Begin()+tx.Commit()--- 事务操作,批量插入时显著提升性能。rows.Scan(&vars...)--- 将查询结果扫描到变量中。
14. HTTP 网络请求
14.1 GET 请求
go
// 来源: OnlineExamApp/api/examsApi.go
import (
"encoding/json"
"io/ioutil"
"net/http"
)
// GET 请求获取试卷数据
func GetOnLineExamsData(CardNumber string) ([]OnLineExams, error) {
serverPath := config.GetServerPath()
r, err := http.Get(serverPath + "/api/read_sjmxb?cardNumber=" + CardNumber)
if err != nil {
return nil, err
}
defer r.Body.Close()
body, _ := ioutil.ReadAll(r.Body) // 读取响应体
// JSON 解析:先转为 map 切片
var sliceData []map[string]interface{}
json.Unmarshal(body, &sliceData)
// 再将 map 转为结构体切片
exams := []OnLineExams{}
for _, v := range sliceData {
var exam OnLineExams
utils.Decode(v, &exam) // map -> struct
exams = append(exams, exam)
}
return exams, nil
}
说明:
http.Get(url)--- 发送 GET 请求。ioutil.ReadAll(reader)--- 读取完整响应体。json.Unmarshal(data, &target)--- JSON 反序列化。
14.2 POST 请求
go
// 来源: OnlineExamApp/api/examsApi.go
import (
"bytes"
"encoding/json"
"net/http"
)
// POST 请求上传数据
func PostLocalExamsDataToCloudServer(data []model.UpExams) (map[string]interface{}, error) {
serverPath := config.GetServerPath()
// 创建请求体
requestBody := new(bytes.Buffer)
json.NewEncoder(requestBody).Encode(data) // 编码为 JSON
// 创建 POST 请求
url := serverPath + "/api/appupexams"
req, _ := http.NewRequest("POST", url, requestBody)
req.Header.Set("Content-Type", "application/json") // 设置请求头
// 发送请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
var reqInfo map[string]interface{}
json.Unmarshal(body, &reqInfo)
return reqInfo, nil
}
说明:
json.NewEncoder(buffer).Encode(data)--- 将数据编码为 JSON。http.NewRequest("POST", url, body)--- 创建 POST 请求。req.Header.Set(key, value)--- 设置请求头。client.Do(req)--- 发送请求。
15. 文件与系统操作
15.1 文件遍历
go
// 来源: fynewh/demoImageCollector.go
import (
"io/ioutil"
"os"
"path/filepath"
)
// 递归遍历目录(文件收集器实例)
func (img *ImgCollector) listAllFileByName(level int, pathSeparator string, fileDir string) {
files, _ := ioutil.ReadDir(fileDir) // 读取目录内容
for _, onefile := range files {
if onefile.IsDir() {
// 子目录:递归遍历
img.listAllFileByName(level+1, pathSeparator,
fileDir+pathSeparator+onefile.Name())
} else {
// 文件:检查扩展名并移动
postfix := filepath.Ext(onefile.Name())
for _, v := range img.imgTypes {
if postfix == v {
// 创建目标目录
os.Mkdir(typePath, os.ModePerm)
// 移动文件
os.Rename(fileDir+"/"+onefile.Name(),
typePath+"/"+onefile.Name())
}
}
}
}
}
说明:
ioutil.ReadDir(dir)--- 读取目录下的文件和子目录列表。filepath.Ext(name)--- 获取文件扩展名。os.Mkdir(path, perm)--- 创建目录。os.Rename(old, new)--- 移动/重命名文件。
15.2 获取运行环境信息
go
// 来源: OnlineExamApp/utils/utils.go
import "runtime"
// 获取操作系统类型
func GetGoos() string {
return runtime.GOOS // "windows", "android", "darwin", "linux"
}
// 来源: fynewh/main.go
func checkAndroid() {
if runtime.GOOS == "android" {
// Android 平台特有逻辑
cmd := exec.Command("su", "-c", "pm grant com.example.app android.permission.CAMERA")
cmd.Run()
} else {
fmt.Println("此功能仅适用于Android设备")
}
}
说明:
runtime.GOOS--- 获取运行环境操作系统名称。exec.Command(name, args...)--- 执行系统命令(Android 上需要权限)。
16. 系统托盘
go
// 来源: fynewh/main.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
)
// 添加系统托盘图标
func addSystemTray(a fyne.App, w fyne.Window) {
// 判断是否支持桌面特性(只有 desktop 驱动支持托盘)
if desk, ok := a.(desktop.App); ok {
// 创建托盘菜单项
h := fyne.NewMenuItem("显示主窗口", func() {
w.Show() // 点击显示窗口
})
h.Icon = theme.HomeIcon() // 设置菜单图标
// 创建菜单
m := fyne.NewMenu("MyApp", h)
// 设置系统托盘菜单
desk.SetSystemTrayMenu(m)
}
// 拦截关闭事件 ------ 关闭时隐藏到托盘而非退出
w.SetCloseIntercept(func() {
w.Hide()
})
}
说明:
a.(desktop.App)--- 类型断言判断是否支持桌面特性。desk.SetSystemTrayMenu(menu)--- 设置系统托盘菜单。desk.SetSystemTrayWindow(w)--- (v2.8.0) 点击托盘图标时显示指定窗口,替换传统 menu。- 结合
SetCloseIntercept实现"关闭到托盘"效果。
17. 多窗口管理
go
// 来源: fynewh/demoCreateWindow.go
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 VBoxCreateWindow(a fyne.App, w fyne.Window) *fyne.Container {
btn := widget.NewButton("新建窗口", func() {
// 从 App 实例创建新窗口
w3 := a.NewWindow("新窗口")
lblMsg := widget.NewLabel("新窗口标题")
btnClose := widget.NewButtonWithIcon("关闭", theme.CancelIcon(), func() {
w3.Close() // 关闭当前窗口
})
box := container.NewVBox(lblMsg, btnClose)
w3.SetContent(box)
w3.Resize(fyne.Size{Width: 400, Height: 400})
w3.Show() // 非阻塞显示
})
return container.NewVBox(btn)
}
说明:
- 所有新窗口必须通过同一个 App 实例 创建:
a.NewWindow("标题")。.Show()--- 非阻塞显示窗口(与.ShowAndRun()不同,不阻塞当前协程)。.Close()--- 关闭窗口。
17.2 Shader 着色器动画
v2.8.0 支持通过 GLSL 编写 GPU 着色器,创建高性能粒子动画、渐变背景等效果:
go
// 来源: fyne demo v2.8.0 --- tutorials/shader.go
import "fyne.io/fyne/v2/canvas"
// GLSL 片段着色器源码(运行时编译到 GPU)
var fractSource = `
package main
func Fragment(_ vec4, pos vec2, _ vec2, uv vec2) vec4 {
col := .0
xx := fract(uv.x * 10.0)
yy := fract(uv.y * 10.0)
col = step(.4, xx) * step(.4, yy)
return vec4(col, col, col, 1.0)
}`
// 创建静态着色器
shader := canvas.NewShader(fractSource)
// 或者创建动画着色器(帧刷新)
shaderAnimated := canvas.NewShaderAnimation(fractSource)
shaderAnimated.FillMode = canvas.ImageFillContain // 填充模式:原比例缩放不裁剪
说明:
canvas.NewShader(source)--- 静态着色器渲染。canvas.NewShaderAnimation(source)--- 动画着色器,每帧自动重绘。- 着色器使用自定义 GPU 语言(定义在 Fyne 内部),非标准 GLSL。