Featured image of post Go-Fyne 入门:Go 跨平台 GUI 实战

Go-Fyne 入门:Go 跨平台 GUI 实战

Go 语言跨平台 GUI 框架 Fyne 入门:环境搭建、容器与布局、常用控件、Canvas 绘图、跨平台编译打包、Linux 桌面应用实战。

什么是 Fyne

Fyne(发音 “feign”)是 2018 年起的 Go 跨平台 GUI 框架,目标:用 Go 写原生体验的桌面应用

特点:

  • 跨平台:Windows / macOS / Linux(也支持 FreeBSD、嵌入式)
  • 单二进制:编译后无外部依赖
  • 原生控件:用系统原生 GUI 库(Win32 GDI、Cocoa、GTK)
  • Material Design 风格:类 Flutter 的现代化外观
  • 声明式 API:v2 起更接近 React / Flutter

安装

1
2
3
4
5
6
7
8
# Go 1.20+
go version

# 安装 fyne CLI
go install fyne.io/fyne/v2/cmd/fyne@latest

# 验证
fyne version

系统依赖

Linux (Debian / Ubuntu)

1
2
3
4
5
6
7
8
9
sudo apt install -y \
  libgl1-mesa-dev \
  xorg-dev \
  pkg-config \
  libxrandr-dev \
  libxinerama-dev \
  libxcursor-dev \
  libxi-dev \
  libxxf86vm-dev

macOS

Xcode Command Line Tools:

1
xcode-select --install

Windows

MinGW-w64 + GCC。Scoop 一键装:

1
scoop install gcc

第一个程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

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

func main() {
    a := app.New()
    w := a.NewWindow("Hello Fyne")
    w.SetContent(widget.NewLabel("Hello, 世界!"))
    w.ShowAndRun()
}
1
go run main.go

布局

Fyne 提供 6 种布局:

布局用途
container.NewVBox垂直排列
container.NewHBox水平排列
container.NewBorder四边 + 中心
container.NewGrid等宽网格
container.NewGridWrap自适应网格
container.NewCenter居中
container.NewPadded加内边距
container.NewMax重叠
1
2
3
4
5
6
7
8
9
vbox := container.NewVBox(
    widget.NewLabel("顶部"),
    widget.NewButton("按钮", func() {
        fmt.Println("点击")
    }),
    widget.NewEntry(),
    widget.NewLabel("底部"),
)
w.SetContent(vbox)

常用控件

基础控件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 文本
widget.NewLabel("静态文本")
widget.NewEntry()                  // 单行输入
widget.NewMultiLineEntry()         // 多行输入
widget.NewPasswordEntry()          // 密码
widget.NewRichText()               // 富文本

// 按钮
widget.NewButton("普通按钮", callback)
widget.NewButtonWithIcon("图标按钮", theme.DocumentIcon(), callback)
widget.NewHyperlink("链接", url)

// 选择
widget.NewCheck("复选框", onChange)
widget.NewRadio([]string{"男", "女"}, onChange)
widget.NewSelect([]string{"Java", "Go", "Rust"}, onChange)
widget.NewSlider(0, 100)            // 滑块
widget.NewProgressBar()             // 进度条

// 容器
widget.NewCard("标题", "副标题", content)
widget.NewAccordion()              // 手风琴
widget.NewTabContainer()           // 标签页
widget.NewSplit()                  // 分割

表单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
form := &widget.Form{
    Items: []*widget.FormItem{
        {Text: "用户名", Widget: widget.NewEntry()},
        {Text: "密码", Widget: widget.NewPasswordEntry()},
    },
    OnSubmit: func() {
        fmt.Println("提交")
    },
    OnCancel: func() {
        fmt.Println("取消")
    },
}

列表

1
2
3
4
5
6
7
list := widget.NewList(
    func() int { return len(items) },
    func() fyne.CanvasObject { return widget.NewLabel("") },
    func(i int, o fyne.CanvasObject) {
        o.(*widget.Label).SetText(items[i])
    },
)

表格

1
2
3
4
5
6
7
8
9
table := widget.NewTable(
    func() (int, int) { return len(rows), 2 },
    func() fyne.CanvasObject {
        return widget.NewLabel("wide content")
    },
    func(i int, j int, o fyne.CanvasObject) {
        o.(*widget.Label).SetText(fmt.Sprintf("%d-%d", i, j))
    },
)

Canvas 绘图

Fyne Canvas 2D API 适合自定义图形:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import (
    "fyne.io/fyne/v2/canvas"
    "image/color"
)

// 矩形
rect := canvas.NewRectangle(color.RGBA{255, 0, 0, 255})
rect.Resize(fyne.NewSize(100, 100))

// 圆形
circle := canvas.NewCircle(color.RGBA{0, 255, 0, 255})

// 文本
text := canvas.NewText("Canvas Text", color.Black)
text.TextSize = 24

// 线条
line := canvas.NewLine(color.Black)
line.StrokeWidth = 2

