//go:build windows // +build windows package systray import ( "crypto/md5" "encoding/hex" "errors" "io/ioutil" "log" "os" "path/filepath" "sort" "sync" "syscall" "unsafe" "github.com/tevino/abool" "golang.org/x/sys/windows" ) // Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32 var ( g32 = windows.NewLazySystemDLL("Gdi32.dll") pCreateCompatibleBitmap = g32.NewProc("CreateCompatibleBitmap") pCreateCompatibleDC = g32.NewProc("CreateCompatibleDC") pCreateDIBSection = g32.NewProc("CreateDIBSection") pDeleteDC = g32.NewProc("DeleteDC") pSelectObject = g32.NewProc("SelectObject") k32 = windows.NewLazySystemDLL("Kernel32.dll") pGetModuleHandle = k32.NewProc("GetModuleHandleW") s32 = windows.NewLazySystemDLL("Shell32.dll") pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW") u32 = windows.NewLazySystemDLL("User32.dll") pCreateMenu = u32.NewProc("CreateMenu") pCreatePopupMenu = u32.NewProc("CreatePopupMenu") pCreateWindowEx = u32.NewProc("CreateWindowExW") pDefWindowProc = u32.NewProc("DefWindowProcW") pDeleteMenu = u32.NewProc("DeleteMenu") pDestroyMenu = u32.NewProc("DestroyMenu") pRemoveMenu = u32.NewProc("RemoveMenu") pDestroyWindow = u32.NewProc("DestroyWindow") pDispatchMessage = u32.NewProc("DispatchMessageW") pDrawIconEx = u32.NewProc("DrawIconEx") pGetCursorPos = u32.NewProc("GetCursorPos") pGetDC = u32.NewProc("GetDC") pGetMessage = u32.NewProc("GetMessageW") pGetSystemMetrics = u32.NewProc("GetSystemMetrics") pInsertMenuItem = u32.NewProc("InsertMenuItemW") pLoadCursor = u32.NewProc("LoadCursorW") pLoadIcon = u32.NewProc("LoadIconW") pLoadImage = u32.NewProc("LoadImageW") pPostMessage = u32.NewProc("PostMessageW") pPostQuitMessage = u32.NewProc("PostQuitMessage") pRegisterClass = u32.NewProc("RegisterClassExW") pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW") pReleaseDC = u32.NewProc("ReleaseDC") pSetForegroundWindow = u32.NewProc("SetForegroundWindow") pSetMenuInfo = u32.NewProc("SetMenuInfo") pSetMenuItemInfo = u32.NewProc("SetMenuItemInfoW") pShowWindow = u32.NewProc("ShowWindow") pTrackPopupMenu = u32.NewProc("TrackPopupMenu") pTranslateMessage = u32.NewProc("TranslateMessage") pUnregisterClass = u32.NewProc("UnregisterClassW") pUpdateWindow = u32.NewProc("UpdateWindow") // ErrTrayNotReadyYet is returned by functions when they are called before the tray has been initialized. ErrTrayNotReadyYet = errors.New("tray not ready yet") ) // Contains window class information. // It is used with the RegisterClassEx and GetClassInfoEx functions. // https://msdn.microsoft.com/en-us/library/ms633577.aspx type wndClassEx struct { Size, Style uint32 WndProc uintptr ClsExtra, WndExtra int32 Instance, Icon, Cursor, Background windows.Handle MenuName, ClassName *uint16 IconSm windows.Handle } // Registers a window class for subsequent use in calls to the CreateWindow or CreateWindowEx function. // https://msdn.microsoft.com/en-us/library/ms633587.aspx func (w *wndClassEx) register() error { w.Size = uint32(unsafe.Sizeof(*w)) res, _, err := pRegisterClass.Call(uintptr(unsafe.Pointer(w))) if res == 0 { return err } return nil } // Unregisters a window class, freeing the memory required for the class. // https://msdn.microsoft.com/en-us/library/ms644899.aspx func (w *wndClassEx) unregister() error { res, _, err := pUnregisterClass.Call( uintptr(unsafe.Pointer(w.ClassName)), uintptr(w.Instance), ) if res == 0 { return err } return nil } // Contains information that the system needs to display notifications in the notification area. // Used by Shell_NotifyIcon. // https://msdn.microsoft.com/en-us/library/windows/desktop/bb773352(v=vs.85).aspx // https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159 type notifyIconData struct { Size uint32 Wnd windows.Handle ID, Flags, CallbackMessage uint32 Icon windows.Handle Tip [128]uint16 State, StateMask uint32 Info [256]uint16 Timeout, Version uint32 InfoTitle [64]uint16 InfoFlags uint32 GuidItem windows.GUID BalloonIcon windows.Handle } func (nid *notifyIconData) add() error { const NIM_ADD = 0x00000000 res, _, err := pShellNotifyIcon.Call( uintptr(NIM_ADD), uintptr(unsafe.Pointer(nid)), ) if res == 0 { return err } return nil } func (nid *notifyIconData) modify() error { const NIM_MODIFY = 0x00000001 res, _, err := pShellNotifyIcon.Call( uintptr(NIM_MODIFY), uintptr(unsafe.Pointer(nid)), ) if res == 0 { return err } return nil } func (nid *notifyIconData) delete() error { const NIM_DELETE = 0x00000002 res, _, err := pShellNotifyIcon.Call( uintptr(NIM_DELETE), uintptr(unsafe.Pointer(nid)), ) if res == 0 { return err } return nil } // Contains information about a menu item. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx type menuItemInfo struct { Size, Mask, Type, State uint32 ID uint32 SubMenu, Checked, Unchecked windows.Handle ItemData uintptr TypeData *uint16 Cch uint32 BMPItem windows.Handle } // The POINT structure defines the x- and y- coordinates of a point. // https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx type point struct { X, Y int32 } // The BITMAPINFO structure defines the dimensions and color information for a DIB. // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo type bitmapInfo struct { BmiHeader bitmapInfoHeader BmiColors windows.Handle } // The BITMAPINFOHEADER structure contains information about the dimensions and color format of a device-independent bitmap (DIB). // https://learn.microsoft.com/en-us/previous-versions/dd183376(v=vs.85) // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader type bitmapInfoHeader struct { BiSize uint32 BiWidth int32 BiHeight int32 BiPlanes uint16 BiBitCount uint16 BiCompression uint32 BiSizeImage uint32 BiXPelsPerMeter int32 BiYPelsPerMeter int32 BiClrUsed uint32 BiClrImportant uint32 } // Contains information about loaded resources type winTray struct { instance, icon, cursor, window windows.Handle loadedImages map[string]windows.Handle muLoadedImages sync.RWMutex // menus keeps track of the submenus keyed by the menu item ID, plus 0 // which corresponds to the main popup menu. menus map[uint32]windows.Handle muMenus sync.RWMutex // menuOf keeps track of the menu each menu item belongs to. menuOf map[uint32]windows.Handle muMenuOf sync.RWMutex // menuItemIcons maintains the bitmap of each menu item (if applies). It's // needed to show the icon correctly when showing a previously hidden menu // item again. menuItemIcons map[uint32]windows.Handle muMenuItemIcons sync.RWMutex visibleItems map[uint32][]uint32 muVisibleItems sync.RWMutex nid *notifyIconData muNID sync.RWMutex wcex *wndClassEx wmSystrayMessage, wmTaskbarCreated uint32 initialized *abool.AtomicBool } // isReady checks if the tray as already been initialized. It is not goroutine safe with in regard to the initialization function, but prevents a panic when functions are called too early. func (t *winTray) isReady() bool { return t.initialized.IsSet() } // Loads an image from file and shows it in tray. // Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx func (t *winTray) setIcon(src string) error { if !wt.isReady() { return ErrTrayNotReadyYet } const NIF_ICON = 0x00000002 h, err := t.loadIconFrom(src) if err != nil { return err } t.muNID.Lock() defer t.muNID.Unlock() t.nid.Icon = h t.nid.Flags |= NIF_ICON t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) return t.nid.modify() } // Sets tooltip on icon. // Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx func (t *winTray) setTooltip(src string) error { if !wt.isReady() { return ErrTrayNotReadyYet } const NIF_TIP = 0x00000004 b, err := windows.UTF16FromString(src) if err != nil { return err } t.muNID.Lock() defer t.muNID.Unlock() copy(t.nid.Tip[:], b[:]) t.nid.Flags |= NIF_TIP t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) return t.nid.modify() } var wt = winTray{ initialized: abool.New(), } // WindowProc callback function that processes messages sent to a window. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) { const ( WM_RBUTTONUP = 0x0205 WM_LBUTTONUP = 0x0202 WM_COMMAND = 0x0111 WM_ENDSESSION = 0x0016 WM_CLOSE = 0x0010 WM_DESTROY = 0x0002 ) switch message { case WM_COMMAND: menuItemId := int32(wParam) // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus if menuItemId != -1 { systrayMenuItemSelected(uint32(wParam)) } case WM_CLOSE: pDestroyWindow.Call(uintptr(t.window)) t.wcex.unregister() case WM_DESTROY: // same as WM_ENDSESSION, but throws 0 exit code after all defer pPostQuitMessage.Call(uintptr(int32(0))) fallthrough case WM_ENDSESSION: t.muNID.Lock() if t.nid != nil { t.nid.delete() } t.muNID.Unlock() runSystrayExit() case t.wmSystrayMessage: switch lParam { case WM_RBUTTONUP, WM_LBUTTONUP: t.showMenu() } case t.wmTaskbarCreated: // on explorer.exe restarts t.muNID.Lock() t.nid.add() t.muNID.Unlock() default: // Calls the default window procedure to provide default processing for any window messages that an application does not process. // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx lResult, _, _ = pDefWindowProc.Call( uintptr(hWnd), uintptr(message), uintptr(wParam), uintptr(lParam), ) } return } func (t *winTray) initInstance() error { const IDI_APPLICATION = 32512 const IDC_ARROW = 32512 // Standard arrow // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633548(v=vs.85).aspx const SW_HIDE = 0 const CW_USEDEFAULT = 0x80000000 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx const ( WS_CAPTION = 0x00C00000 WS_MAXIMIZEBOX = 0x00010000 WS_MINIMIZEBOX = 0x00020000 WS_OVERLAPPED = 0x00000000 WS_SYSMENU = 0x00080000 WS_THICKFRAME = 0x00040000 WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX ) // https://msdn.microsoft.com/en-us/library/windows/desktop/ff729176 const ( CS_HREDRAW = 0x0002 CS_VREDRAW = 0x0001 ) const NIF_MESSAGE = 0x00000001 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644931(v=vs.85).aspx const WM_USER = 0x0400 const ( className = "SystrayClass" windowName = "" ) t.wmSystrayMessage = WM_USER + 1 t.visibleItems = make(map[uint32][]uint32) t.menus = make(map[uint32]windows.Handle) t.menuOf = make(map[uint32]windows.Handle) t.menuItemIcons = make(map[uint32]windows.Handle) taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated") // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947 res, _, err := pRegisterWindowMessage.Call( uintptr(unsafe.Pointer(taskbarEventNamePtr)), ) t.wmTaskbarCreated = uint32(res) t.loadedImages = make(map[string]windows.Handle) instanceHandle, _, err := pGetModuleHandle.Call(0) if instanceHandle == 0 { return err } t.instance = windows.Handle(instanceHandle) // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648072(v=vs.85).aspx iconHandle, _, err := pLoadIcon.Call(0, uintptr(IDI_APPLICATION)) if iconHandle == 0 { return err } t.icon = windows.Handle(iconHandle) // https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391(v=vs.85).aspx cursorHandle, _, err := pLoadCursor.Call(0, uintptr(IDC_ARROW)) if cursorHandle == 0 { return err } t.cursor = windows.Handle(cursorHandle) classNamePtr, err := windows.UTF16PtrFromString(className) if err != nil { return err } windowNamePtr, err := windows.UTF16PtrFromString(windowName) if err != nil { return err } t.wcex = &wndClassEx{ Style: CS_HREDRAW | CS_VREDRAW, WndProc: windows.NewCallback(t.wndProc), Instance: t.instance, Icon: t.icon, Cursor: t.cursor, Background: windows.Handle(6), // (COLOR_WINDOW + 1) ClassName: classNamePtr, IconSm: t.icon, } if err := t.wcex.register(); err != nil { return err } windowHandle, _, err := pCreateWindowEx.Call( uintptr(0), uintptr(unsafe.Pointer(classNamePtr)), uintptr(unsafe.Pointer(windowNamePtr)), uintptr(WS_OVERLAPPEDWINDOW), uintptr(CW_USEDEFAULT), uintptr(CW_USEDEFAULT), uintptr(CW_USEDEFAULT), uintptr(CW_USEDEFAULT), uintptr(0), uintptr(0), uintptr(t.instance), uintptr(0), ) if windowHandle == 0 { return err } t.window = windows.Handle(windowHandle) pShowWindow.Call( uintptr(t.window), uintptr(SW_HIDE), ) pUpdateWindow.Call( uintptr(t.window), ) t.muNID.Lock() defer t.muNID.Unlock() t.nid = ¬ifyIconData{ Wnd: windows.Handle(t.window), ID: 100, Flags: NIF_MESSAGE, CallbackMessage: t.wmSystrayMessage, } t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) return t.nid.add() } func (t *winTray) createMenu() error { const MIM_APPLYTOSUBMENUS = 0x80000000 // Settings apply to the menu and all of its submenus menuHandle, _, err := pCreatePopupMenu.Call() if menuHandle == 0 { return err } t.menus[0] = windows.Handle(menuHandle) // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx mi := struct { Size, Mask, Style, Max uint32 Background windows.Handle ContextHelpID uint32 MenuData uintptr }{ Mask: MIM_APPLYTOSUBMENUS, } mi.Size = uint32(unsafe.Sizeof(mi)) res, _, err := pSetMenuInfo.Call( uintptr(t.menus[0]), uintptr(unsafe.Pointer(&mi)), ) if res == 0 { return err } return nil } func (t *winTray) convertToSubMenu(menuItemId uint32) (windows.Handle, error) { const MIIM_SUBMENU = 0x00000004 res, _, err := pCreateMenu.Call() if res == 0 { return 0, err } menu := windows.Handle(res) mi := menuItemInfo{Mask: MIIM_SUBMENU, SubMenu: menu} mi.Size = uint32(unsafe.Sizeof(mi)) t.muMenuOf.RLock() hMenu := t.menuOf[menuItemId] t.muMenuOf.RUnlock() res, _, err = pSetMenuItemInfo.Call( uintptr(hMenu), uintptr(menuItemId), 0, uintptr(unsafe.Pointer(&mi)), ) if res == 0 { return 0, err } t.muMenus.Lock() t.menus[menuItemId] = menu t.muMenus.Unlock() return menu, nil } func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled, checked bool) error { if !wt.isReady() { return ErrTrayNotReadyYet } // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx const ( MIIM_FTYPE = 0x00000100 MIIM_BITMAP = 0x00000080 MIIM_STRING = 0x00000040 MIIM_SUBMENU = 0x00000004 MIIM_ID = 0x00000002 MIIM_STATE = 0x00000001 ) const MFT_STRING = 0x00000000 const ( MFS_CHECKED = 0x00000008 MFS_DISABLED = 0x00000003 ) titlePtr, err := windows.UTF16PtrFromString(title) if err != nil { return err } mi := menuItemInfo{ Mask: MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE, Type: MFT_STRING, ID: uint32(menuItemId), TypeData: titlePtr, Cch: uint32(len(title)), } mi.Size = uint32(unsafe.Sizeof(mi)) if disabled { mi.State |= MFS_DISABLED } if checked { mi.State |= MFS_CHECKED } t.muMenuItemIcons.RLock() hIcon := t.menuItemIcons[menuItemId] t.muMenuItemIcons.RUnlock() if hIcon > 0 { mi.Mask |= MIIM_BITMAP mi.BMPItem = hIcon } var res uintptr t.muMenus.RLock() menu, exists := t.menus[parentId] t.muMenus.RUnlock() if !exists { menu, err = t.convertToSubMenu(parentId) if err != nil { return err } t.muMenus.Lock() t.menus[parentId] = menu t.muMenus.Unlock() } else if t.getVisibleItemIndex(parentId, menuItemId) != -1 { // We set the menu item info based on the menuID res, _, err = pSetMenuItemInfo.Call( uintptr(menu), uintptr(menuItemId), 0, uintptr(unsafe.Pointer(&mi)), ) } if res == 0 { // Menu item does not already exist, create it t.muMenus.RLock() submenu, exists := t.menus[menuItemId] t.muMenus.RUnlock() if exists { mi.Mask |= MIIM_SUBMENU mi.SubMenu = submenu } t.addToVisibleItems(parentId, menuItemId) position := t.getVisibleItemIndex(parentId, menuItemId) res, _, err = pInsertMenuItem.Call( uintptr(menu), uintptr(position), 1, uintptr(unsafe.Pointer(&mi)), ) if res == 0 { t.delFromVisibleItems(parentId, menuItemId) return err } t.muMenuOf.Lock() t.menuOf[menuItemId] = menu t.muMenuOf.Unlock() } return nil } func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error { if !wt.isReady() { return ErrTrayNotReadyYet } // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx const ( MIIM_FTYPE = 0x00000100 MIIM_ID = 0x00000002 MIIM_STATE = 0x00000001 ) const MFT_SEPARATOR = 0x00000800 mi := menuItemInfo{ Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE, Type: MFT_SEPARATOR, ID: uint32(menuItemId), } mi.Size = uint32(unsafe.Sizeof(mi)) t.addToVisibleItems(parentId, menuItemId) position := t.getVisibleItemIndex(parentId, menuItemId) t.muMenus.RLock() menu := uintptr(t.menus[parentId]) t.muMenus.RUnlock() res, _, err := pInsertMenuItem.Call( menu, uintptr(position), 1, uintptr(unsafe.Pointer(&mi)), ) if res == 0 { return err } return nil } func (t *winTray) removeMenuItem(menuItemId, parentId uint32) error { if !wt.isReady() { return ErrTrayNotReadyYet } const MF_BYCOMMAND = 0x00000000 const ERROR_SUCCESS syscall.Errno = 0 t.muMenus.RLock() menu := uintptr(t.menus[parentId]) t.muMenus.RUnlock() res, _, err := pDeleteMenu.Call( menu, uintptr(menuItemId), MF_BYCOMMAND, ) if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { return err } t.delFromVisibleItems(parentId, menuItemId) return nil } func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error { if !wt.isReady() { return ErrTrayNotReadyYet } const MF_BYCOMMAND = 0x00000000 const ERROR_SUCCESS syscall.Errno = 0 t.muMenus.RLock() menu := uintptr(t.menus[parentId]) t.muMenus.RUnlock() res, _, err := pRemoveMenu.Call( menu, uintptr(menuItemId), MF_BYCOMMAND, ) if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { return err } t.delFromVisibleItems(parentId, menuItemId) return nil } func (t *winTray) showMenu() error { if !wt.isReady() { return ErrTrayNotReadyYet } const ( TPM_BOTTOMALIGN = 0x0020 TPM_LEFTALIGN = 0x0000 ) p := point{} res, _, err := pGetCursorPos.Call(uintptr(unsafe.Pointer(&p))) if res == 0 { return err } pSetForegroundWindow.Call(uintptr(t.window)) res, _, err = pTrackPopupMenu.Call( uintptr(t.menus[0]), TPM_BOTTOMALIGN|TPM_LEFTALIGN, uintptr(p.X), uintptr(p.Y), 0, uintptr(t.window), 0, ) if res == 0 { return err } return nil } func (t *winTray) delFromVisibleItems(parent, val uint32) { t.muVisibleItems.Lock() defer t.muVisibleItems.Unlock() visibleItems := t.visibleItems[parent] for i, itemval := range visibleItems { if val == itemval { t.visibleItems[parent] = append(visibleItems[:i], visibleItems[i+1:]...) break } } } func (t *winTray) addToVisibleItems(parent, val uint32) { t.muVisibleItems.Lock() defer t.muVisibleItems.Unlock() if visibleItems, exists := t.visibleItems[parent]; !exists { t.visibleItems[parent] = []uint32{val} } else { newvisible := append(visibleItems, val) sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] }) t.visibleItems[parent] = newvisible } } func (t *winTray) getVisibleItemIndex(parent, val uint32) int { t.muVisibleItems.RLock() defer t.muVisibleItems.RUnlock() for i, itemval := range t.visibleItems[parent] { if val == itemval { return i } } return -1 } // Loads an image from file to be shown in tray or menu item. // LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx func (t *winTray) loadIconFrom(src string) (windows.Handle, error) { if !wt.isReady() { return 0, ErrTrayNotReadyYet } const IMAGE_ICON = 1 // Loads an icon const LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file const LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero // Save and reuse handles of loaded images t.muLoadedImages.RLock() h, ok := t.loadedImages[src] t.muLoadedImages.RUnlock() if !ok { srcPtr, err := windows.UTF16PtrFromString(src) if err != nil { return 0, err } res, _, err := pLoadImage.Call( 0, uintptr(unsafe.Pointer(srcPtr)), IMAGE_ICON, 0, 0, LR_LOADFROMFILE|LR_DEFAULTSIZE, ) if res == 0 { return 0, err } h = windows.Handle(res) t.muLoadedImages.Lock() t.loadedImages[src] = h t.muLoadedImages.Unlock() } return h, nil } func iconToBitmap(hIcon windows.Handle) (windows.Handle, error) { const SM_CXSMICON = 49 const SM_CYSMICON = 50 const DI_NORMAL = 0x3 hDC, _, err := pGetDC.Call(uintptr(0)) if hDC == 0 { return 0, err } defer pReleaseDC.Call(uintptr(0), hDC) hMemDC, _, err := pCreateCompatibleDC.Call(hDC) if hMemDC == 0 { return 0, err } defer pDeleteDC.Call(hMemDC) cx, _, _ := pGetSystemMetrics.Call(SM_CXSMICON) cy, _, _ := pGetSystemMetrics.Call(SM_CYSMICON) hMemBmp, err := create32BitHBitmap(hMemDC, int32(cx), int32(cy)) hOriginalBmp, _, _ := pSelectObject.Call(hMemDC, hMemBmp) defer pSelectObject.Call(hMemDC, hOriginalBmp) res, _, err := pDrawIconEx.Call(hMemDC, 0, 0, uintptr(hIcon), cx, cy, 0, uintptr(0), DI_NORMAL) if res == 0 { return 0, err } return windows.Handle(hMemBmp), nil } // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createdibsection func create32BitHBitmap(hDC uintptr, cx, cy int32) (uintptr, error) { const BI_RGB uint32 = 0 const DIB_RGB_COLORS = 0 bmi := bitmapInfo{ BmiHeader: bitmapInfoHeader{ BiPlanes: 1, BiCompression: BI_RGB, BiWidth: cx, BiHeight: cy, BiBitCount: 32, }, } bmi.BmiHeader.BiSize = uint32(unsafe.Sizeof(bmi.BmiHeader)) var bits uintptr hBitmap, _, err := pCreateDIBSection.Call( hDC, uintptr(unsafe.Pointer(&bmi)), DIB_RGB_COLORS, uintptr(unsafe.Pointer(&bits)), uintptr(0), 0, ) if hBitmap == 0 { return 0, err } return hBitmap, nil } func registerSystray() { if err := wt.initInstance(); err != nil { log.Printf("systray error: unable to init instance: %s\n", err) return } if err := wt.createMenu(); err != nil { log.Printf("systray error: unable to create menu: %s\n", err) return } wt.initialized.Set() systrayReady() } var m = &struct { WindowHandle windows.Handle Message uint32 Wparam uintptr Lparam uintptr Time uint32 Pt point }{} func nativeLoop() { for doNativeTick() { } } func nativeEnd() { } func nativeStart() { go func() { for doNativeTick() { } }() } func doNativeTick() bool { ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0) // If the function retrieves a message other than WM_QUIT, the return value is nonzero. // If the function retrieves the WM_QUIT message, the return value is zero. // If there is an error, the return value is -1 // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx switch int32(ret) { case -1: log.Printf("systray error: message loop failure: %s\n", err) return false case 0: return false default: pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) } return true } func quit() { const WM_CLOSE = 0x0010 pPostMessage.Call( uintptr(wt.window), WM_CLOSE, 0, 0, ) wt.muNID.Lock() if wt.nid != nil { wt.nid.delete() } wt.muNID.Unlock() runSystrayExit() } func setInternalLoop(bool) { } func iconBytesToFilePath(iconBytes []byte) (string, error) { bh := md5.Sum(iconBytes) dataHash := hex.EncodeToString(bh[:]) iconFilePath := filepath.Join(os.TempDir(), "systray_temp_icon_"+dataHash) if _, err := os.Stat(iconFilePath); os.IsNotExist(err) { if err := ioutil.WriteFile(iconFilePath, iconBytes, 0644); err != nil { return "", err } } return iconFilePath, nil } // SetIcon sets the systray icon. // iconBytes should be the content of .ico for windows and .ico/.jpg/.png // for other platforms. func SetIcon(iconBytes []byte) { iconFilePath, err := iconBytesToFilePath(iconBytes) if err != nil { log.Printf("systray error: unable to write icon data to temp file: %s\n", err) return } if err := wt.setIcon(iconFilePath); err != nil { log.Printf("systray error: unable to set icon: %s\n", err) return } } // SetTemplateIcon sets the systray icon as a template icon (on macOS), falling back // to a regular icon on other platforms. // templateIconBytes and iconBytes should be the content of .ico for windows and // .ico/.jpg/.png for other platforms. func SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { SetIcon(regularIconBytes) } // SetTitle sets the systray title, only available on Mac and Linux. func SetTitle(title string) { // do nothing } func (item *MenuItem) parentId() uint32 { if item.parent != nil { return uint32(item.parent.id) } return 0 } // SetIcon sets the icon of a menu item. Only works on macOS and Windows. // iconBytes should be the content of .ico/.jpg/.png func (item *MenuItem) SetIcon(iconBytes []byte) { iconFilePath, err := iconBytesToFilePath(iconBytes) if err != nil { log.Printf("systray error: unable to write icon data to temp file: %s\n", err) return } h, err := wt.loadIconFrom(iconFilePath) if err != nil { log.Printf("systray error: unable to load icon from temp file: %s\n", err) return } h, err = iconToBitmap(h) if err != nil { log.Printf("systray error: unable to convert icon to bitmap: %s\n", err) return } wt.muMenuItemIcons.Lock() wt.menuItemIcons[uint32(item.id)] = h wt.muMenuItemIcons.Unlock() err = wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked) if err != nil { log.Printf("systray error: unable to addOrUpdateMenuItem: %s\n", err) return } } // SetTooltip sets the systray tooltip to display on mouse hover of the tray icon, // only available on Mac and Windows. func SetTooltip(tooltip string) { if err := wt.setTooltip(tooltip); err != nil { log.Printf("systray error: unable to set tooltip: %s\n", err) return } } func addOrUpdateMenuItem(item *MenuItem) { err := wt.addOrUpdateMenuItem(uint32(item.id), item.parentId(), item.title, item.disabled, item.checked) if err != nil { log.Printf("systray error: unable to addOrUpdateMenuItem: %s\n", err) return } } // SetTemplateIcon sets the icon of a menu item as a template icon (on macOS). On Windows, it // falls back to the regular icon bytes and on Linux it does nothing. // templateIconBytes and regularIconBytes should be the content of .ico for windows and // .ico/.jpg/.png for other platforms. func (item *MenuItem) SetTemplateIcon(templateIconBytes []byte, regularIconBytes []byte) { item.SetIcon(regularIconBytes) } func addSeparator(id uint32, parent uint32) { err := wt.addSeparatorMenuItem(id, parent) if err != nil { log.Printf("systray error: unable to addSeparator: %s\n", err) return } } func hideMenuItem(item *MenuItem) { err := wt.hideMenuItem(uint32(item.id), item.parentId()) if err != nil { log.Printf("systray error: unable to hideMenuItem: %s\n", err) return } } func removeMenuItem(item *MenuItem) { err := wt.removeMenuItem(uint32(item.id), item.parentId()) if err != nil { log.Printf("systray error: unable to removeMenuItem: %s\n", err) return } } func showMenuItem(item *MenuItem) { addOrUpdateMenuItem(item) } func resetMenu() { _, _, _ = pDestroyMenu.Call(uintptr(wt.menus[0])) wt.visibleItems = make(map[uint32][]uint32) wt.menus = make(map[uint32]windows.Handle) wt.menuOf = make(map[uint32]windows.Handle) wt.menuItemIcons = make(map[uint32]windows.Handle) wt.createMenu() }