Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(上)
本文档基于 5 个真实 Fyne 项目的实际开发代码编写,所有示例代码均来源于实战项目。
参考项目:
OnlineExamApp- 在线考试系统FyneCamera- 摄像头调用FyneWebView- WebView 集成FundQuery- 基金查询系统fynewh- Fyne 全组件教学项目
目录
- [Fyne 框架介绍](#Fyne 框架介绍 "#1-fyne-%E6%A1%86%E6%9E%B6%E4%BB%8B%E7%BB%8D")
- 开发环境搭建
- 项目结构组织
- [应用程序与窗口 (App & Window)](#应用程序与窗口 (App & Window) "#4-%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E4%B8%8E%E7%AA%97%E5%8F%A3-app--window")
- [基础控件 (Widget)](#基础控件 (Widget) "#5-%E5%9F%BA%E7%A1%80%E6%8E%A7%E4%BB%B6-widget")
- [布局管理 (Layout)](#布局管理 (Layout) "#6-%E5%B8%83%E5%B1%80%E7%AE%A1%E7%90%86-layout")
- [容器 (Container)](#容器 (Container) "#7-%E5%AE%B9%E5%99%A8-container")
- [Canvas 画布绘图](#Canvas 画布绘图 "#8-canvas-%E7%94%BB%E5%B8%83%E7%BB%98%E5%9B%BE")
- [数据绑定 (Data Binding)](#数据绑定 (Data Binding) "#9-%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A-data-binding")
- [对话框 (Dialog)](#对话框 (Dialog) "#10-%E5%AF%B9%E8%AF%9D%E6%A1%86-dialog")
- [主题与自定义主题 (Theme)](#主题与自定义主题 (Theme) "#11-%E4%B8%BB%E9%A2%98%E4%B8%8E%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%BB%E9%A2%98-theme")
- [自定义控件 (Custom Widget)](#自定义控件 (Custom Widget) "#12-%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8E%A7%E4%BB%B6-custom-widget")
- [数据库集成 (SQLite)](#数据库集成 (SQLite) "#13-%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E6%88%90-sqlite")
- [HTTP 网络请求](#HTTP 网络请求 "#14-http-%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82")
- 文件与系统操作
- 系统托盘
- 多窗口管理
- [移动端适配 (Android)](#移动端适配 (Android) "#18-%E7%A7%BB%E5%8A%A8%E7%AB%AF%E9%80%82%E9%85%8D-android")
- [CGO 与原生平台集成](#CGO 与原生平台集成 "#19-cgo-%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%B9%B3%E5%8F%B0%E9%9B%86%E6%88%90")
1. Fyne 框架介绍
Fyne 是 Go 语言的一个跨平台 GUI 工具包,基于 OpenGL 渲染。特点:
- 跨平台:Windows / macOS / Linux / iOS / Android 全支持
- 声明式 API:代码简洁直观
- 内置 Material Design 风格:美观的默认主题
- 数据绑定支持:实现 UI 与数据分离
- 可扩展:支持自定义控件、自定义主题、自定义布局
go
// 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
bash
# 安装 Fyne 核心库
go get fyne.io/fyne/v2
# 安装 Fyne 命令行工具(用于打包)
go install fyne.io/fyne/v2/cmd/fyne@latest
2.2 Windows 开发环境
以下代码在 Windows 10/11 上可直接运行(需安装 GCC):
go
// 来源: 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 风格)
csharp
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 风格)
bash
project/
├── main.go # 程序入口 + 主界面
├── components/ # 自定义控件(可选)
├── utils/ # 工具函数(可选)
└── model/ # 数据模型(可选)
说明:OnlineExamApp 采用 MVC 分层架构,适合大型项目。FundQuery 和 FyneCamera 将逻辑集中在一个或少数几个文件中,适合小型项目。
4. 应用程序与窗口 (App & Window)
4.1 创建最简应用
go
// 来源: 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 应用程序与窗口的高级设置
go
// 来源: 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 设置与关闭窗口拦截
go
// 来源: fynewh/main.go
func setMainWindow(w fyne.Window) {
// SetCloseIntercept 拦截窗口关闭事件
// 可以在此实现"最小化到托盘"而不是直接退出
w.SetCloseIntercept(func() {
w.Hide() // 隐藏窗口而非关闭
// w.Close() // 真要关闭时调用
})
}
说明 :
SetCloseIntercept拦截用户点击关闭按钮的行为。常用于实现"关闭窗口时最小化到系统托盘"功能。
4.4 读取全局偏好设置
go
// 来源: 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,允许监听应用的前后台切换和启动/停止事件:
go
// 来源: 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 窗口高级特性
go
// 来源: 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 全局工具函数
go
// 来源: 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 桌面快捷键
go
// 来源: 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)
go
// 来源: 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)
go
// 来源: 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)
go
// 来源: 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+):
goentry.Validator = func(s string) error { if len(s) < 3 { return fmt.Errorf("至少3个字符") } return nil }
5.4 多行文本输入框 (MultiLineEntry)
go
// 来源: 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)
go
// 来源: 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)
go
// 来源: 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)
go
// 来源: 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)
go
// 来源: 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)
基本列表(显示字符串数组)
go
// 来源: 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)
go
// 来源: 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中返回的模板包含多个子控件。