c0dd3b9028
- 原生 win32 实现的 TrayManager:Shell_NotifyIcon + 私有 message-only 窗口承载 popup 菜单,专用 OS 线程跑 GetMessage 泵,避免引入 cgo 依赖。 - WindowSubclass 通过 SetWindowLongPtrW 子类化 Gio 主窗口,拦截 WM_CLOSE:minimize_to_tray=true 时吞掉消息只隐藏窗口;exit 路径下 放行让 Gio 正常关闭事件循环。 - Preferences 新增"最小化到托盘"复选框,切换后立即重装/卸载钩子, 不需要重启程序。 - 托盘菜单提供 Show / Hide / Exit;左键单击图标等同 Show;右键弹菜单。 - 非 Windows 平台所有相关 API 退化为 ErrUnsupported / no-op stub。
140 lines
4.0 KiB
Go
140 lines
4.0 KiB
Go
//go:build windows
|
|
|
|
package win32
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
// WindowSubclass intercepts the WM_CLOSE message on an external window
|
|
// (in our case Gio's app window) and redirects it to a Go callback. The
|
|
// caller decides whether to actually close, hide, or ignore.
|
|
//
|
|
// Implementation notes
|
|
//
|
|
// - We swap the window procedure via SetWindowLongPtrW(GWLP_WNDPROC),
|
|
// keeping a reference to the original so non-WM_CLOSE messages pass
|
|
// through unchanged.
|
|
// - The Go callback runs on the UI thread of the subclassed window
|
|
// (because that's who DispatchMessage'd into our hook). Returning
|
|
// false from the callback drops the message; true forwards it on.
|
|
// - Only one subclass per HWND is supported. Calling Install a second
|
|
// time on the same HWND returns an error.
|
|
type WindowSubclass struct {
|
|
hwnd uintptr
|
|
origProc uintptr
|
|
cb func() bool
|
|
|
|
once sync.Once
|
|
gone bool
|
|
}
|
|
|
|
var (
|
|
subclassMu sync.Mutex
|
|
subclasses = map[uintptr]*WindowSubclass{}
|
|
)
|
|
|
|
// InstallCloseHook subclasses hwnd and routes WM_CLOSE through onClose.
|
|
// If onClose returns false the close is swallowed; if true the original
|
|
// window procedure handles it (the default behaviour, which destroys
|
|
// the window).
|
|
func InstallCloseHook(hwnd uintptr, onClose func() bool) (*WindowSubclass, error) {
|
|
if hwnd == 0 {
|
|
return nil, fmt.Errorf("win32: InstallCloseHook: nil hwnd")
|
|
}
|
|
subclassMu.Lock()
|
|
defer subclassMu.Unlock()
|
|
if _, exists := subclasses[hwnd]; exists {
|
|
return nil, fmt.Errorf("win32: hwnd %#x already subclassed", hwnd)
|
|
}
|
|
|
|
s := &WindowSubclass{hwnd: hwnd, cb: onClose}
|
|
proc := syscall.NewCallback(s.wndProc)
|
|
orig, _, e := procSetWindowLongPtrW.Call(hwnd, gwlpWndProc, proc)
|
|
if orig == 0 {
|
|
if errno, ok := e.(syscall.Errno); ok && errno != 0 {
|
|
return nil, fmt.Errorf("win32: SetWindowLongPtrW: %w", errno)
|
|
}
|
|
return nil, fmt.Errorf("win32: SetWindowLongPtrW returned 0")
|
|
}
|
|
s.origProc = orig
|
|
subclasses[hwnd] = s
|
|
return s, nil
|
|
}
|
|
|
|
// Remove restores the original window procedure. Safe to call more than
|
|
// once; subsequent calls are no-ops.
|
|
func (s *WindowSubclass) Remove() {
|
|
s.once.Do(func() {
|
|
s.gone = true
|
|
subclassMu.Lock()
|
|
delete(subclasses, s.hwnd)
|
|
subclassMu.Unlock()
|
|
procSetWindowLongPtrW.Call(s.hwnd, gwlpWndProc, s.origProc)
|
|
})
|
|
}
|
|
|
|
func (s *WindowSubclass) wndProc(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
|
|
if msg == wmClose && s.cb != nil && !s.gone {
|
|
if !s.cb() {
|
|
return 0
|
|
}
|
|
}
|
|
r, _, _ := procCallWindowProcW.Call(s.origProc, hwnd, uintptr(msg), wParam, lParam)
|
|
return r
|
|
}
|
|
|
|
// ShowWindow toggles a window's visibility. cmdShow uses the standard
|
|
// SW_* constants (SW_HIDE = 0, SW_SHOW = 5, SW_RESTORE = 9).
|
|
func ShowWindow(hwnd uintptr, cmdShow int32) {
|
|
procShowWindowProc.Call(hwnd, uintptr(cmdShow))
|
|
}
|
|
|
|
// SW_* constants exposed so callers do not need to redefine them.
|
|
const (
|
|
SW_HIDE int32 = 0
|
|
SW_SHOW int32 = 5
|
|
SW_RESTORE int32 = 9
|
|
)
|
|
|
|
// FindWindowByTitle searches top-level windows for one whose title
|
|
// exactly matches. Returns 0 when nothing matches.
|
|
func FindWindowByTitle(title string) uintptr {
|
|
wtitle, err := syscall.UTF16PtrFromString(title)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
r, _, _ := procFindWindowW.Call(0, uintptr(unsafe.Pointer(wtitle)))
|
|
return r
|
|
}
|
|
|
|
// PostCloseMessage asks the window to close the same way the user
|
|
// clicking the X would. Combined with InstallCloseHook this lets the
|
|
// tray "Exit" item route through the same path as a real close, so the
|
|
// hook can let it through after setting an "exiting" flag.
|
|
func PostCloseMessage(hwnd uintptr) {
|
|
procPostMessageW.Call(hwnd, uintptr(wmClose), 0, 0)
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// raw syscalls
|
|
|
|
var (
|
|
procSetWindowLongPtrW = user32.NewProc(setWindowLongPtrName)
|
|
procCallWindowProcW = user32.NewProc("CallWindowProcW")
|
|
)
|
|
|
|
const (
|
|
wmClose uint32 = 0x0010
|
|
)
|
|
|
|
// gwlpWndProc is the GWLP_WNDPROC index. Declared as a var (not a const)
|
|
// because Go forbids constant negative-to-uintptr conversion.
|
|
var gwlpWndProc = func() uintptr {
|
|
x := int32(-4)
|
|
return uintptr(x)
|
|
}()
|