feat: 接入托盘 / 添加图标生成工具 / 重命名并完善前端
- 托盘:用 getlantern/systray 重写 internal/tray,支持显示/隐藏窗口与退出 - 图标:新增 pkg/icongen 工具,支持 png/jpg/gif/bmp/webp,输出 ico/png 多个尺寸 - 重命名:项目默认名改为「青梧映星」,wails.json/productname/companyname/copyright 同步更新 - 顶栏:新增顶部导航栏(主题切换、设置、用户),iconfont 自托管 fa-solid-900.woff2 - 主题:CSS 变量 + html[data-theme] 控制白昼/黑夜切换,同时调用 WindowSetDarkTheme/LightTheme - 底部:版权栏包含工作室、开发者与点击跳转客服链接 - 打包:wails build + exe 版本更新至 1.20 MB
This commit is contained in:
125
internal/tray/tray.go
Normal file
125
internal/tray/tray.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user