_win32.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. # coding=utf-8
  2. # pystray
  3. # Copyright (C) 2016-2020 Moses Palmér
  4. #
  5. # This program is free software: you can redistribute it and/or modify it under
  6. # the terms of the GNU Lesser General Public License as published by the Free
  7. # Software Foundation, either version 3 of the License, or (at your option) any
  8. # later version.
  9. #
  10. # This program is distributed in the hope that it will be useful, but WITHOUT
  11. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  12. # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  13. # details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. import ctypes
  18. import threading
  19. from ctypes import wintypes
  20. from six.moves import queue
  21. from ._util import serialized_image, win32
  22. from . import _base
  23. class Icon(_base.Icon):
  24. _HWND_TO_ICON = {}
  25. def __init__(self, *args, **kwargs):
  26. super(Icon, self).__init__(*args, **kwargs)
  27. self._atom = self._register_class()
  28. self._icon_handle = None
  29. self._hwnd = None
  30. self._menu_hwnd = None
  31. self._hmenu = None
  32. # This is a mapping from win32 event codes to handlers used by the
  33. # mainloop
  34. self._message_handlers = {
  35. win32.WM_STOP: self._on_stop,
  36. win32.WM_NOTIFY: self._on_notify,
  37. win32.WM_TASKBARCREATED: self._on_taskbarcreated}
  38. self._queue = queue.Queue()
  39. def __del__(self):
  40. if self._running:
  41. self._stop()
  42. if self._thread.ident != threading.current_thread().ident:
  43. self._thread.join()
  44. self._release_icon()
  45. def _show(self):
  46. self._assert_icon_handle()
  47. self._message(
  48. win32.NIM_ADD,
  49. win32.NIF_MESSAGE | win32.NIF_ICON | win32.NIF_TIP,
  50. uCallbackMessage=win32.WM_NOTIFY,
  51. hIcon=self._icon_handle,
  52. szTip=self.title)
  53. def _hide(self):
  54. self._message(
  55. win32.NIM_DELETE,
  56. 0)
  57. def _update_icon(self):
  58. self._release_icon()
  59. self._assert_icon_handle()
  60. self._message(
  61. win32.NIM_MODIFY,
  62. win32.NIF_ICON,
  63. hIcon=self._icon_handle)
  64. self._icon_valid = True
  65. def _update_title(self):
  66. self._message(
  67. win32.NIM_MODIFY,
  68. win32.NIF_TIP,
  69. szTip=self.title)
  70. def _notify(self, message, title=None):
  71. self._message(
  72. win32.NIM_MODIFY,
  73. win32.NIF_INFO,
  74. szInfo=message,
  75. szInfoTitle=title or self.title or '')
  76. def _remove_notification(self):
  77. self._message(
  78. win32.NIM_MODIFY,
  79. win32.NIF_INFO,
  80. szInfo='')
  81. def _update_menu(self):
  82. try:
  83. hmenu, callbacks = self._menu_handle
  84. win32.DestroyMenu(hmenu)
  85. except:
  86. pass
  87. callbacks = []
  88. hmenu = self._create_menu(self.menu, callbacks)
  89. if hmenu:
  90. self._menu_handle = (hmenu, callbacks)
  91. else:
  92. self._menu_handle = None
  93. def _run(self):
  94. # Create the message loop
  95. msg = wintypes.MSG()
  96. lpmsg = ctypes.byref(msg)
  97. win32.PeekMessage(
  98. lpmsg, None, win32.WM_USER, win32.WM_USER, win32.PM_NOREMOVE)
  99. self._hwnd = self._create_window(self._atom)
  100. self._menu_hwnd = self._create_window(self._atom)
  101. self._HWND_TO_ICON[self._hwnd] = self
  102. self._mark_ready()
  103. # Run the event loop
  104. self._thread = threading.current_thread()
  105. self._mainloop()
  106. def _run_detached(self):
  107. threading.Thread(target=lambda: self._run()).start()
  108. def _stop(self):
  109. win32.PostMessage(self._hwnd, win32.WM_STOP, 0, 0)
  110. def _mainloop(self):
  111. """The body of the main loop thread.
  112. This method retrieves all events from *Windows* and makes sure to
  113. dispatch clicks.
  114. """
  115. # Pump messages
  116. try:
  117. msg = wintypes.MSG()
  118. lpmsg = ctypes.byref(msg)
  119. while True:
  120. r = win32.GetMessage(lpmsg, None, 0, 0)
  121. if not r:
  122. break
  123. elif r == -1:
  124. break
  125. else:
  126. win32.TranslateMessage(lpmsg)
  127. win32.DispatchMessage(lpmsg)
  128. except:
  129. self._log.error(
  130. 'An error occurred in the main loop', exc_info=True)
  131. finally:
  132. try:
  133. self._hide()
  134. del self._HWND_TO_ICON[self._hwnd]
  135. except:
  136. # Ignore
  137. pass
  138. win32.DestroyWindow(self._hwnd)
  139. win32.DestroyWindow(self._menu_hwnd)
  140. if self._menu_handle:
  141. hmenu, callbacks = self._menu_handle
  142. win32.DestroyMenu(hmenu)
  143. self._unregister_class(self._atom)
  144. def _on_stop(self, wparam, lparam):
  145. """Handles ``WM_STOP``.
  146. This method posts a quit message, causing the mainloop thread to
  147. terminate.
  148. """
  149. win32.PostQuitMessage(0)
  150. def _on_notify(self, wparam, lparam):
  151. """Handles ``WM_NOTIFY``.
  152. If this is a left button click, this icon will be activated. If a menu
  153. is registered and this is a right button click, the popup menu will be
  154. displayed.
  155. """
  156. if lparam == win32.WM_LBUTTONUP:
  157. self()
  158. elif self._menu_handle and lparam == win32.WM_RBUTTONUP:
  159. # TrackPopupMenuEx does not behave unless our systray window is the
  160. # foreground window
  161. win32.SetForegroundWindow(self._hwnd)
  162. # Get the cursor position to determine where to display the menu
  163. point = wintypes.POINT()
  164. win32.GetCursorPos(ctypes.byref(point))
  165. # Display the menu and get the menu item identifier; the identifier
  166. # is the menu item index
  167. hmenu, descriptors = self._menu_handle
  168. index = win32.TrackPopupMenuEx(
  169. hmenu,
  170. win32.TPM_RIGHTALIGN | win32.TPM_BOTTOMALIGN
  171. | win32.TPM_RETURNCMD,
  172. point.x,
  173. point.y,
  174. self._menu_hwnd,
  175. None)
  176. if index > 0:
  177. descriptors[index - 1](self)
  178. def _on_taskbarcreated(self, wparam, lparam):
  179. """Handles ``WM_TASKBARCREATED``.
  180. This message is broadcast when the notification area becomes available.
  181. Handling this message allows catching explorer restarts.
  182. """
  183. if self.visible:
  184. self._show()
  185. def _create_window(self, atom):
  186. """Creates the system tray icon window.
  187. :param atom: The window class atom.
  188. :return: a window
  189. """
  190. # Broadcast messages (including WM_TASKBARCREATED) can be caught
  191. # only by top-level windows, so we cannot create a message-only window
  192. hwnd = win32.CreateWindowEx(
  193. 0,
  194. atom,
  195. None,
  196. win32.WS_POPUP,
  197. 0, 0, 0, 0,
  198. 0,
  199. None,
  200. win32.GetModuleHandle(None),
  201. None)
  202. # On Vista+, we must explicitly opt-in to receive WM_TASKBARCREATED
  203. # when running with escalated privileges
  204. win32.ChangeWindowMessageFilterEx(
  205. hwnd, win32.WM_TASKBARCREATED, win32.MSGFLT_ALLOW, None)
  206. return hwnd
  207. def _create_menu(self, descriptors, callbacks):
  208. """Creates a :class:`ctypes.wintypes.HMENU` from a
  209. :class:`pystray.Menu` instance.
  210. :param descriptors: The menu descriptors. If this is falsy, ``None`` is
  211. returned.
  212. :param callbacks: A list to which a callback is appended for every menu
  213. item created. The menu item IDs correspond to the items in this
  214. list plus one.
  215. :return: a menu
  216. """
  217. if not descriptors:
  218. return None
  219. else:
  220. # Generate the menu
  221. hmenu = win32.CreatePopupMenu()
  222. for i, descriptor in enumerate(descriptors):
  223. # Append the callbacks before creating the menu items to ensure
  224. # that the first item gets the ID 1
  225. callbacks.append(self._handler(descriptor))
  226. menu_item = self._create_menu_item(descriptor, callbacks)
  227. win32.InsertMenuItem(hmenu, i, True, ctypes.byref(menu_item))
  228. return hmenu
  229. def _create_menu_item(self, descriptor, callbacks):
  230. """Creates a :class:`pystray._util.win32.MENUITEMINFO` from a
  231. :class:`pystray.MenuItem` instance.
  232. :param descriptor: The menu item descriptor.
  233. :param callbacks: A list to which a callback is appended for every menu
  234. item created. The menu item IDs correspond to the items in this
  235. list plus one.
  236. :return: a :class:`pystray._util.win32.MENUITEMINFO`
  237. """
  238. if descriptor is _base.Menu.SEPARATOR:
  239. return win32.MENUITEMINFO(
  240. cbSize=ctypes.sizeof(win32.MENUITEMINFO),
  241. fMask=win32.MIIM_FTYPE,
  242. fType=win32.MFT_SEPARATOR)
  243. else:
  244. return win32.MENUITEMINFO(
  245. cbSize=ctypes.sizeof(win32.MENUITEMINFO),
  246. fMask=win32.MIIM_ID | win32.MIIM_STRING | win32.MIIM_STATE
  247. | win32.MIIM_FTYPE | win32.MIIM_SUBMENU,
  248. wID=len(callbacks),
  249. dwTypeData=descriptor.text,
  250. fState=0
  251. | (win32.MFS_DEFAULT if descriptor.default else 0)
  252. | (win32.MFS_CHECKED if descriptor.checked else 0)
  253. | (win32.MFS_DISABLED if not descriptor.enabled else 0),
  254. fType=0
  255. | (win32.MFT_RADIOCHECK if descriptor.radio else 0),
  256. hSubMenu=self._create_menu(descriptor.submenu, callbacks)
  257. if descriptor.submenu
  258. else None)
  259. def _message(self, code, flags, **kwargs):
  260. """Sends a message the the systray icon.
  261. This method adds ``cbSize``, ``hWnd``, ``hId`` and ``uFlags`` to the
  262. message data.
  263. :param int message: The message to send. This should be one of the
  264. ``NIM_*`` constants.
  265. :param int flags: The value of ``NOTIFYICONDATAW::uFlags``.
  266. :param kwargs: Data for the :class:`NOTIFYICONDATAW` object.
  267. """
  268. win32.Shell_NotifyIcon(code, win32.NOTIFYICONDATAW(
  269. cbSize=ctypes.sizeof(win32.NOTIFYICONDATAW),
  270. hWnd=self._hwnd,
  271. hID=id(self),
  272. uFlags=flags,
  273. **kwargs))
  274. def _release_icon(self):
  275. """Releases the icon handle and sets it to ``None``.
  276. If not icon handle is set, no action is performed.
  277. """
  278. if self._icon_handle:
  279. win32.DestroyIcon(self._icon_handle)
  280. self._icon_handle = None
  281. def _assert_icon_handle(self):
  282. """Asserts that the cached icon handle exists.
  283. """
  284. if self._icon_handle:
  285. return
  286. with serialized_image(self.icon, 'ICO') as icon_path:
  287. self._icon_handle = win32.LoadImage(
  288. None,
  289. icon_path,
  290. win32.IMAGE_ICON,
  291. 0,
  292. 0,
  293. win32.LR_DEFAULTSIZE | win32.LR_LOADFROMFILE)
  294. def _register_class(self):
  295. """Registers the systray window class.
  296. :return: the class atom
  297. """
  298. return win32.RegisterClassEx(win32.WNDCLASSEX(
  299. cbSize=ctypes.sizeof(win32.WNDCLASSEX),
  300. style=0,
  301. lpfnWndProc=_dispatcher,
  302. cbClsExtra=0,
  303. cbWndExtra=0,
  304. hInstance=win32.GetModuleHandle(None),
  305. hIcon=None,
  306. hCursor=None,
  307. hbrBackground=win32.COLOR_WINDOW + 1,
  308. lpszMenuName=None,
  309. lpszClassName='%s%dSystemTrayIcon' % (self.name, id(self)),
  310. hIconSm=None))
  311. def _unregister_class(self, atom):
  312. """Unregisters the systray window class.
  313. :param atom: The class atom returned by :meth:`_register_class`.
  314. """
  315. win32.UnregisterClass(atom, win32.GetModuleHandle(None))
  316. @win32.WNDPROC
  317. def _dispatcher(hwnd, uMsg, wParam, lParam):
  318. """The function used as window procedure for the systray window.
  319. """
  320. # These messages are sent before Icon._HWND_TO_ICON[hwnd] has been set, so
  321. # we handle them explicitly
  322. if uMsg == win32.WM_NCCREATE:
  323. return True
  324. if uMsg == win32.WM_CREATE:
  325. return 0
  326. try:
  327. icon = Icon._HWND_TO_ICON[hwnd]
  328. except KeyError:
  329. return win32.DefWindowProc(hwnd, uMsg, wParam, lParam)
  330. try:
  331. return int(icon._message_handlers.get(
  332. uMsg, lambda w, l: 0)(wParam, lParam) or 0)
  333. except:
  334. icon._log.error(
  335. 'An error occurred when calling message handler', exc_info=True)
  336. return 0