// 图片
img := canvas.NewImageFromResource(resource.ExamplePng())
img.FillMode = canvas.ImageFillOriginal

// 组合
canvasContainer := container.NewWithoutLayout(rect, circle, text, line)

菜单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
menu := fyne.NewMainMenu(
    fyne.NewMenu("文件",
        fyne.NewMenuItem("新建", func() { /* ... */ }),
        fyne.NewMenuItem("打开", func() { /* ... */ }),
        fyne.NewMenuItemSeparator(),
        fyne.NewMenuItem("退出", func() { a.Quit() }),
    ),
    fyne.NewMenu("帮助",
        fyne.NewMenuItem("关于", func() { /* ... */ }),
    ),
)
w.SetMainMenu(menu)

主题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import "fyne.io/fyne/v2/theme"

// 亮色 / 暗色
a.Settings().SetTheme(theme.LightTheme())
a.Settings().SetTheme(theme.DarkTheme())

// 自定义
type MyTheme struct{}
func (m MyTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
    return color.RGBA{0, 100, 200, 255}
}
a.Settings().SetTheme(MyTheme{})

数据绑定

Fyne v2.1+ 支持响应式数据绑定:

1
2
3
4
5
6
7
8
9
str := binding.NewString()
str.Set("Hello")

entry := widget.NewEntryWithData(str)

// 监听变化
str.AddListener(binding.NewDataListener(func() {
    fmt.Println("值变了:", str.Get())
}))

绑定类型:

  • binding.NewString() / NewInt() / NewFloat() / NewBool()
  • binding.NewList() / NewMap()

跨平台编译

1
2
3
4
5
6
7
8
# 在 macOS 上编译 Linux 版本
GOOS=linux GOARCH=amd64 go build -o myapp-linux main.go

# 编译 Windows
GOOS=windows GOARCH=amd64 go build -o myapp.exe main.go

# 编译 macOS(arm64 M1)
GOOS=darwin GOARCH=arm64 go build -o myapp-mac main.go

打包分发

Fyne 自带 fyne CLI 打包:

1
2
3
4
5
6
7
# 打包当前平台
fyne package -name MyApp -icon icon.png

# 跨平台打包(需要对应工具链)
fyne package -os darwin -name MyApp
fyne package -os linux -name MyApp
fyne package -os windows -name MyApp

打包结果:

  • Linux:MyApp.tar.xz(含 .desktop 文件)
  • macOS:MyApp.app
  • Windows:MyApp.exe(可加 installer)

实战:计算器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

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

func main() {
    a := app.New()
    w := a.NewWindow("计算器")
    w.Resize(fyne.NewSize(300, 400))

    output := widget.NewLabel("0")
    output.TextStyle = fyne.TextStyle{Bold: true}
    output.Alignment = fyne.TextAlignTrailing

    btn := func(label string, fn func()) *widget.Button {
        return widget.NewButton(label, fn)
    }

    digits := container.NewGridWithColumns(3,
        btn("7", func() { output.SetText(output.Text + "7") }),
        btn("8", func() { output.SetText(output.Text + "8") }),
        btn("9", func() { output.SetText(output.Text + "9") }),
        // ...
    )

    w.SetContent(container.NewVBox(output, digits))
    w.ShowAndRun()
}

实战:Todo List

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
type Todo struct {
    Title string
    Done  bool
}

var todos []Todo

func main() {
    a := app.New()
    w := a.NewWindow("Todo")
    w.Resize(fyne.NewSize(400, 600))

    input := widget.NewEntry()
    input.SetPlaceHolder("输入新任务")

    list := widget.NewList(
        func() int { return len(todos) },
        func() fyne.CanvasObject {
            return container.NewHBox(
                widget.NewCheck("", nil),
                widget.NewLabel(""),
            )
        },
        func(i int, o fyne.CanvasObject) {
            c := o.(*fyne.Container)
            c.Objects[0].(*widget.Check).SetChecked(todos[i].Done)
            c.Objects[1].(*widget.Label).SetText(todos[i].Title)
        },
    )

    addBtn := widget.NewButton("添加", func() {
        if input.Text != "" {
            todos = append(todos, Todo{Title: input.Text})
            input.SetText("")
            list.Refresh()
        }
    })

    w.SetContent(container.NewVBox(
        container.NewBorder(nil, nil, nil, addBtn, input),
        list,
    ))
    w.ShowAndRun()
}

下一步

  • 跨平台桌面技术栈对比,看 2014-08-15《桌面开发技术栈对比》
  • Tauri 2.x 实战,看 2020-05-15《Tauri 2.x 跨平台桌面应用》
  • Electron 入门,看 2018-12-15《Electron 跨平台桌面应用》

参考资料

  • Fyne 官方:https://fyne.io/
  • Fyne Tour:https://tour.fyne.io/
  • Fyne GitHub:https://github.com/fyne-io/fyne
  • Fyne 示例:https://github.com/fyne-io/fyne/tree/master/v2/cmd/fyne_demo
使用 Hugo 构建
主题 StackJimmy 设计