Last active
August 20, 2021 14:39
-
-
Save jasonbot/5759510 to your computer and use it in GitHub Desktop.
A class to create/manage a raw Windows Tray Icon for an app, with popup menus
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ctypes | |
import ctypes.wintypes | |
import os | |
import threading | |
import Queue | |
import uuid | |
__all__ = ['NotificationIcon'] | |
# Create popup menu | |
CreatePopupMenu = ctypes.windll.user32.CreatePopupMenu | |
CreatePopupMenu.restype = ctypes.wintypes.HMENU | |
CreatePopupMenu.argtypes = [] | |
MF_BYCOMMAND = 0x0 | |
MF_BYPOSITION = 0x400 | |
MF_BITMAP = 0x4 | |
MF_CHECKED = 0x8 | |
MF_DISABLED = 0x2 | |
MF_ENABLED = 0x0 | |
MF_GRAYED = 0x1 | |
MF_MENUBARBREAK = 0x20 | |
MF_MENUBREAK = 0x40 | |
MF_OWNERDRAW = 0x100 | |
MF_POPUP = 0x10 | |
MF_SEPARATOR = 0x800 | |
MF_STRING = 0x0 | |
MF_UNCHECKED = 0x0 | |
InsertMenu = ctypes.windll.user32.InsertMenuW | |
InsertMenu.restype = ctypes.wintypes.BOOL | |
InsertMenu.argtypes = [ctypes.wintypes.HMENU, ctypes.wintypes.UINT, ctypes.wintypes.UINT, ctypes.wintypes.UINT, ctypes.wintypes.LPCWSTR] | |
AppendMenu = ctypes.windll.user32.AppendMenuW | |
AppendMenu.restype = ctypes.wintypes.BOOL | |
AppendMenu.argtypes = [ctypes.wintypes.HMENU, ctypes.wintypes.UINT, ctypes.wintypes.UINT, ctypes.wintypes.LPCWSTR] | |
SetMenuDefaultItem = ctypes.windll.user32.SetMenuDefaultItem | |
SetMenuDefaultItem.restype = ctypes.wintypes.BOOL | |
SetMenuDefaultItem.argtypes = [ctypes.wintypes.HMENU, ctypes.wintypes.UINT, ctypes.wintypes.UINT] | |
#class MENUITEMINFO(ctypes.Structure): | |
# UINT cbSize; | |
# UINT fMask; | |
# UINT fType; | |
# UINT fState; | |
# UINT wID; | |
# HMENU hSubMenu; | |
# HBITMAP hbmpChecked; | |
# HBITMAP hbmpUnchecked; | |
# ULONG_PTR dwItemData; | |
# LPTSTR dwTypeData; | |
# UINT cch; | |
# HBITMAP hbmpItem; | |
# | |
#BOOL WINAPI InsertMenuItem( | |
# __in HMENU hMenu, | |
# __in UINT uItem, | |
# __in BOOL fByPosition, | |
# __in LPCMENUITEMINFO lpmii | |
#); | |
# | |
class POINT(ctypes.Structure): | |
_fields_ = [ ('x', ctypes.wintypes.LONG), | |
('y', ctypes.wintypes.LONG)] | |
GetCursorPos = ctypes.windll.user32.GetCursorPos | |
GetCursorPos.argtypes = [ctypes.POINTER(POINT)] | |
SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow | |
SetForegroundWindow.argtypes = [ctypes.wintypes.HWND] | |
TPM_LEFTALIGN = 0x0 | |
TPM_CENTERALIGN = 0x4 | |
TPM_RIGHTALIGN = 0x8 | |
TPM_TOPALIGN = 0x0 | |
TPM_VCENTERALIGN = 0x10 | |
TPM_BOTTOMALIGN = 0x20 | |
TPM_NONOTIFY = 0x80 | |
TPM_RETURNCMD = 0x100 | |
TPM_LEFTBUTTON = 0x0 | |
TPM_RIGHTBUTTON = 0x2 | |
TPM_HORNEGANIMATION = 0x800 | |
TPM_HORPOSANIMATION = 0x400 | |
TPM_NOANIMATION = 0x4000 | |
TPM_VERNEGANIMATION = 0x2000 | |
TPM_VERPOSANIMATION = 0x1000 | |
TrackPopupMenu = ctypes.windll.user32.TrackPopupMenu | |
TrackPopupMenu.restype = ctypes.wintypes.BOOL | |
TrackPopupMenu.argtypes = [ctypes.wintypes.HMENU, ctypes.wintypes.UINT, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.wintypes.HWND, ctypes.c_void_p] | |
PostMessage = ctypes.windll.user32.PostMessageW | |
PostMessage.restype = ctypes.wintypes.BOOL | |
PostMessage.argtypes = [ctypes.wintypes.HWND, ctypes.wintypes.UINT, ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM] | |
DestroyMenu = ctypes.windll.user32.DestroyMenu | |
DestroyMenu.restype = ctypes.wintypes.BOOL | |
DestroyMenu.argtypes = [ctypes.wintypes.HMENU] | |
# Create notification icon | |
GUID = ctypes.c_ubyte * 16 | |
class TimeoutVersionUnion(ctypes.Union): | |
_fields_ = [('uTimeout', ctypes.wintypes.UINT), | |
('uVersion', ctypes.wintypes.UINT),] | |
NIS_HIDDEN = 0x1 | |
NIS_SHAREDICON = 0x2 | |
class NOTIFYICONDATA(ctypes.Structure): | |
def __init__(self, *args, **kwargs): | |
super(NOTIFYICONDATA, self).__init__(*args, **kwargs) | |
self.cbSize = ctypes.sizeof(self) | |
_fields_ = [ | |
('cbSize', ctypes.wintypes.DWORD), | |
('hWnd', ctypes.wintypes.HWND), | |
('uID', ctypes.wintypes.UINT), | |
('uFlags', ctypes.wintypes.UINT), | |
('uCallbackMessage', ctypes.wintypes.UINT), | |
('hIcon', ctypes.wintypes.HICON), | |
('szTip', ctypes.wintypes.WCHAR * 64), | |
('dwState', ctypes.wintypes.DWORD), | |
('dwStateMask', ctypes.wintypes.DWORD), | |
('szInfo', ctypes.wintypes.WCHAR * 256), | |
('union', TimeoutVersionUnion), | |
('szInfoTitle', ctypes.wintypes.WCHAR * 64), | |
('dwInfoFlags', ctypes.wintypes.DWORD), | |
('guidItem', GUID), | |
('hBalloonIcon', ctypes.wintypes.HICON), | |
] | |
NIM_ADD = 0 | |
NIM_MODIFY = 1 | |
NIM_DELETE = 2 | |
NIM_SETFOCUS = 3 | |
NIM_SETVERSION = 4 | |
NIF_MESSAGE = 1 | |
NIF_ICON = 2 | |
NIF_TIP = 4 | |
NIF_STATE = 8 | |
NIF_INFO = 16 | |
NIF_GUID = 32 | |
NIF_REALTIME = 64 | |
NIF_SHOWTIP = 128 | |
NIIF_NONE = 0 | |
NIIF_INFO = 1 | |
NIIF_WARNING = 2 | |
NIIF_ERROR = 3 | |
NIIF_USER = 4 | |
NOTIFYICON_VERSION = 3 | |
NOTIFYICON_VERSION_4 = 4 | |
Shell_NotifyIcon = ctypes.windll.shell32.Shell_NotifyIconW | |
Shell_NotifyIcon.restype = ctypes.wintypes.BOOL | |
Shell_NotifyIcon.argtypes = [ctypes.wintypes.DWORD, ctypes.POINTER(NOTIFYICONDATA)] | |
# Load icon/image | |
IMAGE_BITMAP = 0 | |
IMAGE_ICON = 1 | |
IMAGE_CURSOR = 2 | |
LR_CREATEDIBSECTION = 0x00002000 | |
LR_DEFAULTCOLOR = 0x00000000 | |
LR_DEFAULTSIZE = 0x00000040 | |
LR_LOADFROMFILE = 0x00000010 | |
LR_LOADMAP3DCOLORS = 0x00001000 | |
LR_LOADTRANSPARENT = 0x00000020 | |
LR_MONOCHROME = 0x00000001 | |
LR_SHARED = 0x00008000 | |
LR_VGACOLOR = 0x00000080 | |
OIC_SAMPLE = 32512 | |
OIC_HAND = 32513 | |
OIC_QUES = 32514 | |
OIC_BANG = 32515 | |
OIC_NOTE = 32516 | |
OIC_WINLOGO = 32517 | |
OIC_WARNING = OIC_BANG | |
OIC_ERROR = OIC_HAND | |
OIC_INFORMATION = OIC_NOTE | |
LoadImage = ctypes.windll.user32.LoadImageW | |
LoadImage.restype = ctypes.wintypes.HANDLE | |
LoadImage.argtypes = [ctypes.wintypes.HINSTANCE, ctypes.wintypes.LPCWSTR, ctypes.wintypes.UINT, ctypes.c_int, ctypes.c_int, ctypes.wintypes.UINT] | |
# CreateWindow call | |
WNDPROC = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.wintypes.HWND, ctypes.c_uint, ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM) | |
DefWindowProc = ctypes.windll.user32.DefWindowProcW | |
DefWindowProc.restype = ctypes.c_int | |
DefWindowProc.argtypes = [ctypes.wintypes.HWND, ctypes.c_uint, ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM] | |
WS_OVERLAPPED = 0x00000000L | |
WS_POPUP = 0x80000000L | |
WS_CHILD = 0x40000000L | |
WS_MINIMIZE = 0x20000000L | |
WS_VISIBLE = 0x10000000L | |
WS_DISABLED = 0x08000000L | |
WS_CLIPSIBLINGS = 0x04000000L | |
WS_CLIPCHILDREN = 0x02000000L | |
WS_MAXIMIZE = 0x01000000L | |
WS_CAPTION = 0x00C00000L | |
WS_BORDER = 0x00800000L | |
WS_DLGFRAME = 0x00400000L | |
WS_VSCROLL = 0x00200000L | |
WS_HSCROLL = 0x00100000L | |
WS_SYSMENU = 0x00080000L | |
WS_THICKFRAME = 0x00040000L | |
WS_GROUP = 0x00020000L | |
WS_TABSTOP = 0x00010000L | |
WS_MINIMIZEBOX = 0x00020000L | |
WS_MAXIMIZEBOX = 0x00010000L | |
WS_OVERLAPPEDWINDOW = (WS_OVERLAPPED | | |
WS_CAPTION | | |
WS_SYSMENU | | |
WS_THICKFRAME | | |
WS_MINIMIZEBOX | | |
WS_MAXIMIZEBOX) | |
SM_XVIRTUALSCREEN = 76 | |
SM_YVIRTUALSCREEN = 77 | |
SM_CXVIRTUALSCREEN = 78 | |
SM_CYVIRTUALSCREEN = 79 | |
SM_CMONITORS = 80 | |
SM_SAMEDISPLAYFORMAT = 81 | |
WM_NULL = 0x0000 | |
WM_CREATE = 0x0001 | |
WM_DESTROY = 0x0002 | |
WM_MOVE = 0x0003 | |
WM_SIZE = 0x0005 | |
WM_ACTIVATE = 0x0006 | |
WM_SETFOCUS = 0x0007 | |
WM_KILLFOCUS = 0x0008 | |
WM_ENABLE = 0x000A | |
WM_SETREDRAW = 0x000B | |
WM_SETTEXT = 0x000C | |
WM_GETTEXT = 0x000D | |
WM_GETTEXTLENGTH = 0x000E | |
WM_PAINT = 0x000F | |
WM_CLOSE = 0x0010 | |
WM_QUERYENDSESSION = 0x0011 | |
WM_QUIT = 0x0012 | |
WM_QUERYOPEN = 0x0013 | |
WM_ERASEBKGND = 0x0014 | |
WM_SYSCOLORCHANGE = 0x0015 | |
WM_ENDSESSION = 0x0016 | |
WM_SHOWWINDOW = 0x0018 | |
WM_CTLCOLOR = 0x0019 | |
WM_WININICHANGE = 0x001A | |
WM_SETTINGCHANGE = 0x001A | |
WM_DEVMODECHANGE = 0x001B | |
WM_ACTIVATEAPP = 0x001C | |
WM_FONTCHANGE = 0x001D | |
WM_TIMECHANGE = 0x001E | |
WM_CANCELMODE = 0x001F | |
WM_SETCURSOR = 0x0020 | |
WM_MOUSEACTIVATE = 0x0021 | |
WM_CHILDACTIVATE = 0x0022 | |
WM_QUEUESYNC = 0x0023 | |
WM_GETMINMAXINFO = 0x0024 | |
WM_PAINTICON = 0x0026 | |
WM_ICONERASEBKGND = 0x0027 | |
WM_NEXTDLGCTL = 0x0028 | |
WM_SPOOLERSTATUS = 0x002A | |
WM_DRAWITEM = 0x002B | |
WM_MEASUREITEM = 0x002C | |
WM_DELETEITEM = 0x002D | |
WM_VKEYTOITEM = 0x002E | |
WM_CHARTOITEM = 0x002F | |
WM_SETFONT = 0x0030 | |
WM_GETFONT = 0x0031 | |
WM_SETHOTKEY = 0x0032 | |
WM_GETHOTKEY = 0x0033 | |
WM_QUERYDRAGICON = 0x0037 | |
WM_COMPAREITEM = 0x0039 | |
WM_GETOBJECT = 0x003D | |
WM_COMPACTING = 0x0041 | |
WM_COMMNOTIFY = 0x0044 | |
WM_WINDOWPOSCHANGING = 0x0046 | |
WM_WINDOWPOSCHANGED = 0x0047 | |
WM_POWER = 0x0048 | |
WM_COPYDATA = 0x004A | |
WM_CANCELJOURNAL = 0x004B | |
WM_NOTIFY = 0x004E | |
WM_INPUTLANGCHANGEREQUEST = 0x0050 | |
WM_INPUTLANGCHANGE = 0x0051 | |
WM_TCARD = 0x0052 | |
WM_HELP = 0x0053 | |
WM_USERCHANGED = 0x0054 | |
WM_NOTIFYFORMAT = 0x0055 | |
WM_CONTEXTMENU = 0x007B | |
WM_STYLECHANGING = 0x007C | |
WM_STYLECHANGED = 0x007D | |
WM_DISPLAYCHANGE = 0x007E | |
WM_GETICON = 0x007F | |
WM_SETICON = 0x0080 | |
WM_NCCREATE = 0x0081 | |
WM_NCDESTROY = 0x0082 | |
WM_NCCALCSIZE = 0x0083 | |
WM_NCHITTEST = 0x0084 | |
WM_NCPAINT = 0x0085 | |
WM_NCACTIVATE = 0x0086 | |
WM_GETDLGCODE = 0x0087 | |
WM_SYNCPAINT = 0x0088 | |
WM_NCMOUSEMOVE = 0x00A0 | |
WM_NCLBUTTONDOWN = 0x00A1 | |
WM_NCLBUTTONUP = 0x00A2 | |
WM_NCLBUTTONDBLCLK = 0x00A3 | |
WM_NCRBUTTONDOWN = 0x00A4 | |
WM_NCRBUTTONUP = 0x00A5 | |
WM_NCRBUTTONDBLCLK = 0x00A6 | |
WM_NCMBUTTONDOWN = 0x00A7 | |
WM_NCMBUTTONUP = 0x00A8 | |
WM_NCMBUTTONDBLCLK = 0x00A9 | |
WM_KEYDOWN = 0x0100 | |
WM_KEYUP = 0x0101 | |
WM_CHAR = 0x0102 | |
WM_DEADCHAR = 0x0103 | |
WM_SYSKEYDOWN = 0x0104 | |
WM_SYSKEYUP = 0x0105 | |
WM_SYSCHAR = 0x0106 | |
WM_SYSDEADCHAR = 0x0107 | |
WM_KEYLAST = 0x0108 | |
WM_IME_STARTCOMPOSITION = 0x010D | |
WM_IME_ENDCOMPOSITION = 0x010E | |
WM_IME_COMPOSITION = 0x010F | |
WM_IME_KEYLAST = 0x010F | |
WM_INITDIALOG = 0x0110 | |
WM_COMMAND = 0x0111 | |
WM_SYSCOMMAND = 0x0112 | |
WM_TIMER = 0x0113 | |
WM_HSCROLL = 0x0114 | |
WM_VSCROLL = 0x0115 | |
WM_INITMENU = 0x0116 | |
WM_INITMENUPOPUP = 0x0117 | |
WM_MENUSELECT = 0x011F | |
WM_MENUCHAR = 0x0120 | |
WM_ENTERIDLE = 0x0121 | |
WM_MENURBUTTONUP = 0x0122 | |
WM_MENUDRAG = 0x0123 | |
WM_MENUGETOBJECT = 0x0124 | |
WM_UNINITMENUPOPUP = 0x0125 | |
WM_MENUCOMMAND = 0x0126 | |
WM_CTLCOLORMSGBOX = 0x0132 | |
WM_CTLCOLOREDIT = 0x0133 | |
WM_CTLCOLORLISTBOX = 0x0134 | |
WM_CTLCOLORBTN = 0x0135 | |
WM_CTLCOLORDLG = 0x0136 | |
WM_CTLCOLORSCROLLBAR = 0x0137 | |
WM_CTLCOLORSTATIC = 0x0138 | |
WM_MOUSEMOVE = 0x0200 | |
WM_LBUTTONDOWN = 0x0201 | |
WM_LBUTTONUP = 0x0202 | |
WM_LBUTTONDBLCLK = 0x0203 | |
WM_RBUTTONDOWN = 0x0204 | |
WM_RBUTTONUP = 0x0205 | |
WM_RBUTTONDBLCLK = 0x0206 | |
WM_MBUTTONDOWN = 0x0207 | |
WM_MBUTTONUP = 0x0208 | |
WM_MBUTTONDBLCLK = 0x0209 | |
WM_MOUSEWHEEL = 0x020A | |
WM_PARENTNOTIFY = 0x0210 | |
WM_ENTERMENULOOP = 0x0211 | |
WM_EXITMENULOOP = 0x0212 | |
WM_NEXTMENU = 0x0213 | |
WM_SIZING = 0x0214 | |
WM_CAPTURECHANGED = 0x0215 | |
WM_MOVING = 0x0216 | |
WM_DEVICECHANGE = 0x0219 | |
WM_MDICREATE = 0x0220 | |
WM_MDIDESTROY = 0x0221 | |
WM_MDIACTIVATE = 0x0222 | |
WM_MDIRESTORE = 0x0223 | |
WM_MDINEXT = 0x0224 | |
WM_MDIMAXIMIZE = 0x0225 | |
WM_MDITILE = 0x0226 | |
WM_MDICASCADE = 0x0227 | |
WM_MDIICONARRANGE = 0x0228 | |
WM_MDIGETACTIVE = 0x0229 | |
WM_MDISETMENU = 0x0230 | |
WM_ENTERSIZEMOVE = 0x0231 | |
WM_EXITSIZEMOVE = 0x0232 | |
WM_DROPFILES = 0x0233 | |
WM_MDIREFRESHMENU = 0x0234 | |
WM_IME_SETCONTEXT = 0x0281 | |
WM_IME_NOTIFY = 0x0282 | |
WM_IME_CONTROL = 0x0283 | |
WM_IME_COMPOSITIONFULL = 0x0284 | |
WM_IME_SELECT = 0x0285 | |
WM_IME_CHAR = 0x0286 | |
WM_IME_REQUEST = 0x0288 | |
WM_IME_KEYDOWN = 0x0290 | |
WM_IME_KEYUP = 0x0291 | |
WM_MOUSEHOVER = 0x02A1 | |
WM_MOUSELEAVE = 0x02A3 | |
WM_CUT = 0x0300 | |
WM_COPY = 0x0301 | |
WM_PASTE = 0x0302 | |
WM_CLEAR = 0x0303 | |
WM_UNDO = 0x0304 | |
WM_RENDERFORMAT = 0x0305 | |
WM_RENDERALLFORMATS = 0x0306 | |
WM_DESTROYCLIPBOARD = 0x0307 | |
WM_DRAWCLIPBOARD = 0x0308 | |
WM_PAINTCLIPBOARD = 0x0309 | |
WM_VSCROLLCLIPBOARD = 0x030A | |
WM_SIZECLIPBOARD = 0x030B | |
WM_ASKCBFORMATNAME = 0x030C | |
WM_CHANGECBCHAIN = 0x030D | |
WM_HSCROLLCLIPBOARD = 0x030E | |
WM_QUERYNEWPALETTE = 0x030F | |
WM_PALETTEISCHANGING = 0x0310 | |
WM_PALETTECHANGED = 0x0311 | |
WM_HOTKEY = 0x0312 | |
WM_PRINT = 0x0317 | |
WM_PRINTCLIENT = 0x0318 | |
WM_HANDHELDFIRST = 0x0358 | |
WM_HANDHELDLAST = 0x035F | |
WM_AFXFIRST = 0x0360 | |
WM_AFXLAST = 0x037F | |
WM_PENWINFIRST = 0x0380 | |
WM_PENWINLAST = 0x038F | |
WM_APP = 0x8000 | |
WM_USER = 0x0400 | |
WM_REFLECT = WM_USER + 0x1c00 | |
class WNDCLASSEX(ctypes.Structure): | |
def __init__(self, *args, **kwargs): | |
super(WNDCLASSEX, self).__init__(*args, **kwargs) | |
self.cbSize = ctypes.sizeof(self) | |
_fields_ = [("cbSize", ctypes.c_uint), | |
("style", ctypes.c_uint), | |
("lpfnWndProc", WNDPROC), | |
("cbClsExtra", ctypes.c_int), | |
("cbWndExtra", ctypes.c_int), | |
("hInstance", ctypes.wintypes.HANDLE), | |
("hIcon", ctypes.wintypes.HANDLE), | |
("hCursor", ctypes.wintypes.HANDLE), | |
("hBrush", ctypes.wintypes.HANDLE), | |
("lpszMenuName", ctypes.wintypes.LPCWSTR), | |
("lpszClassName", ctypes.wintypes.LPCWSTR), | |
("hIconSm", ctypes.wintypes.HANDLE)] | |
UpdateWindow = ctypes.windll.user32.UpdateWindow | |
UpdateWindow.argtypes = [ctypes.wintypes.HWND] | |
SW_HIDE = 0 | |
SW_SHOWNORMAL = 1 | |
SW_SHOW = 5 | |
ShowWindow = ctypes.windll.user32.ShowWindow | |
ShowWindow.argtypes = [ctypes.wintypes.HWND, ctypes.c_int] | |
CS_VREDRAW = 0x0001 | |
CS_HREDRAW = 0x0002 | |
CS_KEYCVTWINDOW = 0x0004 | |
CS_DBLCLKS = 0x0008 | |
CS_OWNDC = 0x0020 | |
CS_CLASSDC = 0x0040 | |
CS_PARENTDC = 0x0080 | |
CS_NOKEYCVT = 0x0100 | |
CS_NOCLOSE = 0x0200 | |
CS_SAVEBITS = 0x0800 | |
CS_BYTEALIGNCLIENT = 0x1000 | |
CS_BYTEALIGNWINDOW = 0x2000 | |
CS_GLOBALCLASS = 0x4000 | |
COLOR_SCROLLBAR = 0 | |
COLOR_BACKGROUND = 1 | |
COLOR_ACTIVECAPTION = 2 | |
COLOR_INACTIVECAPTION = 3 | |
COLOR_MENU = 4 | |
COLOR_WINDOW = 5 | |
COLOR_WINDOWFRAME = 6 | |
COLOR_MENUTEXT = 7 | |
COLOR_WINDOWTEXT = 8 | |
COLOR_CAPTIONTEXT = 9 | |
COLOR_ACTIVEBORDER = 10 | |
COLOR_INACTIVEBORDER = 11 | |
COLOR_APPWORKSPACE = 12 | |
COLOR_HIGHLIGHT = 13 | |
COLOR_HIGHLIGHTTEXT = 14 | |
COLOR_BTNFACE = 15 | |
COLOR_BTNSHADOW = 16 | |
COLOR_GRAYTEXT = 17 | |
COLOR_BTNTEXT = 18 | |
COLOR_INACTIVECAPTIONTEXT = 19 | |
COLOR_BTNHIGHLIGHT = 20 | |
LoadCursor = ctypes.windll.user32.LoadCursorW | |
def GenerateDummyWindow(callback, uid): | |
newclass = WNDCLASSEX() | |
newclass.lpfnWndProc = callback | |
newclass.style = CS_VREDRAW | CS_HREDRAW | |
newclass.lpszClassName = uid.replace("-", "") | |
newclass.hBrush = COLOR_BACKGROUND | |
newclass.hCursor = LoadCursor(0, 32512) | |
ATOM = ctypes.windll.user32.RegisterClassExW(ctypes.byref(newclass)) | |
#print "ATOM", ATOM | |
#print "CLASS", newclass.lpszClassName | |
hwnd = ctypes.windll.user32.CreateWindowExW(0, | |
newclass.lpszClassName, | |
u"Dummy Window", | |
WS_OVERLAPPEDWINDOW | WS_SYSMENU, | |
ctypes.windll.user32.GetSystemMetrics(SM_CXVIRTUALSCREEN), | |
ctypes.windll.user32.GetSystemMetrics(SM_CYVIRTUALSCREEN), | |
800, 600, 0, 0, 0, 0) | |
ShowWindow(hwnd, SW_SHOW) | |
UpdateWindow(hwnd) | |
ShowWindow(hwnd, SW_HIDE) | |
return hwnd | |
# Message loop calls | |
TIMERCALLBACK = ctypes.WINFUNCTYPE(None, | |
ctypes.wintypes.HWND, | |
ctypes.wintypes.UINT, | |
ctypes.POINTER(ctypes.wintypes.UINT), | |
ctypes.wintypes.DWORD) | |
SetTimer = ctypes.windll.user32.SetTimer | |
SetTimer.restype = ctypes.POINTER(ctypes.wintypes.UINT) | |
SetTimer.argtypes = [ctypes.wintypes.HWND, | |
ctypes.POINTER(ctypes.wintypes.UINT), | |
ctypes.wintypes.UINT, | |
TIMERCALLBACK] | |
KillTimer = ctypes.windll.user32.KillTimer | |
KillTimer.restype = ctypes.wintypes.BOOL | |
KillTimer.argtypes = [ctypes.wintypes.HWND, | |
ctypes.POINTER(ctypes.wintypes.UINT)] | |
class MSG(ctypes.Structure): | |
_fields_ = [ ('HWND', ctypes.wintypes.HWND), | |
('message', ctypes.wintypes.UINT), | |
('wParam', ctypes.wintypes.WPARAM), | |
('lParam', ctypes.wintypes.LPARAM), | |
('time', ctypes.wintypes.DWORD), | |
('pt', POINT)] | |
GetMessage = ctypes.windll.user32.GetMessageW | |
GetMessage.restype = ctypes.wintypes.BOOL | |
GetMessage.argtypes = [ctypes.POINTER(MSG), ctypes.wintypes.HWND, ctypes.wintypes.UINT, ctypes.wintypes.UINT] | |
TranslateMessage = ctypes.windll.user32.TranslateMessage | |
TranslateMessage.restype = ctypes.wintypes.ULONG | |
TranslateMessage.argtypes = [ctypes.POINTER(MSG)] | |
DispatchMessage = ctypes.windll.user32.DispatchMessageW | |
DispatchMessage.restype = ctypes.wintypes.ULONG | |
DispatchMessage.argtypes = [ctypes.POINTER(MSG)] | |
def LoadIcon(iconfilename, small=False): | |
return LoadImage(0, | |
unicode(iconfilename), | |
IMAGE_ICON, | |
16 if small else 0, | |
16 if small else 0, | |
LR_LOADFROMFILE) | |
class NotificationIcon(object): | |
def __init__(self, iconfilename, tooltip=None): | |
assert os.path.isfile(unicode(iconfilename)), "{} doesn't exist".format(iconfilename) | |
self._iconfile = unicode(iconfilename) | |
self._hicon = LoadIcon(self._iconfile, True) | |
assert self._hicon, "Failed to load {}".format(iconfilename) | |
self._pumpqueue = Queue.Queue() | |
self._die = False | |
self._timerid = None | |
self._uid = uuid.uuid4() | |
self._tooltip = unicode(tooltip) if tooltip else u'' | |
self._thread = threading.Thread(target=self._run) | |
self._thread.start() | |
self._info_bubble = None | |
self.items = [] | |
def _bubble(self, iconinfo): | |
if self._info_bubble: | |
info_bubble = self._info_bubble | |
self._info_bubble = None | |
message = unicode(self._info_bubble) | |
iconinfo.uFlags |= NIF_INFO | |
iconinfo.szInfo = message | |
iconinfo.szInfoTitle = message | |
iconinfo.dwInfoFlags = NIIF_INFO | |
iconinfo.union.uTimeout = 10000 | |
Shell_NotifyIcon(NIM_MODIFY, ctypes.pointer(iconinfo)) | |
def _run(self): | |
self._windowproc = WNDPROC(self._callback) | |
self._hwnd = GenerateDummyWindow(self._windowproc, str(self._uid)) | |
iconinfo = NOTIFYICONDATA() | |
iconinfo.hWnd = self._hwnd | |
iconinfo.uID = 100 | |
iconinfo.uFlags = NIF_ICON | NIF_SHOWTIP | NIF_MESSAGE | (NIF_TIP if self._tooltip else 0) | |
iconinfo.uCallbackMessage = WM_MENUCOMMAND | |
iconinfo.hIcon = self._hicon | |
iconinfo.szTip = self._tooltip | |
iconinfo.dwState = NIS_SHAREDICON | |
iconinfo.dwInfoFlags = NIIF_INFO | |
# iconinfo.dwStateMask = NIS_SHAREDICON | |
iconinfo.szInfo = "Application Title" | |
iconinfo.union.uTimeout = 5000 | |
Shell_NotifyIcon(NIM_ADD, ctypes.pointer(iconinfo)) | |
iconinfo.union.uVersion = NOTIFYICON_VERSION | |
Shell_NotifyIcon(NIM_SETVERSION, ctypes.pointer(iconinfo)) | |
PostMessage(self._hwnd, WM_NULL, 0, 0) | |
self._timerid = SetTimer(self._hwnd, self._timerid, 25, TIMERCALLBACK()) | |
message = MSG() | |
while not self._die: | |
GetMessage(ctypes.pointer(message), 0, 0, 0) | |
TranslateMessage(ctypes.pointer(message)) | |
DispatchMessage(ctypes.pointer(message)) | |
self._bubble(iconinfo) | |
KillTimer(self._hwnd, self._timerid) | |
Shell_NotifyIcon(NIM_DELETE, ctypes.pointer(iconinfo)) | |
ctypes.windll.user32.DestroyWindow(self._hwnd) | |
ctypes.windll.user32.DestroyIcon(self._hicon) | |
def _menu(self): | |
if not hasattr(self, 'items'): | |
return | |
menu = CreatePopupMenu() | |
try: | |
iidx = 1000 | |
defaultitem = -1 | |
item_map = {} | |
for fs in self.items: | |
iidx += 1 | |
if isinstance(fs, basestring): | |
if fs and not fs.strip('-_='): | |
AppendMenu(menu, MF_SEPARATOR, iidx, fs) | |
else: | |
AppendMenu(menu, MF_STRING | MF_GRAYED, iidx, fs) | |
elif isinstance(fs, tuple): | |
itemstring = unicode(fs[0]) | |
if itemstring.startswith("!"): | |
itemstring = itemstring[1:] | |
defaultitem = iidx | |
itemcallable = fs[1] | |
item_map[iidx] = itemcallable | |
if callable(itemcallable): | |
AppendMenu(menu, MF_STRING, iidx, itemstring) | |
elif itemcallable is False: | |
AppendMenu(menu, MF_STRING | MF_DISABLED, iidx, itemstring) | |
else: | |
AppendMenu(menu, MF_STRING | MF_GRAYED, iidx, itemstring) | |
if defaultitem != -1: | |
SetMenuDefaultItem(menu, defaultitem, 0) | |
pos = POINT() | |
GetCursorPos(ctypes.pointer(pos)) | |
PostMessage(self._hwnd, WM_NULL, 0, 0) | |
SetForegroundWindow(self._hwnd) | |
ti = TrackPopupMenu(menu, TPM_RIGHTBUTTON | TPM_RETURNCMD | TPM_NONOTIFY, pos.x, pos.y, 0, self._hwnd, None) | |
if ti in item_map: | |
self._pumpqueue.put(item_map[ti]) | |
PostMessage(self._hwnd, WM_NULL, 0, 0) | |
finally: | |
DestroyMenu(menu) | |
def _callback(self, hWnd, msg, wParam, lParam): | |
# Check if the main thread is still alive | |
if msg == WM_TIMER: | |
if not any(thread.getName() == 'MainThread' and thread.isAlive() | |
for thread in threading.enumerate()): | |
self._die = True | |
elif msg == WM_MENUCOMMAND and lParam in (WM_RBUTTONUP, WM_LBUTTONUP): | |
self._menu() | |
else: | |
return DefWindowProc(hWnd, msg, wParam, lParam) | |
return 1 | |
def die(self): | |
self._die = True | |
def pump(self): | |
try: | |
while not self._pumpqueue.empty(): | |
callable = self._pumpqueue.get(False) | |
callable() | |
except Queue.Empty: | |
pass | |
def announce(self, text): | |
self._info_bubble = text | |
if __name__ == "__main__": | |
ni = NotificationIcon(os.path.join( | |
os.path.dirname( | |
os.path.abspath(__file__)), | |
'codereview.ico')) | |
import time | |
def greet(): | |
print "Hello" | |
def quit(): | |
import sys | |
sys.exit() | |
def announce(): | |
ni.announce("Hello there") | |
ni.items = [('Hello', greet), | |
('Title', False), | |
('!Default', greet), | |
('Popup bubble', announce), | |
'Nothing', | |
'--', | |
('Quit', quit)] | |
while True: | |
ni.pump() | |
time.sleep(0.125) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment