Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(上)
本文档基于 5 个真实 Fyne 项目的实际开发代码编写,所有示例代码均来源于实战项目。
参考项目:
OnlineExamApp- 在线考试系统FyneCamera- 摄像头调用FyneWebView- WebView 集成FundQuery- 基金查询系统fynewh- Fyne 全组件教学项目
目录
- Fyne 框架介绍
- 开发环境搭建
- 项目结构组织
- 应用程序与窗口 (App & Window)
- 基础控件 (Widget)
- 布局管理 (Layout)
- 容器 (Container)
- Canvas 画布绘图
- 数据绑定 (Data Binding)
- 对话框 (Dialog)
- 主题与自定义主题 (Theme)
- 自定义控件 (Custom Widget)
- 数据库集成 (SQLite)
- HTTP 网络请求
- 文件与系统操作
- 系统托盘
- 多窗口管理
- 移动端适配 (Android)
- CGO 与原生平台集成
1. Fyne 框架介绍
Fyne 是 Go 语言的一个跨平台 GUI 工具包,基于 OpenGL 渲染。特点:
- 跨平台:Windows / macOS / Linux / iOS / Android 全支持
- 声明式 API:代码简洁直观
- 内置 Material Design 风格:美观的默认主题
- 数据绑定支持:实现 UI 与数据分离
- 可扩展:支持自定义控件、自定义主题、自定义布局
// Fyne 核心包导入方式
import (
"fyne.io/fyne/v2" // 核心类型定义
"fyne.io/fyne/v2/app" // 应用程序入口
"fyne.io/fyne/v2/container" // 容器和布局
"fyne.io/fyne/v2/widget" // 标准控件
"fyne.io/fyne/v2/canvas" // 画布绘图
"fyne.io/fyne/v2/theme" // 主题图标和颜色
"fyne.io/fyne/v2/layout" // 独立布局
"fyne.io/fyne/v2/data/binding" // 数据绑定
"fyne.io/fyne/v2/dialog" // 对话框
"fyne.io/fyne/v2/driver/desktop" // 桌面特有功能
"fyne.io/fyne/v2/driver/mobile" // 移动端特有功能
"fyne.io/fyne/v2/storage" // 文件存储抽象
)
来源:以上导入方式在 5 个项目中均有使用,是标准的 Fyne 项目导入结构。
2. 开发环境搭建
2.1 安装 Fyne
# 安装 Fyne 核心库
go get fyne.io/fyne/v2
# 安装 Fyne 命令行工具(用于打包)
go install fyne.io/fyne/v2/cmd/fyne@latest
2.2 Windows 开发环境
以下代码在 Windows 10/11 上可直接运行(需安装 GCC):
// 来源: fynewh/main.go - init函数中设置环境变量
package main
import "os"
func init() {
// 设置 Fyne 主题(light 或 dark)
os.Setenv("FYNE_THEME", "light")
// 如需自定义字体(解决中文显示问题),设置:
// os.Setenv("FYNE_FONT", "C:/Windows/Fonts/simkai.ttf")
}
说明:
init()函数在包初始化时自动执行。os.Setenv("FYNE_THEME", "light")设置应用主题为浅色模式。Windows 下如需显示中文,可使用os.Setenv("FYNE_FONT", "字体路径")指定支持中文的字体文件。
3. 项目结构组织
3.1 大项目推荐结构(OnlineExamApp 风格)
project/
├── main.go # 程序入口
├── mainFrame.go # 主界面框架
├── login.go # 登录模块
├── api/ # API 对接层
│ └── api.go
├── components/ # 自定义控件
│ ├── myButtonCircle.go
│ ├── myHyperlink.go
│ └── myNumberEntry.go
├── config/ # 配置文件
│ └── config.go
├── model/ # 数据模型
│ ├── user.go
│ └── ...
├── service/ # 业务逻辑层
│ └── service.go
├── theme/ # 自定义主题
│ └── themeTtf.go
├── utils/ # 工具函数
│ ├── sqlite.go
│ └── utils.go
└── sql/ # SQL 脚本
└── init.sql
3.2 中小项目结构(FundQuery / FyneCamera 风格)
project/
├── main.go # 程序入口 + 主界面
├── components/ # 自定义控件(可选)
├── utils/ # 工具函数(可选)
└── model/ # 数据模型(可选)
说明:OnlineExamApp 采用 MVC 分层架构,适合大型项目。FundQuery 和 FyneCamera 将逻辑集中在一个或少数几个文件中,适合小型项目。
4. 应用程序与窗口 (App & Window)
4.1 创建最简应用
// 来源: fynewh/main.go
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/widget"
)
func main() {
// 创建应用程序实例 —— 每个Fyne程序从这里开始
a := app.New()
// 创建一个带标题的窗口
w := a.NewWindow("Hello World")
// 设置窗口内容为标签控件
w.SetContent(widget.NewLabel("Hello,World!"))
// 显示窗口并进入事件循环(阻塞当前协程)
w.ShowAndRun()
}
说明:
app.New()— 创建 Fyne 应用程序实例,这是所有 Fyne 应用的起点。a.NewWindow("标题")— 创建新窗口,参数为窗口标题。w.SetContent(obj)— 设置窗口的内容区域,接受任何fyne.CanvasObject。w.ShowAndRun()— 显示窗口并启动事件循环,等效于w.Show()+a.Run()。
4.2 应用程序与窗口的高级设置
// 来源: fynewh/main.go
func main() {
// 创建带 ID 的应用 —— ID 用于存储持久化偏好设置
a := app.NewWithID("com.xxx.yyy")
// 设置全局键值对 —— 跨页面共享数据
a.Preferences().SetString("userId", "1")
a.Preferences().SetString("userName", "李梓诚")
a.Preferences().SetString("userPassword", "123456789")
// 自定义主题(详见第11章)
mytheme := &MyTheme{}
a.Settings().SetTheme(mytheme)
// 创建主窗口
w := a.NewWindow("Hello World")
// 设置窗口初始大小 —— 像素为单位
w.Resize(fyne.Size{Width: 700, Height: 600})
// 设置窗口是否固定大小 —— true=固定,false=可调整
// w.SetFixedSize(true)
// 设置窗口全屏
// w.SetFullScreen(true)
// 添加系统托盘
addSystemTray(a, w)
// 为窗口添加键盘事件
w.Canvas().SetOnTypedKey(func(k *fyne.KeyEvent) {
// 监听键盘按键
fmt.Println("退出当前窗口")
})
// 设置窗口内容并运行
w.SetContent(vbox)
w.ShowAndRun()
}
说明:
app.NewWithID— 带应用 ID 创建,ID 用于存储持久化设置(JSON 文件存储,跨启动保持)。a.Preferences()— 全局键值存储 API,支持 String/Int/Bool/Float 等类型。w.Resize(size)— 设置窗口初始尺寸,参数类型fyne.Size{Width, Height}。w.Canvas().SetOnTypedKey()— 注册全局键盘事件回调,接收*fyne.KeyEvent。
4.3 设置与关闭窗口拦截
// 来源: fynewh/main.go
func setMainWindow(w fyne.Window) {
// SetCloseIntercept 拦截窗口关闭事件
// 可以在此实现"最小化到托盘"而不是直接退出
w.SetCloseIntercept(func() {
w.Hide() // 隐藏窗口而非关闭
// w.Close() // 真要关闭时调用
})
}
说明:
SetCloseIntercept拦截用户点击关闭按钮的行为。常用于实现"关闭窗口时最小化到系统托盘"功能。
4.4 读取全局偏好设置
// 来源: fynewh/main.go
// 全局取值,可在任何地方通过 a.Preferences() 读取
func readPreferences(a fyne.App) {
userId := a.Preferences().String("userId")
userName := a.Preferences().String("userName")
fmt.Printf("User: %s, ID: %s\n", userName, userId)
}
说明:
a.Preferences().String(key)读取之前存储的字符串值。偏好数据自动持久化到磁盘(通常在$HOME/.fyne/目录下)。
4.5 应用生命周期 (Lifecycle)
v2.0+ 引入生命周期 API,允许监听应用的前后台切换和启动/停止事件:
// 来源: fyne demo v2.8.0 — main.go
func main() {
a := app.NewWithID("io.fyne.demo")
// 应用启动完成后回调
a.Lifecycle().SetOnStarted(func() {
fyne.LogError("应用已启动", nil)
})
// 应用进入前台时回调(适合恢复暂停的操作)
a.Lifecycle().SetOnEnteredForeground(func() {
fyne.LogError("应用进入前台", nil)
})
// 应用退到后台时回调(适合暂停耗时操作、保存状态)
a.Lifecycle().SetOnExitedForeground(func() {
fyne.LogError("应用退到后台", nil)
})
// 应用即将退出时回调(最后一次清理资源的机会)
a.Lifecycle().SetOnStopped(func() {
fyne.LogError("应用即将退出", nil)
})
w := a.NewWindow("Demo")
w.ShowAndRun()
}
生命周期顺序:
启动 → SetOnStarted → SetOnEnteredForeground 切换后台 → SetOnExitedForeground 切回来 → SetOnEnteredForeground 退出 → SetOnExitedForeground → SetOnStopped
4.6 窗口高级特性
// 来源: fyne v2.8.0 widget/window.go, fyne_demo/advanced.go
// SetMaster — 标记为主窗口(桌面端:关闭此窗口即退出应用)
w.SetMaster()
// SetOnClosed — 窗口关闭后回调(v2.5+)
w.SetOnClosed(func() {
// 所有窗口关闭后执行
fmt.Println("窗口已关闭")
})
// 获取窗口是否为固定大小
if w.FixedSize() {
fmt.Println("窗口大小固定")
}
4.7 全局工具函数
// 来源: fyne demo v2.8.0 — main.go, welcome.go, advanced.go
import "fyne.io/fyne/v2"
// fyne.Do — 安全地在 UI 线程执行回调
// 如果当前已在 UI 线程,直接执行;否则排入 UI 事件队列
fyne.Do(func() {
// 安全地更新 UI 组件
label.SetText("updated from goroutine")
})
// fyne.LogError — 记录错误日志(生产环境静默,debug 模式输出)
fyne.LogError("操作失败", err)
// 剪贴板操作
clipboard := fyne.CurrentApp().Driver().AllWindows()[0].Clipboard()
clipboard.SetContent("复制这段文字") // 设置剪贴板内容
content, _ := clipboard.Content() // 读取剪贴板内容
// 打开浏览器 URL(桌面端)
url, _ := url.Parse("https://fyne.io/")
fyne.CurrentApp().OpenURL(url)
// 获取应用元数据(v2.8.0)
md := fyne.CurrentApp().Metadata()
fmt.Println("App Name:", md.Name)
fmt.Println("App Version:", md.Version)
fmt.Println("App Build:", md.Build)
4.8 桌面快捷键
// 来源: fyne demo v2.8.0 — main.go
import "fyne.io/fyne/v2/driver/desktop"
// 自定义桌面快捷键(跨平台 modifier key)
w.Canvas().AddShortcut(
&desktop.CustomShortcut{
KeyName: fyne.KeyF,
Modifier: fyne.KeyModifierShortcutDefault, // Windows: Ctrl, macOS: Cmd
},
func(shortcut fyne.Shortcut) {
fmt.Println("快捷键被触发")
},
)
// 设备类型检测
if fyne.CurrentDevice().IsMobile() {
// 移动端:调整布局以适配小屏幕
} else if fyne.CurrentDevice().IsBrowser() {
// WebAssembly 环境
} else {
// 桌面端
}
fyne.KeyModifierShortcutDefault(v2.4+) 解决了跨平台快捷键修饰键不一致的问题,Windows 自动映射为 Ctrl,macOS 映射为 Cmd。
5. 基础控件 (Widget)
5.1 标签 (Label)
// 来源: fynewh/main.go
import "fyne.io/fyne/v2/widget"
// 创建简单标签
label := widget.NewLabel("Fyne控件测试")
// 设置自动换行
label.Wrapping = fyne.TextWrapWord
// 绑定数据(详见第9章)
lbl := widget.NewLabelWithData(db) // db 是 binding.String
说明:
NewLabel(text)— 创建静态文本标签。.Wrapping— 设置换行模式:
fyne.TextWrapOff— 不换行(默认)fyne.TextWrapWord— 按单词边界换行fyne.TextWrapBreak— 在任意字符处可换行NewLabelWithData(binding)— 创建数据绑定的标签,文本随数据源自动更新。NewLabelWithStyle(text, alignment, style)— 创建带对齐和样式的标签(v2.4+):
fyne.TextAlignLeading/TextAlignCenter/TextAlignTrailingfyne.TextStyle{Bold: true, Italic: true, Monospace: true}
5.2 按钮 (Button)
// 来源: fynewh/widgetButton.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func HboxButton() *fyne.Container {
hbox := container.NewHBox()
// 1. 创建普通按钮 —— 参数为文字和点击回调
btn1 := widget.NewButton("button click", func() {
fmt.Println("button click")
})
// 设置按钮重要性样式 —— HighImportance 为蓝色高亮
btn1.Importance = widget.HighImportance
// 设置按钮文字对齐方式
btn1.Alignment = widget.ButtonAlignCenter
hbox.Add(btn1)
// 2. 创建带图标的按钮 —— 使用内置主题图标
btn2 := widget.NewButtonWithIcon("btn2", theme.CancelIcon(), func() {
fmt.Println("btn2 CancelIcon")
})
hbox.Add(btn2)
return hbox
}
说明:
NewButton("文字", callback)— 创建按钮,回调是func()类型。NewButtonWithIcon("文字", icon, callback)— 创建带图标的按钮。.Importance— 按钮重要性样式:
widget.LowImportance— 低重要性(灰色)widget.MediumImportance— 中等(默认)widget.HighImportance— 高重要性(蓝色高亮).Alignment— 文字对齐:
widget.ButtonAlignCenter— 居中(默认)widget.ButtonAlignLeading— 左对齐widget.ButtonAlignTrailing— 右对齐
5.3 输入框 (Entry) 和密码框 (PasswordEntry)
// 来源: fynewh/widgetEntry.go, fynewh/widgetForm.go
import (
"fyne.io/fyne/v2/widget"
)
// 普通输入框
input := widget.NewEntry()
// 设置占位提示文字
input.SetPlaceHolder("请输入用户名")
// 密码输入框 —— 输入内容显示为圆点
passWord := widget.NewPasswordEntry()
// 设置输入框宽度
input.Resize(fyne.NewSize(200, 40))
说明:
NewEntry()— 标准单行文本输入框。.SetPlaceHolder("文字")— 设置占位符提示。NewPasswordEntry()— 密码输入框,输入字符显示为●。.Text— 获取当前输入文本。.OnChanged— 文本变化回调函数。.OnSubmitted— 用户按 Enter 键提交时回调(v2.0+)。.Validator— 设置输入验证函数,返回nil表示合法(v2.5+):entry.Validator = func(s string) error { if len(s) < 3 { return fmt.Errorf("至少3个字符") } return nil }
5.4 多行文本输入框 (MultiLineEntry)
// 来源: fynewh/demoMainShow.go
import "fyne.io/fyne/v2/widget"
// 创建多行输入框
text := widget.NewMultiLineEntry()
// 禁用手动输入 —— 只读模式
text.Disable()
// 设置多行文本内容
text.SetText("程序完成:\n" + textInfo)
// 刷新显示
text.Refresh()
说明:
NewMultiLineEntry()— 支持多行输入/显示的文本框,自带滚动条。.Disable()— 禁用编辑,变为只读模式。.SetText(text)— 设置文本内容。.Refresh()— 手动刷新渲染。
5.5 下拉选择框 (Select)
// 来源: fynewh/widgetSelect.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func HBoxSelect() *fyne.Container {
lblMsg := widget.NewLabel("")
// 创建下拉选择框:参数为选项列表和选择回调
sel := widget.NewSelect([]string{"orange", "red", "blue", "yellow", "black"},
func(s string) {
lblMsg.SetText(s) // 选择后将选项文字显示在标签上
})
// 设置默认选中第 0 项(索引从 0 开始)
sel.SetSelectedIndex(0)
return container.NewHBox(sel, widget.NewSeparator(), lblMsg)
}
说明:
NewSelect([]string{...}, callback)— 创建下拉选择框。.SetSelectedIndex(n)— 设置默认选中项。.Selected— 获取当前选中项字符串。
5.6 可编辑下拉框 (SelectEntry)
// 来源: fynewh/widgetSelectEntry.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func HBoxSelectEntry() *fyne.Container {
opts := []string{"orange", "red", "blue", "yellow", "black"}
// 创建可编辑下拉框 —— 既可以选也可以输入
selRoot := widget.NewSelectEntry(opts)
// 按回车后触发
selRoot.OnSubmitted = func(s string) {
if s != "" {
opts = append(opts, s) // 将输入值添加到选项
}
selRoot.SetOptions(opts) // 更新选项列表
}
// 子选择框 —— 根据父选择框的选项动态变化
selChild := widget.NewSelectEntry([]string{})
selRoot.OnChanged = func(s string) {
// 根据父级选项切换子级选项
switch s {
case "red":
selChild.SetOptions([]string{"red1", "red2", "red3"})
case "orange":
selChild.SetOptions([]string{"orange1", "orange2", "orange3"})
// ...
}
}
return container.NewHBox(selRoot, widget.NewSeparator(), selChild)
}
说明:
NewSelectEntry(opts)— 创建可编辑下拉框,用户可以输入自定义值。.OnSubmitted— 用户按回车键时触发。.OnChanged— 选择项变化时触发。.SetOptions(opts)— 动态更新选项列表。
5.7 复选框 (Check)
// 来源: fynewh/widgetCheck.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func HboxCheck() *fyne.Container {
lblMsg := widget.NewLabel("")
// 创建复选框:参数为文字和状态变化回调
cb1 := widget.NewCheck("check1.orange", func(b bool) {
if b {
lblMsg.SetText("check1-selected") // 选中状态
} else {
lblMsg.SetText("check1-unselected") // 未选中状态
}
})
// 设置默认状态为选中
cb1.SetChecked(true)
return container.NewHBox(cb1, widget.NewSeparator(), lblMsg)
}
说明:
NewCheck("文字", func(bool){})— 创建复选框,回调参数的 bool 表示当前选中状态。.SetChecked(true/false)— 设置初始选中状态。
5.8 单选框 (RadioGroup)
// 来源: fynewh/widgetRadio.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func HboxRadio() *fyne.Container {
lblMsg := widget.NewLabel("")
// 创建单选组:参数为选项列表和选择回调
radio := widget.NewRadioGroup([]string{"orange", "red", "blue"}, func(s string) {
lblMsg.SetText(s) // 显示当前选中值
})
// 设置默认选中第 0 项
radio.SetSelected("orange")
return container.NewHBox(radio, widget.NewSeparator(), lblMsg)
}
说明:
NewRadioGroup([]string{...}, func(string){})— 创建单选组,回调参数是选中的值。.SetSelected("value")— 设置默认选中项。.Selected— 获取当前选中值。
5.9 列表 (List)
基本列表(显示字符串数组)
// 来源: fynewh/widgetList.go
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func VBoxList(w fyne.Window) *fyne.Container {
// 定义数据源
listData := []string{"orange", "red", "blue", "yellow", "black"}
// 创建列表:参数为数据长度和两个回调函数
lst := widget.NewList(
// 返回列表行数
func() int {
return len(listData)
},
// 创建每行的模板 —— 返回一个可复用的画布对象
func() fyne.CanvasObject {
return widget.NewLabel("")
},
// 绑定数据到模板 —— 对每一行调用
func(id widget.ListItemID, object fyne.CanvasObject) {
object.(*widget.Label).SetText(listData[id])
},
)
// 选中行回调
lst.OnSelected = func(id widget.ListItemID) {
fmt.Println("Selected:", listData[id])
}
return container.NewVBox(lst)
}
说明:
NewList(lenFn, createFn, updateFn)— 创建列表控件。
lenFn— 返回列表总行数。createFn— 创建每行的模板控件(Fyne 会复用这些控件)。updateFn— 根据行号更新模板内容,id是行索引。.OnSelected— 行被选中时的回调。.OnUnselected— 行取消选中时的回调(v2.4+)。.SetItemHeight(id, height)— 设置指定行的高度(v2.4+)。.Select(id)/.Unselect(id)— 编程方式选中/取消选中行。
图标列表 (ListIcon)
// 来源: fynewh/widgetListIcon.go
import "fyne.io/fyne/v2/theme"
func VBoxListIcon(w fyne.Window) *fyne.Container {
lst := widget.NewList(
func() int { return 5 },
func() fyne.CanvasObject {
// 每行模板:图标 + 标签
return container.NewHBox(widget.NewIcon(theme.HomeIcon()), widget.NewLabel(""))
},
func(id widget.ListItemID, object fyne.CanvasObject) {
// 找到第 2 个控件(标签)
object.(*fyne.Container).Objects[1].(*widget.Label).SetText("Item" + strconv.Itoa(id))
},
)
return container.NewVBox(lst)
}
说明:图标列表与普通列表用法相同,只是
createFn中返回的模板包含多个子控件。