You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows下GoLang Fyne窗口关闭与系统托盘应用兼容问题

解决Fyne+systray托盘应用关闭窗口崩溃问题

问题根源

崩溃的核心原因是调用mainApp.Quit()会终止Fyne的GLFW驱动事件循环,内部通道被永久关闭,后续再次调用mainApp.Run()时就会触发panic: close of closed channel。而循环重建App会导致GLFW初始化错误,因为GLFW要求在主线程完成初始化,且不支持重复初始化。

解决方案

  • 放弃调用mainApp.Quit(),改为隐藏窗口(window.Hide()),保持Fyne事件循环持续运行
  • 重写窗口的关闭拦截器(SetCloseIntercept),让点击红色×时执行隐藏操作而非退出App
  • 调整主线程逻辑:Fyne的Run()会阻塞主线程运行事件循环,托盘逻辑通过goroutine处理即可,无需循环调用Run()

修改后的完整代码

package main

import (
    icon "company/winres"
    "fyne.io/fyne/v2"
    "fyne.io/fyne/v2/app"
    "fyne.io/fyne/v2/container"
    "fyne.io/fyne/v2/layout"
    "fyne.io/fyne/v2/widget"
    "github.com/getlantern/systray"
    "github.com/k0kubun/pp/v3"
)

// 用于通知显示窗口的通道(空结构体仅做信号传递)
var showWindowCh = make(chan struct{})

var mainApp fyne.App
var mainWindow fyne.Window

func main() {
    // 初始化pp日志配色
    scheme := pp.ColorScheme{
        Integer:         pp.Yellow | pp.Bold,
        String:          pp.Green | pp.BackgroundBlack | pp.Bold,
        StringQuotation: pp.Green | pp.BackgroundBlack | pp.Bold,
    }
    pp.Default.SetColorScheme(scheme)
    pp.Println("Starting Application now ...")

    // 启动系统托盘(放入goroutine避免阻塞主线程)
    go func() {
        systray.Register(onReady, onExit)
    }()

    // 初始化Fyne应用和窗口
    pp.Println("Creating app and window")
    mainApp = app.New()
    mainWindow = mainApp.NewWindow("Backup Information")
    mainWindow.Resize(fyne.NewSize(256, 256))

    // 拦截窗口关闭事件:点击×时隐藏窗口而非退出App
    mainWindow.SetCloseIntercept(func() {
        mainWindow.Hide()
    })

    // 构建窗口内容
    yesBtn := widget.NewButton("Yes", func() {
        mainWindow.Hide()
    })
    yesBtn.Importance = widget.HighImportance
    mainWindow.SetContent(container.NewVBox(
        widget.NewLabel("Are you sure you want to interact with this test dialog?"),
        container.NewHBox(
            layout.NewSpacer(),
            widget.NewButton("No", func() {
                mainWindow.Hide()
            }),
            yesBtn,
            layout.NewSpacer(),
        ),
    ))

    // 监听显示窗口的信号
    go func() {
        for range showWindowCh {
            mainWindow.Show()
        }
    }()

    // 运行Fyne事件循环(阻塞主线程,保持应用持续运行)
    mainApp.Run()
}

func onReady() {
    // 设置托盘图标和标题
    systray.SetIcon(icon.Data)
    systray.SetTemplateIcon(icon.Data, icon.Data)
    systray.SetTitle("asdf")

    // 创建托盘菜单
    mInformation := systray.AddMenuItem("Info", "Information")
    mBackupNow := systray.AddMenuItem("Now", "Immediately start")
    mQuit := systray.AddMenuItem("Quit", "Quit")

    // 处理菜单点击事件
    go func() {
        for {
            select {
            case <-mInformation.ClickedCh:
                pp.Println("User chose to view information")
                showWindowCh <- struct{}{}
            case <-mBackupNow.ClickedCh:
                pp.Println("User chose to start backup")
            case <-mQuit.ClickedCh:
                // 退出时同时关闭Fyne应用和托盘
                mainApp.Quit()
                systray.Quit()
            }
        }
    }()
}

func onExit() {
    pp.Println("Exiting now ...")
}

关键修改点

  • 将原字符串通道改为空结构体通道showWindowCh,更适合仅做信号传递的场景
  • 给窗口添加SetCloseIntercept,覆盖默认的关闭行为为隐藏窗口
  • 所有按钮的关闭回调改为mainWindow.Hide(),不再触发App退出
  • 将systray.Register放入goroutine运行,避免阻塞Fyne的主线程初始化
  • 移除main函数末尾的循环mainApp.Run(),因为mainApp.Run()本身会阻塞主线程维持事件循环

内容的提问来源于stack exchange,提问作者philbot999

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 17:48:26