From 58138ac09bf78f9ad50f3dc3559365732d64f051 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sun, 7 May 2023 19:13:07 +1000 Subject: [PATCH] [v3 windows] Initial systray support --- v3/pkg/application/application_darwin.go | 4 + v3/pkg/application/application_windows.go | 22 +- v3/pkg/application/systemtray.go | 6 + v3/pkg/application/systemtray_windows.go | 223 ++++++++-- v3/pkg/application/webview_window_windows.go | 3 +- v3/pkg/w32/constants.go | 182 +++++---- v3/pkg/w32/icon.go | 41 ++ v3/pkg/w32/shell32.go | 8 + v3/pkg/w32/typedef.go | 19 + v3/pkg/w32/user32.go | 39 +- v3/pkg/w32/utils.go | 407 +++++++++++++++++++ v3/pkg/w32/window.go | 49 +++ 12 files changed, 861 insertions(+), 142 deletions(-) create mode 100644 v3/pkg/w32/icon.go diff --git a/v3/pkg/application/application_darwin.go b/v3/pkg/application/application_darwin.go index b299c1f6..a5127c2e 100644 --- a/v3/pkg/application/application_darwin.go +++ b/v3/pkg/application/application_darwin.go @@ -137,6 +137,10 @@ type macosApp struct { parent *App } +func getNativeApplication() *macosApp { + return globalApplication.impl.(*macosApp) +} + func (m *macosApp) hide() { C.hide() } diff --git a/v3/pkg/application/application_windows.go b/v3/pkg/application/application_windows.go index 2cf49bc8..15995278 100644 --- a/v3/pkg/application/application_windows.go +++ b/v3/pkg/application/application_windows.go @@ -20,7 +20,8 @@ type windowsApp struct { instance w32.HINSTANCE - windowMap map[w32.HWND]*windowsWebviewWindow + windowMap map[w32.HWND]*windowsWebviewWindow + systrayMap map[w32.HMENU]*windowsSystemTray mainThreadID w32.HANDLE mainThreadWindowHWND w32.HWND @@ -33,6 +34,10 @@ type windowsApp struct { isDarkMode bool } +func getNativeApplication() *windowsApp { + return globalApplication.impl.(*windowsApp) +} + func (m *windowsApp) getPrimaryScreen() (*Screen, error) { //TODO implement me panic("implement me") @@ -194,6 +199,10 @@ func (m *windowsApp) wndProc(hwnd w32.HWND, msg uint32, wParam, lParam uintptr) return window.WndProc(msg, wParam, lParam) } + if systray, ok := m.systrayMap[hwnd]; ok { + return systray.wndProc(msg, wParam, lParam) + } + // Dispatch the message to the appropriate window return w32.DefWindowProc(hwnd, msg, wParam, lParam) @@ -203,6 +212,10 @@ func (m *windowsApp) registerWindow(result *windowsWebviewWindow) { m.windowMap[result.hwnd] = result } +func (m *windowsApp) registerSystemTray(result *windowsSystemTray) { + m.systrayMap[result.hwnd] = result +} + func (m *windowsApp) unregisterWindow(w *windowsWebviewWindow) { delete(m.windowMap, w.hwnd) @@ -220,9 +233,10 @@ func newPlatformApp(app *App) *windowsApp { } result := &windowsApp{ - parent: app, - instance: w32.GetModuleHandle(""), - windowMap: make(map[w32.HWND]*windowsWebviewWindow), + parent: app, + instance: w32.GetModuleHandle(""), + windowMap: make(map[w32.HWND]*windowsWebviewWindow), + systrayMap: make(map[w32.HWND]*windowsSystemTray), } result.init() diff --git a/v3/pkg/application/systemtray.go b/v3/pkg/application/systemtray.go index b7cf41d9..9b0cdece 100644 --- a/v3/pkg/application/systemtray.go +++ b/v3/pkg/application/systemtray.go @@ -30,6 +30,11 @@ type SystemTray struct { icon []byte iconPosition int + leftButtonClickHandler func() + rightButtonClickHandler func() + leftButtonDoubleClickHandler func() + rightButtonDoubleClickHandler func() + // Platform specific implementation impl systemTrayImpl menu *Menu @@ -41,6 +46,7 @@ func NewSystemTray(id uint) *SystemTray { id: id, label: "", iconPosition: NSImageLeading, + icon: DefaultApplicationIcon, } } diff --git a/v3/pkg/application/systemtray_windows.go b/v3/pkg/application/systemtray_windows.go index 2e11586d..da300ed2 100644 --- a/v3/pkg/application/systemtray_windows.go +++ b/v3/pkg/application/systemtray_windows.go @@ -2,10 +2,24 @@ package application +import ( + "github.com/wailsapp/wails/v3/pkg/events" + "github.com/wailsapp/wails/v3/pkg/w32" + "syscall" + "unsafe" +) + type windowsSystemTray struct { - id uint - icon []byte - menu *Menu + parent *SystemTray + + // Platform specific implementation + uid uint32 + hwnd w32.HWND + appIcon w32.HICON + lightModeIcon w32.HICON + darkModeIcon w32.HICON + currentIcon w32.HICON + //menu *w32.PopupMenu } func (s *windowsSystemTray) setIconPosition(position int) { @@ -13,56 +27,183 @@ func (s *windowsSystemTray) setIconPosition(position int) { } func (s *windowsSystemTray) setMenu(menu *Menu) { - s.menu = menu + //s.menu = menu + panic("implement me") } func (s *windowsSystemTray) run() { - globalApplication.dispatchOnMainThread(func() { - //if s.nsStatusItem != nil { - // Fatal("System tray '%d' already running", s.id) - //} - //s.nsStatusItem = unsafe.Pointer(C.systemTrayNew()) - //if s.label != "" { - // C.systemTraySetLabel(s.nsStatusItem, C.CString(s.label)) - //} - //if s.icon != nil { - // s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&s.icon[0]), C.int(len(s.icon)))) - // C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) - //} - //if s.menu != nil { - // s.menu.Update() - // // Convert impl to macosMenu object - // s.nsMenu = (s.menu.impl).(*macosMenu).nsMenu - // C.systemTraySetMenu(s.nsStatusItem, s.nsMenu) - //} - panic("implement me") + + NotifyIconClassName := "WailsSystray" + _, err := w32.RegisterWindow(NotifyIconClassName, getNativeApplication().wndProc) + if err != nil { + panic(err) + } + + s.hwnd = w32.CreateWindowEx( + 0, + w32.MustStringToUTF16Ptr(NotifyIconClassName), + nil, + 0, + 0, + 0, + 0, + 0, + w32.HWND_MESSAGE, + 0, + 0, + nil) + if s.hwnd == 0 { + panic(syscall.GetLastError()) + } + + nid := w32.NOTIFYICONDATA{ + HWnd: s.hwnd, + UID: uint32(s.parent.id), + UFlags: w32.NIF_ICON | w32.NIF_MESSAGE, + HIcon: s.currentIcon, + UCallbackMessage: w32.WM_USER + uint32(s.parent.id), + } + nid.CbSize = uint32(unsafe.Sizeof(nid)) + + if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) { + panic(syscall.GetLastError()) + } + + nid.UVersion = w32.NOTIFYICON_VERSION + + if !w32.ShellNotifyIcon(w32.NIM_SETVERSION, &nid) { + panic(syscall.GetLastError()) + } + + defaultIcon, err := w32.CreateHIconFromPNG(s.parent.icon) + if err != nil { + panic(err) + } + s.lightModeIcon = defaultIcon + s.darkModeIcon = defaultIcon + s.uid = nid.UID + + // TODO: Set Menu + + // Update the icon + s.updateIcon() + + // Listen for dark mode changes + globalApplication.On(events.Windows.SystemThemeChanged, func() { + s.updateIcon() }) + + // Register the system tray + getNativeApplication().registerSystemTray(s) + +} + +func (s *windowsSystemTray) updateIcon() { + + var newIcon w32.HICON + if w32.IsCurrentlyDarkMode() { + newIcon = s.darkModeIcon + } else { + newIcon = s.lightModeIcon + } + if s.currentIcon == newIcon { + return + } + + s.currentIcon = newIcon + nid := s.newNotifyIconData() + nid.UFlags = w32.NIF_ICON | w32.NIF_MESSAGE + if s.currentIcon != 0 { + nid.HIcon = s.currentIcon + } + + if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) { + panic(syscall.GetLastError()) + } + return +} + +func (s *windowsSystemTray) newNotifyIconData() w32.NOTIFYICONDATA { + nid := w32.NOTIFYICONDATA{ + UID: s.uid, + HWnd: s.hwnd, + } + nid.CbSize = uint32(unsafe.Sizeof(nid)) + return nid } func (s *windowsSystemTray) setIcon(icon []byte) { - s.icon = icon - globalApplication.dispatchOnMainThread(func() { - //s.nsImage = unsafe.Pointer(C.imageFromBytes((*C.uchar)(&icon[0]), C.int(len(icon)))) - //C.systemTraySetIcon(s.nsStatusItem, s.nsImage, C.int(s.iconPosition), C.bool(s.isTemplateIcon)) - }) -} - -func (s *windowsSystemTray) setTemplateIcon(icon []byte) { - // Unsupported - do nothing -} - -func newSystemTrayImpl(s *SystemTray) systemTrayImpl { - return &windowsSystemTray{ - id: s.id, - icon: s.icon, - menu: s.menu, + // TODO: + var err error + if w32.IsCurrentlyDarkMode() { + s.darkModeIcon, err = w32.CreateHIconFromPNG(icon) + if err != nil { + panic(syscall.GetLastError()) + } + } else { + s.lightModeIcon, err = w32.CreateHIconFromPNG(icon) + if err != nil { + panic(syscall.GetLastError()) + } } + // Update the icon + s.updateIcon() } -func (s *windowsSystemTray) setLabel(label string) { - // Unsupported - do nothing +func newSystemTrayImpl(parent *SystemTray) systemTrayImpl { + return &windowsSystemTray{ + parent: parent, + } } func (s *windowsSystemTray) destroy() { panic("implement me") } + +func (s *windowsSystemTray) wndProc(msg uint32, wParam, lParam uintptr) uintptr { + switch msg { + case w32.WM_USER + uint32(s.parent.id): + switch lParam { + case w32.WM_LBUTTONUP: + if s.parent.leftButtonClickHandler != nil { + s.parent.leftButtonClickHandler() + } + case w32.WM_RBUTTONUP: + if s.parent.rightButtonClickHandler != nil { + s.parent.rightButtonClickHandler() + } + case w32.WM_LBUTTONDBLCLK: + if s.parent.leftButtonDoubleClickHandler != nil { + s.parent.leftButtonDoubleClickHandler() + } + case w32.WM_RBUTTONDBLCLK: + if s.parent.rightButtonDoubleClickHandler != nil { + s.parent.rightButtonDoubleClickHandler() + } + default: + println(w32.WMMessageToString(lParam)) + } + // TODO: Menu processing + //case w32.WM_COMMAND: + // cmdMsgID := int(wparam & 0xffff) + // switch cmdMsgID { + // default: + // p.menu.ProcessCommand(cmdMsgID) + // } + default: + msg := int(wParam & 0xffff) + println(w32.WMMessageToString(uintptr(msg))) + } + + return w32.DefWindowProc(s.hwnd, msg, wParam, lParam) +} + +// ---- Unsupported ---- + +func (s *windowsSystemTray) setLabel(_ string) { + // Unsupported - do nothing +} + +func (s *windowsSystemTray) setTemplateIcon(_ []byte) { + // Unsupported - do nothing +} diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index b196c363..d9ac705e 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -115,8 +115,7 @@ func (w *windowsWebviewWindow) run() { } // Register the window with the application - windowsApp := globalApplication.impl.(*windowsApp) - windowsApp.registerWindow(w) + getNativeApplication().registerWindow(w) w.setResizable(!options.DisableResize) diff --git a/v3/pkg/w32/constants.go b/v3/pkg/w32/constants.go index cb4cc930..fb8cfe56 100644 --- a/v3/pkg/w32/constants.go +++ b/v3/pkg/w32/constants.go @@ -1046,39 +1046,39 @@ const ( ) const ( - TBM_GETPOS = (WM_USER) - TBM_GETRANGEMIN = (WM_USER + 1) - TBM_GETRANGEMAX = (WM_USER + 2) - TBM_GETTIC = (WM_USER + 3) - TBM_SETTIC = (WM_USER + 4) - TBM_SETPOS = (WM_USER + 5) - TBM_SETRANGE = (WM_USER + 6) - TBM_SETRANGEMIN = (WM_USER + 7) - TBM_SETRANGEMAX = (WM_USER + 8) - TBM_CLEARTICS = (WM_USER + 9) - TBM_SETSEL = (WM_USER + 10) - TBM_SETSELSTART = (WM_USER + 11) - TBM_SETSELEND = (WM_USER + 12) - TBM_GETPTICS = (WM_USER + 14) - TBM_GETTICPOS = (WM_USER + 15) - TBM_GETNUMTICS = (WM_USER + 16) - TBM_GETSELSTART = (WM_USER + 17) - TBM_GETSELEND = (WM_USER + 18) - TBM_CLEARSEL = (WM_USER + 19) - TBM_SETTICFREQ = (WM_USER + 20) - TBM_SETPAGESIZE = (WM_USER + 21) - TBM_GETPAGESIZE = (WM_USER + 22) - TBM_SETLINESIZE = (WM_USER + 23) - TBM_GETLINESIZE = (WM_USER + 24) - TBM_GETTHUMBRECT = (WM_USER + 25) - TBM_GETCHANNELRECT = (WM_USER + 26) - TBM_SETTHUMBLENGTH = (WM_USER + 27) - TBM_GETTHUMBLENGTH = (WM_USER + 28) - TBM_SETTOOLTIPS = (WM_USER + 29) - TBM_GETTOOLTIPS = (WM_USER + 30) - TBM_SETTIPSIDE = (WM_USER + 31) - TBM_SETBUDDY = (WM_USER + 32) - TBM_GETBUDDY = (WM_USER + 33) + TBM_GETPOS = WM_USER + TBM_GETRANGEMIN = WM_USER + 1 + TBM_GETRANGEMAX = WM_USER + 2 + TBM_GETTIC = WM_USER + 3 + TBM_SETTIC = WM_USER + 4 + TBM_SETPOS = WM_USER + 5 + TBM_SETRANGE = WM_USER + 6 + TBM_SETRANGEMIN = WM_USER + 7 + TBM_SETRANGEMAX = WM_USER + 8 + TBM_CLEARTICS = WM_USER + 9 + TBM_SETSEL = WM_USER + 10 + TBM_SETSELSTART = WM_USER + 11 + TBM_SETSELEND = WM_USER + 12 + TBM_GETPTICS = WM_USER + 14 + TBM_GETTICPOS = WM_USER + 15 + TBM_GETNUMTICS = WM_USER + 16 + TBM_GETSELSTART = WM_USER + 17 + TBM_GETSELEND = WM_USER + 18 + TBM_CLEARSEL = WM_USER + 19 + TBM_SETTICFREQ = WM_USER + 20 + TBM_SETPAGESIZE = WM_USER + 21 + TBM_GETPAGESIZE = WM_USER + 22 + TBM_SETLINESIZE = WM_USER + 23 + TBM_GETLINESIZE = WM_USER + 24 + TBM_GETTHUMBRECT = WM_USER + 25 + TBM_GETCHANNELRECT = WM_USER + 26 + TBM_SETTHUMBLENGTH = WM_USER + 27 + TBM_GETTHUMBLENGTH = WM_USER + 28 + TBM_SETTOOLTIPS = WM_USER + 29 + TBM_GETTOOLTIPS = WM_USER + 30 + TBM_SETTIPSIDE = WM_USER + 31 + TBM_SETBUDDY = WM_USER + 32 + TBM_GETBUDDY = WM_USER + 33 ) const ( @@ -1809,8 +1809,8 @@ const ( RRF_RT_REG_DWORD = 0x00000010 RRF_RT_REG_MULTI_SZ = 0x00000020 RRF_RT_REG_QWORD = 0x00000040 - RRF_RT_DWORD = (RRF_RT_REG_BINARY | RRF_RT_REG_DWORD) - RRF_RT_QWORD = (RRF_RT_REG_BINARY | RRF_RT_REG_QWORD) + RRF_RT_DWORD = RRF_RT_REG_BINARY | RRF_RT_REG_DWORD + RRF_RT_QWORD = RRF_RT_REG_BINARY | RRF_RT_REG_QWORD RRF_RT_ANY = 0x0000ffff RRF_NOEXPAND = 0x10000000 RRF_ZEROONFAILURE = 0x20000000 @@ -2047,39 +2047,39 @@ const ( // Tooltip messages const ( - TTM_ACTIVATE = (WM_USER + 1) - TTM_SETDELAYTIME = (WM_USER + 3) - TTM_ADDTOOL = (WM_USER + 50) - TTM_DELTOOL = (WM_USER + 51) - TTM_NEWTOOLRECT = (WM_USER + 52) - TTM_RELAYEVENT = (WM_USER + 7) - TTM_GETTOOLINFO = (WM_USER + 53) - TTM_SETTOOLINFO = (WM_USER + 54) - TTM_HITTEST = (WM_USER + 55) - TTM_GETTEXT = (WM_USER + 56) - TTM_UPDATETIPTEXT = (WM_USER + 57) - TTM_GETTOOLCOUNT = (WM_USER + 13) - TTM_ENUMTOOLS = (WM_USER + 58) - TTM_GETCURRENTTOOL = (WM_USER + 59) - TTM_WINDOWFROMPOINT = (WM_USER + 16) - TTM_TRACKACTIVATE = (WM_USER + 17) - TTM_TRACKPOSITION = (WM_USER + 18) - TTM_SETTIPBKCOLOR = (WM_USER + 19) - TTM_SETTIPTEXTCOLOR = (WM_USER + 20) - TTM_GETDELAYTIME = (WM_USER + 21) - TTM_GETTIPBKCOLOR = (WM_USER + 22) - TTM_GETTIPTEXTCOLOR = (WM_USER + 23) - TTM_SETMAXTIPWIDTH = (WM_USER + 24) - TTM_GETMAXTIPWIDTH = (WM_USER + 25) - TTM_SETMARGIN = (WM_USER + 26) - TTM_GETMARGIN = (WM_USER + 27) - TTM_POP = (WM_USER + 28) - TTM_UPDATE = (WM_USER + 29) - TTM_GETBUBBLESIZE = (WM_USER + 30) - TTM_ADJUSTRECT = (WM_USER + 31) - TTM_SETTITLE = (WM_USER + 33) - TTM_POPUP = (WM_USER + 34) - TTM_GETTITLE = (WM_USER + 35) + TTM_ACTIVATE = WM_USER + 1 + TTM_SETDELAYTIME = WM_USER + 3 + TTM_ADDTOOL = WM_USER + 50 + TTM_DELTOOL = WM_USER + 51 + TTM_NEWTOOLRECT = WM_USER + 52 + TTM_RELAYEVENT = WM_USER + 7 + TTM_GETTOOLINFO = WM_USER + 53 + TTM_SETTOOLINFO = WM_USER + 54 + TTM_HITTEST = WM_USER + 55 + TTM_GETTEXT = WM_USER + 56 + TTM_UPDATETIPTEXT = WM_USER + 57 + TTM_GETTOOLCOUNT = WM_USER + 13 + TTM_ENUMTOOLS = WM_USER + 58 + TTM_GETCURRENTTOOL = WM_USER + 59 + TTM_WINDOWFROMPOINT = WM_USER + 16 + TTM_TRACKACTIVATE = WM_USER + 17 + TTM_TRACKPOSITION = WM_USER + 18 + TTM_SETTIPBKCOLOR = WM_USER + 19 + TTM_SETTIPTEXTCOLOR = WM_USER + 20 + TTM_GETDELAYTIME = WM_USER + 21 + TTM_GETTIPBKCOLOR = WM_USER + 22 + TTM_GETTIPTEXTCOLOR = WM_USER + 23 + TTM_SETMAXTIPWIDTH = WM_USER + 24 + TTM_GETMAXTIPWIDTH = WM_USER + 25 + TTM_SETMARGIN = WM_USER + 26 + TTM_GETMARGIN = WM_USER + 27 + TTM_POP = WM_USER + 28 + TTM_UPDATE = WM_USER + 29 + TTM_GETBUBBLESIZE = WM_USER + 30 + TTM_ADJUSTRECT = WM_USER + 31 + TTM_SETTITLE = WM_USER + 33 + TTM_POPUP = WM_USER + 34 + TTM_GETTITLE = WM_USER + 35 ) // Tooltip icons @@ -2097,10 +2097,10 @@ const ( const ( TTN_FIRST = -520 TTN_LAST = -549 - TTN_GETDISPINFO = (TTN_FIRST - 10) - TTN_SHOW = (TTN_FIRST - 1) - TTN_POP = (TTN_FIRST - 2) - TTN_LINKCLICK = (TTN_FIRST - 3) + TTN_GETDISPINFO = TTN_FIRST - 10 + TTN_SHOW = TTN_FIRST - 1 + TTN_POP = TTN_FIRST - 2 + TTN_LINKCLICK = TTN_FIRST - 3 TTN_NEEDTEXT = TTN_GETDISPINFO ) @@ -2244,8 +2244,8 @@ const ( // WM_NCHITTEST and MOUSEHOOKSTRUCT Mouse Position Codes const ( - HTERROR = (-2) - HTTRANSPARENT = (-1) + HTERROR = -2 + HTTRANSPARENT = -1 HTNOWHERE = 0 HTCLIENT = 1 HTCAPTION = 2 @@ -2328,8 +2328,8 @@ const ( GMEM_LOWER = GMEM_NOT_BANKED GMEM_VALID_FLAGS = 0x7F72 GMEM_INVALID_HANDLE = 0x8000 - GHND = (GMEM_MOVEABLE | GMEM_ZEROINIT) - GPTR = (GMEM_FIXED | GMEM_ZEROINIT) + GHND = GMEM_MOVEABLE | GMEM_ZEROINIT + GPTR = GMEM_FIXED | GMEM_ZEROINIT ) // Ternary raster operations @@ -2511,6 +2511,31 @@ const ( MAX_PATH = 260 ) +const ( + NIM_ADD = 0x00000000 + NIM_MODIFY = 0x00000001 + NIM_DELETE = 0x00000002 + NIM_SETVERSION = 0x00000004 + + NIF_MESSAGE = 0x00000001 + NIF_ICON = 0x00000002 + NIF_TIP = 0x00000004 + NIF_STATE = 0x00000008 + NIF_INFO = 0x00000010 + + NIS_HIDDEN = 0x00000001 + + NIIF_NONE = 0x00000000 + NIIF_INFO = 0x00000001 + NIIF_WARNING = 0x00000002 + NIIF_ERROR = 0x00000003 + NIIF_USER = 0x00000004 + NIIF_NOSOUND = 0x00000010 + NIIF_LARGE_ICON = 0x00000020 + NIIF_RESPECT_QUIET_TIME = 0x00000080 + NIIF_ICON_MASK = 0x0000000F +) + const ( FOREGROUND_BLUE = 0x0001 FOREGROUND_GREEN = 0x0002 @@ -2675,6 +2700,11 @@ const ( XBUTTON2 = 2 ) +const ( + LR_LOADFROMFILE = 0x00000010 + LR_DEFAULTSIZE = 0x00000040 +) + // Devmode const ( DM_SPECVERSION = 0x0401 diff --git a/v3/pkg/w32/icon.go b/v3/pkg/w32/icon.go new file mode 100644 index 00000000..e62171bb --- /dev/null +++ b/v3/pkg/w32/icon.go @@ -0,0 +1,41 @@ +//go:build windows + +package w32 + +import ( + "unsafe" +) + +func CreateIconFromResourceEx(presbits uintptr, dwResSize uint32, isIcon bool, version uint32, cxDesired int, cyDesired int, flags uint) (uintptr, error) { + icon := 0 + if isIcon { + icon = 1 + } + r, _, err := procCreateIconFromResourceEx.Call( + presbits, + uintptr(dwResSize), + uintptr(icon), + uintptr(version), + uintptr(cxDesired), + uintptr(cyDesired), + uintptr(flags), + ) + + if r == 0 { + return 0, err + } + return r, nil +} + +// CreateHIconFromPNG creates a HICON from a PNG file +func CreateHIconFromPNG(pngData []byte) (HICON, error) { + icon, err := CreateIconFromResourceEx( + uintptr(unsafe.Pointer(&pngData[0])), + uint32(len(pngData)), + true, + 0x00030000, + 0, + 0, + LR_DEFAULTSIZE) + return HICON(icon), err +} diff --git a/v3/pkg/w32/shell32.go b/v3/pkg/w32/shell32.go index 458fbc64..3b6f3e30 100644 --- a/v3/pkg/w32/shell32.go +++ b/v3/pkg/w32/shell32.go @@ -78,6 +78,8 @@ const ( CSIDL_FLAG_NO_ALIAS = 0x1000 CSIDL_FLAG_PER_USER_INIT = 0x8000 CSIDL_FLAG_MASK = 0xFF00 + + NOTIFYICON_VERSION = 4 ) var ( @@ -92,8 +94,14 @@ var ( procShellExecute = modshell32.NewProc("ShellExecuteW") procExtractIcon = modshell32.NewProc("ExtractIconW") procGetSpecialFolderPath = modshell32.NewProc("SHGetSpecialFolderPathW") + procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW") ) +func ShellNotifyIcon(cmd uintptr, nid *NOTIFYICONDATA) bool { + ret, _, _ := procShellNotifyIcon.Call(cmd, uintptr(unsafe.Pointer(nid))) + return ret == 1 +} + func SHBrowseForFolder(bi *BROWSEINFO) uintptr { ret, _, _ := procSHBrowseForFolder.Call(uintptr(unsafe.Pointer(bi))) diff --git a/v3/pkg/w32/typedef.go b/v3/pkg/w32/typedef.go index bd50b71c..219a7885 100644 --- a/v3/pkg/w32/typedef.go +++ b/v3/pkg/w32/typedef.go @@ -9,6 +9,7 @@ package w32 import ( "fmt" + "golang.org/x/sys/windows" "unsafe" ) @@ -653,6 +654,24 @@ type TRACKMOUSEEVENT struct { DwHoverTime uint32 } +type NOTIFYICONDATA struct { + CbSize uint32 + HWnd HWND + UID uint32 + UFlags uint32 + UCallbackMessage uint32 + HIcon HICON + SzTip [128]uint16 + DwState uint32 + DwStateMask uint32 + SzInfo [256]uint16 + UVersion uint32 + SzInfoTitle [64]uint16 + DwInfoFlags uint32 + GuidItem windows.GUID + HBalloonIcon HICON +} + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms534067.aspx type GdiplusStartupInput struct { GdiplusVersion uint32 diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index b8f69728..724ad531 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -69,27 +69,28 @@ var ( procGetSystemMetrics = moduser32.NewProc("GetSystemMetrics") procPostThreadMessageW = moduser32.NewProc("PostThreadMessageW") procRegisterWindowMessageA = moduser32.NewProc("RegisterWindowMessageA") + procCopyRect = moduser32.NewProc("CopyRect") + procEqualRect = moduser32.NewProc("EqualRect") + procInflateRect = moduser32.NewProc("InflateRect") + procIntersectRect = moduser32.NewProc("IntersectRect") + procIsRectEmpty = moduser32.NewProc("IsRectEmpty") + procOffsetRect = moduser32.NewProc("OffsetRect") + procPtInRect = moduser32.NewProc("PtInRect") + procSetRect = moduser32.NewProc("SetRect") + procSetRectEmpty = moduser32.NewProc("SetRectEmpty") + procSubtractRect = moduser32.NewProc("SubtractRect") + procUnionRect = moduser32.NewProc("UnionRect") + procCreateDialogParam = moduser32.NewProc("CreateDialogParamW") + procDialogBoxParam = moduser32.NewProc("DialogBoxParamW") + procGetDlgItem = moduser32.NewProc("GetDlgItem") + procDrawIcon = moduser32.NewProc("DrawIcon") + procCreateMenu = moduser32.NewProc("CreateMenu") + procDestroyMenu = moduser32.NewProc("DestroyMenu") + procCreatePopupMenu = moduser32.NewProc("CreatePopupMenu") + procCheckMenuRadioItem = moduser32.NewProc("CheckMenuRadioItem") + procCreateIconFromResourceEx = moduser32.NewProc("CreateIconFromResourceEx") //procSysColorBrush = moduser32.NewProc("GetSysColorBrush") - procCopyRect = moduser32.NewProc("CopyRect") - procEqualRect = moduser32.NewProc("EqualRect") - procInflateRect = moduser32.NewProc("InflateRect") - procIntersectRect = moduser32.NewProc("IntersectRect") - procIsRectEmpty = moduser32.NewProc("IsRectEmpty") - procOffsetRect = moduser32.NewProc("OffsetRect") - procPtInRect = moduser32.NewProc("PtInRect") - procSetRect = moduser32.NewProc("SetRect") - procSetRectEmpty = moduser32.NewProc("SetRectEmpty") - procSubtractRect = moduser32.NewProc("SubtractRect") - procUnionRect = moduser32.NewProc("UnionRect") - procCreateDialogParam = moduser32.NewProc("CreateDialogParamW") - procDialogBoxParam = moduser32.NewProc("DialogBoxParamW") - procGetDlgItem = moduser32.NewProc("GetDlgItem") - procDrawIcon = moduser32.NewProc("DrawIcon") - procCreateMenu = moduser32.NewProc("CreateMenu") //procSetMenu = moduser32.NewProc("SetMenu") - procDestroyMenu = moduser32.NewProc("DestroyMenu") - procCreatePopupMenu = moduser32.NewProc("CreatePopupMenu") - procCheckMenuRadioItem = moduser32.NewProc("CheckMenuRadioItem") //procDrawMenuBar = moduser32.NewProc("DrawMenuBar") //procInsertMenuItem = moduser32.NewProc("InsertMenuItemW") // FIXIT: diff --git a/v3/pkg/w32/utils.go b/v3/pkg/w32/utils.go index 8a72d484..1a3ad7cd 100644 --- a/v3/pkg/w32/utils.go +++ b/v3/pkg/w32/utils.go @@ -8,6 +8,7 @@ package w32 import ( + "fmt" "syscall" "unicode/utf16" "unsafe" @@ -228,3 +229,409 @@ func ComInvoke(disp *IDispatch, dispid int32, dispatch int16, params ...interfac result = &ret return } + +func WMMessageToString(msg uintptr) string { + // Convert windows message to string + switch msg { + case WM_APP: + return "WM_APP" + case WM_ACTIVATE: + return "WM_ACTIVATE" + case WM_ACTIVATEAPP: + return "WM_ACTIVATEAPP" + case WM_AFXFIRST: + return "WM_AFXFIRST" + case WM_AFXLAST: + return "WM_AFXLAST" + case WM_ASKCBFORMATNAME: + return "WM_ASKCBFORMATNAME" + case WM_CANCELJOURNAL: + return "WM_CANCELJOURNAL" + case WM_CANCELMODE: + return "WM_CANCELMODE" + case WM_CAPTURECHANGED: + return "WM_CAPTURECHANGED" + case WM_CHANGECBCHAIN: + return "WM_CHANGECBCHAIN" + case WM_CHAR: + return "WM_CHAR" + case WM_CHARTOITEM: + return "WM_CHARTOITEM" + case WM_CHILDACTIVATE: + return "WM_CHILDACTIVATE" + case WM_CLEAR: + return "WM_CLEAR" + case WM_CLOSE: + return "WM_CLOSE" + case WM_COMMAND: + return "WM_COMMAND" + case WM_COMMNOTIFY /* OBSOLETE */ : + return "WM_COMMNOTIFY" + case WM_COMPACTING: + return "WM_COMPACTING" + case WM_COMPAREITEM: + return "WM_COMPAREITEM" + case WM_CONTEXTMENU: + return "WM_CONTEXTMENU" + case WM_COPY: + return "WM_COPY" + case WM_COPYDATA: + return "WM_COPYDATA" + case WM_CREATE: + return "WM_CREATE" + case WM_CTLCOLORBTN: + return "WM_CTLCOLORBTN" + case WM_CTLCOLORDLG: + return "WM_CTLCOLORDLG" + case WM_CTLCOLOREDIT: + return "WM_CTLCOLOREDIT" + case WM_CTLCOLORLISTBOX: + return "WM_CTLCOLORLISTBOX" + case WM_CTLCOLORMSGBOX: + return "WM_CTLCOLORMSGBOX" + case WM_CTLCOLORSCROLLBAR: + return "WM_CTLCOLORSCROLLBAR" + case WM_CTLCOLORSTATIC: + return "WM_CTLCOLORSTATIC" + case WM_CUT: + return "WM_CUT" + case WM_DEADCHAR: + return "WM_DEADCHAR" + case WM_DELETEITEM: + return "WM_DELETEITEM" + case WM_DESTROY: + return "WM_DESTROY" + case WM_DESTROYCLIPBOARD: + return "WM_DESTROYCLIPBOARD" + case WM_DEVICECHANGE: + return "WM_DEVICECHANGE" + case WM_DEVMODECHANGE: + return "WM_DEVMODECHANGE" + case WM_DISPLAYCHANGE: + return "WM_DISPLAYCHANGE" + case WM_DRAWCLIPBOARD: + return "WM_DRAWCLIPBOARD" + case WM_DRAWITEM: + return "WM_DRAWITEM" + case WM_DROPFILES: + return "WM_DROPFILES" + case WM_ENABLE: + return "WM_ENABLE" + case WM_ENDSESSION: + return "WM_ENDSESSION" + case WM_ENTERIDLE: + return "WM_ENTERIDLE" + case WM_ENTERMENULOOP: + return "WM_ENTERMENULOOP" + case WM_ENTERSIZEMOVE: + return "WM_ENTERSIZEMOVE" + case WM_ERASEBKGND: + return "WM_ERASEBKGND" + case WM_EXITMENULOOP: + return "WM_EXITMENULOOP" + case WM_EXITSIZEMOVE: + return "WM_EXITSIZEMOVE" + case WM_FONTCHANGE: + return "WM_FONTCHANGE" + case WM_GETDLGCODE: + return "WM_GETDLGCODE" + case WM_GETFONT: + return "WM_GETFONT" + case WM_GETHOTKEY: + return "WM_GETHOTKEY" + case WM_GETICON: + return "WM_GETICON" + case WM_GETMINMAXINFO: + return "WM_GETMINMAXINFO" + case WM_GETTEXT: + return "WM_GETTEXT" + case WM_GETTEXTLENGTH: + return "WM_GETTEXTLENGTH" + case WM_HANDHELDFIRST: + return "WM_HANDHELDFIRST" + case WM_HANDHELDLAST: + return "WM_HANDHELDLAST" + case WM_HELP: + return "WM_HELP" + case WM_HOTKEY: + return "WM_HOTKEY" + case WM_HSCROLL: + return "WM_HSCROLL" + case WM_HSCROLLCLIPBOARD: + return "WM_HSCROLLCLIPBOARD" + case WM_ICONERASEBKGND: + return "WM_ICONERASEBKGND" + case WM_INITDIALOG: + return "WM_INITDIALOG" + case WM_INITMENU: + return "WM_INITMENU" + case WM_INITMENUPOPUP: + return "WM_INITMENUPOPUP" + case WM_INPUT: + return "WM_INPUT" + case WM_INPUTLANGCHANGE: + return "WM_INPUTLANGCHANGE" + case WM_INPUTLANGCHANGEREQUEST: + return "WM_INPUTLANGCHANGEREQUEST" + case WM_KEYDOWN: + return "WM_KEYDOWN" + case WM_KEYUP: + return "WM_KEYUP" + case WM_KILLFOCUS: + return "WM_KILLFOCUS" + case WM_MDIACTIVATE: + return "WM_MDIACTIVATE" + case WM_MDICASCADE: + return "WM_MDICASCADE" + case WM_MDICREATE: + return "WM_MDICREATE" + case WM_MDIDESTROY: + return "WM_MDIDESTROY" + case WM_MDIGETACTIVE: + return "WM_MDIGETACTIVE" + case WM_MDIICONARRANGE: + return "WM_MDIICONARRANGE" + case WM_MDIMAXIMIZE: + return "WM_MDIMAXIMIZE" + case WM_MDINEXT: + return "WM_MDINEXT" + case WM_MDIREFRESHMENU: + return "WM_MDIREFRESHMENU" + case WM_MDIRESTORE: + return "WM_MDIRESTORE" + case WM_MDISETMENU: + return "WM_MDISETMENU" + case WM_MDITILE: + return "WM_MDITILE" + case WM_MEASUREITEM: + return "WM_MEASUREITEM" + case WM_GETOBJECT: + return "WM_GETOBJECT" + case WM_CHANGEUISTATE: + return "WM_CHANGEUISTATE" + case WM_UPDATEUISTATE: + return "WM_UPDATEUISTATE" + case WM_QUERYUISTATE: + return "WM_QUERYUISTATE" + case WM_UNINITMENUPOPUP: + return "WM_UNINITMENUPOPUP" + case WM_MENURBUTTONUP: + return "WM_MENURBUTTONUP" + case WM_MENUCOMMAND: + return "WM_MENUCOMMAND" + case WM_MENUGETOBJECT: + return "WM_MENUGETOBJECT" + case WM_MENUDRAG: + return "WM_MENUDRAG" + case WM_APPCOMMAND: + return "WM_APPCOMMAND" + case WM_MENUCHAR: + return "WM_MENUCHAR" + case WM_MENUSELECT: + return "WM_MENUSELECT" + case WM_MOVE: + return "WM_MOVE" + case WM_MOVING: + return "WM_MOVING" + case WM_NCACTIVATE: + return "WM_NCACTIVATE" + case WM_NCCALCSIZE: + return "WM_NCCALCSIZE" + case WM_NCCREATE: + return "WM_NCCREATE" + case WM_NCDESTROY: + return "WM_NCDESTROY" + case WM_NCHITTEST: + return "WM_NCHITTEST" + case WM_NCLBUTTONDBLCLK: + return "WM_NCLBUTTONDBLCLK" + case WM_NCLBUTTONDOWN: + return "WM_NCLBUTTONDOWN" + case WM_NCLBUTTONUP: + return "WM_NCLBUTTONUP" + case WM_NCMBUTTONDBLCLK: + return "WM_NCMBUTTONDBLCLK" + case WM_NCMBUTTONDOWN: + return "WM_NCMBUTTONDOWN" + case WM_NCMBUTTONUP: + return "WM_NCMBUTTONUP" + case WM_NCXBUTTONDOWN: + return "WM_NCXBUTTONDOWN" + case WM_NCXBUTTONUP: + return "WM_NCXBUTTONUP" + case WM_NCXBUTTONDBLCLK: + return "WM_NCXBUTTONDBLCLK" + case WM_NCMOUSEHOVER: + return "WM_NCMOUSEHOVER" + case WM_NCMOUSELEAVE: + return "WM_NCMOUSELEAVE" + case WM_NCMOUSEMOVE: + return "WM_NCMOUSEMOVE" + case WM_NCPAINT: + return "WM_NCPAINT" + case WM_NCRBUTTONDBLCLK: + return "WM_NCRBUTTONDBLCLK" + case WM_NCRBUTTONDOWN: + return "WM_NCRBUTTONDOWN" + case WM_NCRBUTTONUP: + return "WM_NCRBUTTONUP" + case WM_NEXTDLGCTL: + return "WM_NEXTDLGCTL" + case WM_NEXTMENU: + return "WM_NEXTMENU" + case WM_NOTIFY: + return "WM_NOTIFY" + case WM_NOTIFYFORMAT: + return "WM_NOTIFYFORMAT" + case WM_NULL: + return "WM_NULL" + case WM_PAINT: + return "WM_PAINT" + case WM_PAINTCLIPBOARD: + return "WM_PAINTCLIPBOARD" + case WM_PAINTICON: + return "WM_PAINTICON" + case WM_PALETTECHANGED: + return "WM_PALETTECHANGED" + case WM_PALETTEISCHANGING: + return "WM_PALETTEISCHANGING" + case WM_PARENTNOTIFY: + return "WM_PARENTNOTIFY" + case WM_PASTE: + return "WM_PASTE" + case WM_PENWINFIRST: + return "WM_PENWINFIRST" + case WM_PENWINLAST: + return "WM_PENWINLAST" + case WM_POWER: + return "WM_POWER" + case WM_PRINT: + return "WM_PRINT" + case WM_PRINTCLIENT: + return "WM_PRINTCLIENT" + case WM_QUERYDRAGICON: + return "WM_QUERYDRAGICON" + case WM_QUERYENDSESSION: + return "WM_QUERYENDSESSION" + case WM_QUERYNEWPALETTE: + return "WM_QUERYNEWPALETTE" + case WM_QUERYOPEN: + return "WM_QUERYOPEN" + case WM_QUEUESYNC: + return "WM_QUEUESYNC" + case WM_QUIT: + return "WM_QUIT" + case WM_RENDERALLFORMATS: + return "WM_RENDERALLFORMATS" + case WM_RENDERFORMAT: + return "WM_RENDERFORMAT" + case WM_SETCURSOR: + return "WM_SETCURSOR" + case WM_SETFOCUS: + return "WM_SETFOCUS" + case WM_SETFONT: + return "WM_SETFONT" + case WM_SETHOTKEY: + return "WM_SETHOTKEY" + case WM_SETICON: + return "WM_SETICON" + case WM_SETREDRAW: + return "WM_SETREDRAW" + case WM_SETTEXT: + return "WM_SETTEXT" + case WM_SETTINGCHANGE: + return "WM_SETTINGCHANGE" + case WM_SHOWWINDOW: + return "WM_SHOWWINDOW" + case WM_SIZE: + return "WM_SIZE" + case WM_SIZECLIPBOARD: + return "WM_SIZECLIPBOARD" + case WM_SIZING: + return "WM_SIZING" + case WM_SPOOLERSTATUS: + return "WM_SPOOLERSTATUS" + case WM_STYLECHANGED: + return "WM_STYLECHANGED" + case WM_STYLECHANGING: + return "WM_STYLECHANGING" + case WM_SYSCHAR: + return "WM_SYSCHAR" + case WM_SYSCOLORCHANGE: + return "WM_SYSCOLORCHANGE" + case WM_SYSCOMMAND: + return "WM_SYSCOMMAND" + case WM_SYSDEADCHAR: + return "WM_SYSDEADCHAR" + case WM_SYSKEYDOWN: + return "WM_SYSKEYDOWN" + case WM_SYSKEYUP: + return "WM_SYSKEYUP" + case WM_TCARD: + return "WM_TCARD" + case WM_THEMECHANGED: + return "WM_THEMECHANGED" + case WM_TIMECHANGE: + return "WM_TIMECHANGE" + case WM_TIMER: + return "WM_TIMER" + case WM_UNDO: + return "WM_UNDO" + case WM_USER: + return "WM_USER" + case WM_USERCHANGED: + return "WM_USERCHANGED" + case WM_VKEYTOITEM: + return "WM_VKEYTOITEM" + case WM_VSCROLL: + return "WM_VSCROLL" + case WM_VSCROLLCLIPBOARD: + return "WM_VSCROLLCLIPBOARD" + case WM_WINDOWPOSCHANGED: + return "WM_WINDOWPOSCHANGED" + case WM_WINDOWPOSCHANGING: + return "WM_WINDOWPOSCHANGING" + case WM_KEYLAST: + return "WM_KEYLAST" + case WM_SYNCPAINT: + return "WM_SYNCPAINT" + case WM_MOUSEACTIVATE: + return "WM_MOUSEACTIVATE" + case WM_MOUSEMOVE: + return "WM_MOUSEMOVE" + case WM_LBUTTONDOWN: + return "WM_LBUTTONDOWN" + case WM_LBUTTONUP: + return "WM_LBUTTONUP" + case WM_LBUTTONDBLCLK: + return "WM_LBUTTONDBLCLK" + case WM_RBUTTONDOWN: + return "WM_RBUTTONDOWN" + case WM_RBUTTONUP: + return "WM_RBUTTONUP" + case WM_RBUTTONDBLCLK: + return "WM_RBUTTONDBLCLK" + case WM_MBUTTONDOWN: + return "WM_MBUTTONDOWN" + case WM_MBUTTONUP: + return "WM_MBUTTONUP" + case WM_MBUTTONDBLCLK: + return "WM_MBUTTONDBLCLK" + case WM_MOUSEWHEEL: + return "WM_MOUSEWHEEL" + case WM_XBUTTONDOWN: + return "WM_XBUTTONDOWN" + case WM_XBUTTONUP: + return "WM_XBUTTONUP" + case WM_MOUSELAST: + return "WM_MOUSELAST" + case WM_MOUSEHOVER: + return "WM_MOUSEHOVER" + case WM_MOUSELEAVE: + return "WM_MOUSELEAVE" + case WM_CLIPBOARDUPDATE: + return "WM_CLIPBOARDUPDATE" + default: + return fmt.Sprintf("0x%08x", msg) + } +} diff --git a/v3/pkg/w32/window.go b/v3/pkg/w32/window.go index fec225cf..7a60d650 100644 --- a/v3/pkg/w32/window.go +++ b/v3/pkg/w32/window.go @@ -1,4 +1,5 @@ //go:build windows + package w32 import ( @@ -6,6 +7,7 @@ import ( "github.com/samber/lo" "log" "strconv" + "sync" "syscall" "unsafe" ) @@ -162,3 +164,50 @@ func GetMonitorInfoForWindow(hwnd HWND) *MONITORINFO { GetMonitorInfo(currentMonitor, &info) return &info } + +type WindowProc func(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr + +var windowClasses = make(map[string]HINSTANCE) +var windowClassesLock sync.Mutex + +func getWindowClass(name string) (HINSTANCE, bool) { + windowClassesLock.Lock() + defer windowClassesLock.Unlock() + result, exists := windowClasses[name] + return result, exists +} + +func setWindowClass(name string, instance HINSTANCE) { + windowClassesLock.Lock() + defer windowClassesLock.Unlock() + windowClasses[name] = instance +} + +func RegisterWindow(name string, proc WindowProc) (HINSTANCE, error) { + classInstance, exists := getWindowClass(name) + if exists { + return classInstance, nil + } + applicationInstance := GetModuleHandle("") + if applicationInstance == 0 { + return 0, fmt.Errorf("get module handle failed") + } + + var wc WNDCLASSEX + wc.Size = uint32(unsafe.Sizeof(wc)) + wc.WndProc = syscall.NewCallback(proc) + wc.Instance = applicationInstance + wc.Icon = LoadIconWithResourceID(0, uint16(IDI_APPLICATION)) + wc.Cursor = LoadCursorWithResourceID(0, uint16(IDC_ARROW)) + wc.Background = COLOR_BTNFACE + 1 + wc.ClassName = MustStringToUTF16Ptr(name) + + atom := RegisterClassEx(&wc) + if atom == 0 { + panic(syscall.GetLastError()) + } + + setWindowClass(name, applicationInstance) + + return applicationInstance, nil +}