-
- Wails + React + Go
- Windows 桌面应用框架示例
-
+
-
-
+
+
+ 青梧映星
+ Windows 桌面应用框架示例
+
-
-
-
用户列表
-
-
- {users.length === 0 ? (
- 暂无数据
- ) : (
-
-
-
- | ID |
- 名称 |
- 邮箱 |
- 创建时间 |
-
-
-
- {users.map((u) => (
-
- | {u.id} |
- {u.name} |
- {u.email} |
- {u.created_at} |
+
+
+
+
+
+
用户列表
+
+
+ {users.length === 0 ? (
+ 暂无数据
+ ) : (
+
+
+
+ | ID |
+ 名称 |
+ 邮箱 |
+ 创建时间 |
- ))}
-
-
- )}
-
-
+
+
+ {users.map((u) => (
+
+ | {u.id} |
+ {u.name} |
+ {u.email} |
+ {u.created_at} |
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+ {toast && {toast}
}
);
}
diff --git a/frontend/src/assets/fonts/fa/fa-solid-900.woff2 b/frontend/src/assets/fonts/fa/fa-solid-900.woff2
new file mode 100644
index 0000000..1672102
Binary files /dev/null and b/frontend/src/assets/fonts/fa/fa-solid-900.woff2 differ
diff --git a/go.mod b/go.mod
index 684a5ba..7f0625e 100644
--- a/go.mod
+++ b/go.mod
@@ -3,14 +3,22 @@ module go_win_template
go 1.25.0
require (
+ github.com/getlantern/systray v1.2.2
github.com/wailsapp/wails/v2 v2.16.0
- golang.org/x/sys v0.46.0
+ golang.org/x/image v0.41.0
)
require (
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
+ github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
+ github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
+ github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
+ github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
+ github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
+ github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
+ github.com/go-stack/stack v1.8.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
@@ -23,6 +31,7 @@ require (
github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
@@ -34,6 +43,7 @@ require (
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
+ golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.39.0 // indirect
)
diff --git a/go.sum b/go.sum
index 8235dbe..50b99f5 100644
--- a/go.sum
+++ b/go.sum
@@ -2,10 +2,27 @@ git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
+github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
+github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
+github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
+github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
+github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
+github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
+github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
+github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
+github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
+github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
+github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
+github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
+github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
+github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -28,6 +45,8 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
+github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
+github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
@@ -36,6 +55,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
+github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -47,6 +68,9 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
+github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
@@ -63,10 +87,13 @@ github.com/wailsapp/wails/v2 v2.16.0 h1:hLYrSHUUq6hRXl3rWT9ZXiyFIgM94nVN9Ns6iEJz
github.com/wailsapp/wails/v2 v2.16.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
+golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
+golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -79,5 +106,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/go_win_template.exe b/go_win_template.exe
index 40598fb..e7c45bc 100644
Binary files a/go_win_template.exe and b/go_win_template.exe differ
diff --git a/internal/tray/tray.go b/internal/tray/tray.go
new file mode 100644
index 0000000..56eff77
--- /dev/null
+++ b/internal/tray/tray.go
@@ -0,0 +1,125 @@
+// Package tray 系统托盘管理器,基于 getlantern/systray 跨平台实现。
+// Windows 下图标需为 .ico 数据,macOS/Linux 下可为 .png/.jpg/.ico。
+package tray
+
+import (
+ "context"
+ "sync"
+
+ "github.com/getlantern/systray"
+ "github.com/wailsapp/wails/v2/pkg/runtime"
+)
+
+// Options 托盘初始化参数
+type Options struct {
+ // Icon 托盘图标数据
+ Icon []byte
+ // Tooltip 悬停提示文本
+ Tooltip string
+
+ // OnShow 点击"显示窗口"时回调,为空时默认调用 runtime.WindowShow
+ OnShow func()
+ // OnHide 点击"隐藏窗口"时回调,为空时默认调用 runtime.WindowHide
+ OnHide func()
+ // OnQuit 点击"退出"时回调,为空时默认调用 runtime.Quit
+ OnQuit func()
+}
+
+// Manager 系统托盘管理器
+type Manager struct {
+ mu sync.Mutex
+ opts Options
+ started bool
+ ready chan struct{}
+ quitItem *systray.MenuItem
+}
+
+// New 创建托盘管理器,Run 之前不会显示图标
+func New(opts Options) *Manager {
+ return &Manager{
+ opts: opts,
+ ready: make(chan struct{}),
+ }
+}
+
+// Run 在后台 goroutine 中启动托盘事件循环(非阻塞)。
+// ctx 用于窗口显示/隐藏/退出。
+func (m *Manager) Run(ctx context.Context) {
+ m.mu.Lock()
+ if m.started {
+ m.mu.Unlock()
+ return
+ }
+ m.started = true
+ m.mu.Unlock()
+
+ go func() {
+ systray.Run(func() {
+ m.onReady(ctx)
+ close(m.ready)
+ }, func() {
+ m.mu.Lock()
+ m.started = false
+ m.mu.Unlock()
+ })
+ }()
+}
+
+// WaitReady 阻塞直到托盘初始化完成(可选)
+func (m *Manager) WaitReady() {
+ <-m.ready
+}
+
+// Active 托盘是否已成功启动
+func (m *Manager) Active() bool {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ return m.started
+}
+
+// Quit 移除托盘图标并停止托盘 goroutine
+func (m *Manager) Quit() {
+ systray.Quit()
+}
+
+func (m *Manager) onReady(ctx context.Context) {
+ if len(m.opts.Icon) > 0 {
+ systray.SetIcon(m.opts.Icon)
+ }
+ if m.opts.Tooltip != "" {
+ systray.SetTooltip(m.opts.Tooltip)
+ }
+
+ show := systray.AddMenuItem("显示窗口", "Show the main window")
+ hide := systray.AddMenuItem("隐藏窗口", "Hide the main window")
+ systray.AddSeparator()
+ quitItem := systray.AddMenuItem("退出", "Quit the application")
+ m.quitItem = quitItem
+
+ m.handleMenuClicks(ctx, show, hide, quitItem)
+}
+
+func (m *Manager) handleMenuClicks(ctx context.Context, show, hide, quit *systray.MenuItem) {
+ for {
+ select {
+ case <-show.ClickedCh:
+ if m.opts.OnShow != nil {
+ m.opts.OnShow()
+ } else {
+ runtime.WindowShow(ctx)
+ }
+ case <-hide.ClickedCh:
+ if m.opts.OnHide != nil {
+ m.opts.OnHide()
+ } else {
+ runtime.WindowHide(ctx)
+ }
+ case <-quit.ClickedCh:
+ if m.opts.OnQuit != nil {
+ m.opts.OnQuit()
+ } else {
+ runtime.Quit(ctx)
+ }
+ }
+ }
+}
diff --git a/internal/tray/win/tray.go b/internal/tray/win/tray.go
deleted file mode 100644
index 50d5f28..0000000
--- a/internal/tray/win/tray.go
+++ /dev/null
@@ -1,167 +0,0 @@
-// Package tray Windows 系统托盘实现(使用 win32 API)
-package tray
-
-import (
- "log"
- "sync"
- "unsafe"
-
- "golang.org/x/sys/windows"
-)
-
-// Hook 托盘图标句柄
-var hook windows.Handle
-
-// MenuHandle 菜单句柄
-type MenuHandle uintptr
-
-// Tray 系统托盘管理器
-type Tray struct {
- mu sync.Mutex
- visible bool
- callback uintptr // WNDPROC 回调
- hWnd windows.Handle
-}
-
-// MenuItem 菜单项
-type MenuItem struct {
- ID uint32
- Text string
-}
-
-// NewTray 创建系统托盘
-func NewTray(iconData []byte, tooltip string) *Tray {
- t := &Tray{}
- if err := t.init(iconData, tooltip); err != nil {
- log.Printf("[tray] init failed: %v", err)
- return nil
- }
- return t
-}
-
-func (t *Tray) init(iconData []byte, tooltip string) error {
- // 获取当前进程窗口
- hWnd, _ := windows.GetForegroundWindow()
- if hWnd == 0 {
- return windows.ERROR_ACCESS_DENIED
- }
- t.hWnd = hWnd
-
- // 注册托盘图标
- // NOTIFYICONDATA structure
- // https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa
- data := ¬ifyIconData{
- CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
- HWnd: uint32(hWnd),
- UID: 1000,
- UFlags: NIF_MESSAGE | NIF_ICON | NIF_TIP,
- UCallbackMessage: WM_USER + 1000,
- HIcon: 0, // 使用默认图标
- }
-
- // 设置 tooltip
- for i := 0; i < len(tooltip) && i < 128; i++ {
- data.Tip[i] = uint8(tooltip[i])
- }
- data.Tip[len(tooltip)] = 0
-
- // 添加托盘图标
- ret, _, _ := procShellNotifyIcon.Call(
- SHM_ADD,
- uintptr(unsafe.Pointer(data)),
- )
- if ret == 0 {
- return windows.GetLastError()
- }
-
- t.visible = true
- return nil
-}
-
-// Destroy 销毁托盘图标
-func (t *Tray) Destroy() {
- t.mu.Lock()
- defer t.mu.Unlock()
- if !t.visible {
- return
- }
- data := ¬ifyIconData{
- CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
- HWnd: uint32(t.hWnd),
- UID: 1000,
- UFlags: NIF_MESSAGE,
- }
- procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data)))
- t.visible = false
-}
-
-// Show 显示托盘
-func (t *Tray) Show(iconData []byte, tooltip string) error {
- t.mu.Lock()
- defer t.mu.Unlock()
- if t.visible {
- return nil
- }
- return t.init(iconData, tooltip)
-}
-
-// Hide 隐藏托盘
-func (t *Tray) Hide() {
- t.mu.Lock()
- defer t.mu.Unlock()
- if !t.visible {
- return
- }
- data := ¬ifyIconData{
- CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
- HWnd: uint32(t.hWnd),
- UID: 1000,
- UFlags: NIF_MESSAGE,
- }
- procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data)))
- t.visible = false
-}
-
-// IsVisible 是否可见
-func (t *Tray) IsVisible() bool {
- t.mu.Lock()
- defer t.mu.Unlock()
- return t.visible
-}
-
-// ─────────────────────────────────────────────
-// Win32 API 定义
-// ─────────────────────────────────────────────
-
-const (
- WM_USER = 0x0400
- SHM_ADD uintptr = 0x00000000
- SHM_DELETE uintptr = 0x00000001
-
- NIF_MESSAGE = 0x00000001
- NIF_ICON = 0x00000002
- NIF_TIP = 0x00000004
-)
-
-// NOTIFYICONDATA struct
-type notifyIconData struct {
- CbSize uint32
- HWnd uint32
- UID uint32
- UFlags uint32
- UCallbackMessage uint32
- HIcon uintptr
- Tip [128]uint8
-}
-
-var (
- procShellNotifyIcon = mustLoad("shell32.dll").MustFindProc("Shell_NotifyIconA")
-)
-
-func mustLoad(dllName string) *windows.DynLib {
- dll, err := windows.LoadDLL(dllName)
- if err != nil {
- log.Fatalf("[tray] failed to load %s: %v", dllName, err)
- }
- return dll
-}
diff --git a/main.go b/main.go
index c658f6e..58c4b07 100644
--- a/main.go
+++ b/main.go
@@ -4,23 +4,33 @@ import (
"context"
"embed"
"log"
+ "sync/atomic"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
+ "github.com/wailsapp/wails/v2/pkg/options/windows"
+ "github.com/wailsapp/wails/v2/pkg/runtime"
"go_win_template/internal/plugins"
+ "go_win_template/internal/tray"
)
//go:embed all:frontend/dist
//go:embed assets/icon.png
+//go:embed build/windows/icon.ico
var assets embed.FS
+var (
+ trayMgr *tray.Manager
+ forceQuit atomic.Bool
+)
+
func main() {
wailsApp := NewApp()
cfg := &options.App{
- Title: "go_win_template",
+ Title: "青梧映星",
Width: 1280,
Height: 800,
MinWidth: 1024,
@@ -30,6 +40,10 @@ func main() {
Assets: assets,
},
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
+ Windows: &windows.Options{
+ // 窗口标题栏跟随系统白昼/黑夜主题
+ Theme: windows.SystemDefault,
+ },
OnStartup: func(ctx context.Context) {
log.Println("[main] startup complete")
// 启动插件
@@ -37,6 +51,25 @@ func main() {
log.Printf("[main] plugin start error: %v", err)
}
plugins.PrintStatus()
+ // 启动系统托盘
+ startTray(ctx)
+ },
+ OnBeforeClose: func(ctx context.Context) bool {
+ // 真正退出时不再拦截
+ if forceQuit.Load() {
+ return false
+ }
+ // 托盘可用时,关闭窗口改为隐藏到托盘
+ if trayMgr != nil && trayMgr.Active() {
+ runtime.WindowHide(ctx)
+ return true
+ }
+ return false
+ },
+ OnShutdown: func(context.Context) {
+ if trayMgr != nil {
+ trayMgr.Quit()
+ }
},
Bind: []interface{}{
wailsApp,
@@ -48,3 +81,23 @@ func main() {
log.Fatal(err)
}
}
+
+// startTray 初始化系统托盘。Windows 下托盘图标需为 .ico 格式,
+// 使用 build/windows/icon.ico;macOS/Linux 可改用 .png。
+func startTray(ctx context.Context) {
+ icon, err := assets.ReadFile("build/windows/icon.ico")
+ if err != nil {
+ log.Printf("[tray] load icon failed: %v", err)
+ return
+ }
+
+ trayMgr = tray.New(tray.Options{
+ Icon: icon,
+ Tooltip: "青梧映星",
+ OnQuit: func() {
+ forceQuit.Store(true)
+ runtime.Quit(ctx)
+ },
+ })
+ trayMgr.Run(ctx)
+}
diff --git a/pkg/icongen/main.go b/pkg/icongen/main.go
new file mode 100644
index 0000000..aa5fb13
--- /dev/null
+++ b/pkg/icongen/main.go
@@ -0,0 +1,160 @@
+// Command icongen 根据一张图片快速生成当前项目的托盘图标与打包图标,
+// 已存在的图标文件会被直接覆盖。
+//
+// 用法(在项目根目录执行):
+//
+// go run ./pkg/icongen -src /path/to/image.png
+//
+// 可选参数:
+//
+// go run ./pkg/icongen -src logo.jpg -root . -appicon 1024 -tray 256
+package main
+
+import (
+ "bytes"
+ "encoding/binary"
+ "flag"
+ "fmt"
+ "image"
+ imagedraw "image/draw"
+ "image/png"
+ "log"
+ "os"
+ "path/filepath"
+
+ "golang.org/x/image/draw"
+ _ "golang.org/x/image/webp"
+ _ "image/gif"
+ _ "image/jpeg"
+)
+
+// ico 文件中包含的尺寸
+var icoSizes = []int{256, 128, 64, 48, 32, 16}
+
+func main() {
+ src := flag.String("src", "", "源图片路径(png/jpg/jpeg/gif/bmp/webp)")
+ root := flag.String("root", ".", "项目根目录")
+ appicon := flag.Int("appicon", 1024, "build/appicon.png 尺寸")
+ icon := flag.Int("icon", 1024, "build/icon.png 尺寸")
+ tray := flag.Int("tray", 256, "trayicons/assets 图标尺寸")
+ flag.Parse()
+
+ if *src == "" {
+ log.Fatal("[-src] 源图片路径不能为空")
+ }
+
+ f, err := os.Open(*src)
+ if err != nil {
+ log.Fatalf("打开源图片失败: %v", err)
+ }
+ defer f.Close()
+
+ srcImg, _, err := image.Decode(f)
+ if err != nil {
+ log.Fatalf("解码源图片失败(支持 png/jpg/gif/bmp/webp): %v", err)
+ }
+ log.Printf("源图片: %s (%dx%d)", *src, srcImg.Bounds().Dx(), srcImg.Bounds().Dy())
+
+ cropped := centerCrop(srcImg)
+
+ // PNG 目标:源图居中裁剪后缩放
+ writePNG(filepath.Join(*root, "build", "appicon.png"), cropped, *appicon)
+ writePNG(filepath.Join(*root, "build", "icon.png"), cropped, *icon)
+ writePNG(filepath.Join(*root, "assets", "icon.png"), cropped, *tray)
+ writePNG(filepath.Join(*root, "trayicons", "icon.png"), cropped, *tray)
+
+ // ICO 目标:Windows exe / 托盘图标(多尺寸)
+ writeICO(filepath.Join(*root, "build", "windows", "icon.ico"), cropped, icoSizes)
+
+ log.Println("完成,所有图标已按需覆盖。")
+}
+
+// centerCrop 从图像中心裁剪出正方形区域。
+func centerCrop(srcImg image.Image) image.Image {
+ b := srcImg.Bounds()
+ w, h := b.Dx(), b.Dy()
+ side := w
+ if h < side {
+ side = h
+ }
+ x0 := b.Min.X + (w-side)/2
+ y0 := b.Min.Y + (h-side)/2
+ rect := image.Rect(0, 0, side, side)
+ out := image.NewRGBA(rect)
+ imagedraw.Draw(out, rect, srcImg, image.Pt(x0, y0), imagedraw.Src)
+ return out
+}
+
+// writePNG 按 size 缩放后写出 PNG 文件。
+func writePNG(path string, cropped image.Image, size int) {
+ dst := image.NewRGBA(image.Rect(0, 0, size, size))
+ draw.CatmullRom.Scale(dst, dst.Bounds(), cropped, cropped.Bounds(), draw.Over, nil)
+
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ log.Fatalf("创建目录失败 %s: %v", filepath.Dir(path), err)
+ }
+ out, err := os.Create(path)
+ if err != nil {
+ log.Fatalf("创建文件失败 %s: %v", path, err)
+ }
+ defer out.Close()
+ if err := png.Encode(out, dst); err != nil {
+ log.Fatalf("编码 PNG 失败 %s: %v", path, err)
+ }
+ fmt.Printf(" [ok] %s (%dx%d)\n", path, size, size)
+}
+
+// writeICO 写出多尺寸 ICO(Windows 图标,PNG 条目压缩,Vista+ 支持)。
+func writeICO(path string, cropped image.Image, sizes []int) {
+ type entry struct {
+ w, h byte
+ data []byte
+ }
+ entries := make([]entry, 0, len(sizes))
+ for _, s := range sizes {
+ dst := image.NewRGBA(image.Rect(0, 0, s, s))
+ draw.CatmullRom.Scale(dst, dst.Bounds(), cropped, cropped.Bounds(), draw.Over, nil)
+
+ var pngBuf bytes.Buffer
+ if err := png.Encode(&pngBuf, dst); err != nil {
+ log.Fatalf("编码 ICO 条目失败 (%dpx): %v", s, err)
+ }
+ w, h := byte(s), byte(s)
+ if s == 256 {
+ w, h = 0, 0 // 0 表示 256
+ }
+ entries = append(entries, entry{w: w, h: h, data: pngBuf.Bytes()})
+ }
+
+ var buf bytes.Buffer
+ // ICONDIR(6 字节头)
+ binary.Write(&buf, binary.LittleEndian, uint16(0)) // Reserved
+ binary.Write(&buf, binary.LittleEndian, uint16(1)) // Type: icon
+ binary.Write(&buf, binary.LittleEndian, uint16(len(entries)))
+
+ // ICONDIRENTRY(16 字节 x N)
+ cur := 6 + 16*len(entries)
+ for _, e := range entries {
+ binary.Write(&buf, binary.LittleEndian, e.w)
+ binary.Write(&buf, binary.LittleEndian, e.h)
+ binary.Write(&buf, binary.LittleEndian, uint8(0)) // 颜色数(忽略)
+ binary.Write(&buf, binary.LittleEndian, uint8(0)) // 保留
+ binary.Write(&buf, binary.LittleEndian, uint16(1)) // 平面数
+ binary.Write(&buf, binary.LittleEndian, uint16(32))
+ binary.Write(&buf, binary.LittleEndian, uint32(len(e.data)))
+ binary.Write(&buf, binary.LittleEndian, uint32(cur))
+ cur += len(e.data)
+ }
+ // PNG 图像数据
+ for _, e := range entries {
+ buf.Write(e.data)
+ }
+
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ log.Fatalf("创建目录失败 %s: %v", filepath.Dir(path), err)
+ }
+ if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
+ log.Fatalf("写入 ICO 失败 %s: %v", path, err)
+ }
+ fmt.Printf(" [ok] %s (ico %v)\n", path, sizes)
+}
diff --git a/trayicons/icon.png b/trayicons/icon.png
index 78872a9..ea518cf 100644
Binary files a/trayicons/icon.png and b/trayicons/icon.png differ
diff --git a/wails.json b/wails.json
index 9105b88..21fd995 100644
--- a/wails.json
+++ b/wails.json
@@ -11,8 +11,9 @@
"email": "1297754537@qq.com"
},
"info": {
- "productname": "go_win_template",
+ "productname": "青梧映星",
"version": "1.0.0",
- "copyright": ""
+ "companyname": "青梧映星软件开发工作室",
+ "copyright": "© 2026 青梧映星软件开发工作室"
}
}