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

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=trueShowHeaderColumn=true,数据区索引从 (0,0) 开始,表头由框架自动管理。

5.10.2 自定义表头内容

如果需要自定义表头显示的文本(默认是 A、B、C... 和 1、2、3...),可覆写 CreateHeaderUpdateHeader

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.0Max 默认为 1.0
widget.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.TextWrapWordfyne.TextWrapOfffyne.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) --- 列表控件绑定数据源。
  • updateFndi.(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。

相关推荐
光头闪亮亮1 小时前
Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(上)
android·sqlite·go
光头闪亮亮1 小时前
Fyne ( go跨平台GUI )项目实战-项目开发必备基础知识(下)
android·c++·go
_wyt0012 小时前
洛谷 P7912 [CSP-J 2021] 小熊的果篮 题解
c++·队列
Ch_champion2 小时前
2018年之前的Android 项目上使用的技术点(及第三方库)
android
龚礼鹏2 小时前
RK Android16 wifi 投屏失败问题排查
android
choumin3 小时前
创建型模式——原型模式
c++·设计模式·原型模式·创建型模式
code_pgf3 小时前
C/C++ 常用容器功能汇总
c语言·开发语言·c++
晓梦林4 小时前
[ACTF2020 新生赛]BackupFile学习笔记
android·笔记·学习
妙码生花4 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(三十六):多驱动上传接口
后端·go·ai编程