Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(上)

0 阅读11分钟

Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(上)

本文档基于 5 个真实 Fyne 项目的实际开发代码编写,所有示例代码均来源于实战项目。

参考项目:

  • OnlineExamApp - 在线考试系统
  • FyneCamera - 摄像头调用
  • FyneWebView - WebView 集成
  • FundQuery - 基金查询系统
  • fynewh - Fyne 全组件教学项目

目录

  1. Fyne 框架介绍
  2. 开发环境搭建
  3. 项目结构组织
  4. 应用程序与窗口 (App & Window)
  5. 基础控件 (Widget)
  6. 布局管理 (Layout)
  7. 容器 (Container)
  8. Canvas 画布绘图
  9. 数据绑定 (Data Binding)
  10. 对话框 (Dialog)
  11. 主题与自定义主题 (Theme)
  12. 自定义控件 (Custom Widget)
  13. 数据库集成 (SQLite)
  14. HTTP 网络请求
  15. 文件与系统操作
  16. 系统托盘
  17. 多窗口管理
  18. 移动端适配 (Android)
  19. 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 / TextAlignTrailing
    • fyne.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 中返回的模板包含多个子控件。