diff --git a/plyer/platforms/win/libs/balloontip.py b/plyer/platforms/win/libs/balloontip.py index 6acfa9451..d00c8a6f9 100644 --- a/plyer/platforms/win/libs/balloontip.py +++ b/plyer/platforms/win/libs/balloontip.py @@ -1,13 +1,13 @@ # -- coding: utf-8 -- -# Original from https://gist.github.com/wontoncc/1808234 -# Modified from https://gist.github.com/boppreh/4000505 -import os -import sys +__all__ = ('WindowsBalloonTip', 'balloon_tip') + + import time +import ctypes +import win_api_defs +from threading import RLock -import win32gui -from win32api import GetModuleHandle WS_OVERLAPPED = 0x00000000 WS_SYSMENU = 0x00080000 @@ -16,57 +16,127 @@ LR_LOADFROMFILE = 16 LR_DEFAULTSIZE = 0x0040 - +IDI_APPLICATION = 32512 IMAGE_ICON = 1 -IDI_APPLICATION = 32512 +NOTIFYICON_VERSION_4 = 4 +NIM_ADD = 0 +NIM_MODIFY = 1 +NIM_DELETE = 2 +NIM_SETVERSION = 4 +NIF_MESSAGE = 1 +NIF_ICON = 2 +NIF_TIP = 4 +NIF_INFO = 0x10 +NIIF_USER = 4 +NIIF_LARGE_ICON = 0x20 + + +class WindowsBalloonTip(object): + + _class_atom = 0 + _wnd_class_ex = None + _hwnd = None + _hicon = None + _balloon_icon = None + _notify_data = None + _count = 0 + _lock = RLock() -WM_USER = 1024 - -class WindowsBalloonTip: - - def __init__(self, title, message, app_name, app_icon, timeout=10): - message_map = {WM_DESTROY: self.OnDestroy, } - # Register the Window class. - wc = win32gui.WNDCLASS() - hinst = wc.hInstance = GetModuleHandle(None) - wc.lpszClassName = "PythonTaskbar" - wc.lpfnWndProc = message_map # could also specify a wndproc. - class_atom = win32gui.RegisterClass(wc) - # Create the Window. - style = WS_OVERLAPPED | WS_SYSMENU - self.hwnd = win32gui.CreateWindow(class_atom, "Taskbar", style, - 0, 0, CW_USEDEFAULT, - CW_USEDEFAULT, 0, 0, - hinst, None) - win32gui.UpdateWindow(self.hwnd) + @staticmethod + def _get_unique_id(): + WindowsBalloonTip._lock.acquire() + val = WindowsBalloonTip._count + WindowsBalloonTip._count += 1 + WindowsBalloonTip._lock.release() + return val + + def __init__(self, title, message, app_name, app_icon='', timeout=10): + ''' app_icon if given is a icon file. + ''' + + wnd_class_ex = win_api_defs.get_WNDCLASSEXW() + wnd_class_ex.lpszClassName = ('PlyerTaskbar' + + str(WindowsBalloonTip._get_unique_id())).decode('utf8') + # keep ref to it as long as window is alive + wnd_class_ex.lpfnWndProc =\ + win_api_defs.WindowProc(win_api_defs.DefWindowProcW) + wnd_class_ex.hInstance = win_api_defs.GetModuleHandleW(None) + if wnd_class_ex.hInstance == None: + raise Exception('Could not get windows module instance.') + class_atom = win_api_defs.RegisterClassExW(wnd_class_ex) + if class_atom == 0: + raise Exception('Could not register the PlyerTaskbar class.') + self._class_atom = class_atom + self._wnd_class_ex = wnd_class_ex + + # create window + self._hwnd = win_api_defs.CreateWindowExW(0, class_atom, + '', WS_OVERLAPPED, 0, 0, CW_USEDEFAULT, + CW_USEDEFAULT, None, None, wnd_class_ex.hInstance, None) + if self._hwnd == None: + raise Exception('Could not get create window.') + win_api_defs.UpdateWindow(self._hwnd) + + # load icon if app_icon: - icon_path_name = app_icon + icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE + hicon = win_api_defs.LoadImageW(None, app_icon.decode('utf8'), + IMAGE_ICON, 0, 0, icon_flags) + if hicon is None: + raise Exception('Could not load icon {}'. + format(icon_path_name).decode('utf8')) + self._balloon_icon = self._hicon = hicon else: - icon_path_name = os.path.abspath(os.path.join(sys.path[0], - "balloontip.ico")) - icon_flags = LR_LOADFROMFILE | LR_DEFAULTSIZE - try: - hicon = win32gui.LoadImage(hinst, icon_path_name, - IMAGE_ICON, 0, 0, icon_flags) - except: - hicon = win32gui.LoadIcon(0, IDI_APPLICATION) - flags = win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP - nid = (self.hwnd, 0, flags, WM_USER+20, hicon, "tooltip") - win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, nid) - win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, - (self.hwnd, 0, win32gui.NIF_INFO, - WM_USER+20, hicon, - "Balloon tooltip", message, 200, title)) - # self.show_balloon(title, msg) - time.sleep(timeout) - win32gui.DestroyWindow(self.hwnd) - win32gui.UnregisterClass(class_atom, hinst) - - def OnDestroy(self, hwnd, msg, wparam, lparam): - nid = (self.hwnd, 0) - win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, nid) - win32gui.PostQuitMessage(0) # Terminate the app. + self._hicon = win_api_defs.LoadIconW(None, + ctypes.cast(IDI_APPLICATION, win_api_defs.LPCWSTR)) + self.notify(title, message, app_name) + if timeout: + time.sleep(timeout) + + def __del__(self): + self.remove_notify() + if self._hicon is not None: + win_api_defs.DestroyIcon(self._hicon) + if self._wnd_class_ex is not None: + win_api_defs.UnregisterClassW(self._class_atom, + self._wnd_class_ex.hInstance) + if self._hwnd is not None: + win_api_defs.DestroyWindow(self._hwnd) + + def notify(self, title, message, app_name): + ''' Displays a balloon in the systray. Can be called multiple times + with different parameter values. + ''' + self.remove_notify() + # add icon and messages to window + hicon = self._hicon + flags = NIF_TIP | NIF_INFO + icon_flag = 0 + if hicon is not None: + flags |= NIF_ICON + # if icon is default app's one, don't display it in message + if self._balloon_icon is not None: + icon_flag = NIIF_USER | NIIF_LARGE_ICON + notify_data = win_api_defs.get_NOTIFYICONDATAW(0, self._hwnd, + id(self), flags, 0, hicon, app_name.decode('utf8')[:127], 0, 0, + message.decode('utf8')[:255], NOTIFYICON_VERSION_4, + title.decode('utf8')[:63], icon_flag, win_api_defs.GUID(), + self._balloon_icon) + + self._notify_data = notify_data + if not win_api_defs.Shell_NotifyIconW(NIM_ADD, notify_data): + raise Exception('Shell_NotifyIconW failed.') + if not win_api_defs.Shell_NotifyIconW(NIM_SETVERSION, + notify_data): + raise Exception('Shell_NotifyIconW failed.') + + def remove_notify(self): + '''Removes the notify balloon, if displayed. + ''' + if self._notify_data is not None: + win_api_defs.Shell_NotifyIconW(NIM_DELETE, self._notify_data) + self._notify_data = None def balloon_tip(**kwargs): diff --git a/plyer/platforms/win/libs/win_api_defs.py b/plyer/platforms/win/libs/win_api_defs.py new file mode 100644 index 000000000..688569d8c --- /dev/null +++ b/plyer/platforms/win/libs/win_api_defs.py @@ -0,0 +1,178 @@ +''' Defines ctypes windows api. +''' + +__all__ = ('GUID', 'get_DLLVERSIONINFO', 'MAKEDLLVERULL', + 'get_NOTIFYICONDATAW', 'CreateWindowExW', 'WindowProc', + 'DefWindowProcW', 'get_WNDCLASSEXW', 'GetModuleHandleW', + 'RegisterClassExW', 'UpdateWindow', 'LoadImageW', + 'Shell_NotifyIconW', 'DestroyIcon', 'UnregisterClassW', + 'DestroyWindow', 'LoadIconW') + +import ctypes +from ctypes import Structure, windll, sizeof, byref, POINTER, memset,\ + WINFUNCTYPE +from ctypes.wintypes import DWORD, HICON, HWND, UINT, WCHAR, WORD, BYTE,\ + HRESULT, LPCWSTR, LPWSTR, INT, LPVOID, HINSTANCE, HMENU, LPARAM, WPARAM,\ + HBRUSH, HMODULE, ATOM, BOOL, HANDLE, LONG, HHOOK +LRESULT = LPARAM +HCURSOR = HICON + + + +class GUID(Structure): + _fields_ = [ + ('Data1', DWORD), + ('Data2', WORD), + ('Data3', WORD), + ('Data4', BYTE * 8) + ] + + +class DLLVERSIONINFO(Structure): + _fields_ = [ + ('cbSize', DWORD), + ('dwMajorVersion', DWORD), + ('dwMinorVersion', DWORD), + ('dwBuildNumber', DWORD), + ('dwPlatformID', DWORD), + ] + +def get_DLLVERSIONINFO(*largs): + version_info = DLLVERSIONINFO(*largs) + version_info.cbSize = sizeof(DLLVERSIONINFO) + return version_info + +def MAKEDLLVERULL(major, minor, build, sp): + return (major << 48) | (minor << 32) | (build << 16) | sp + + +NOTIFYICONDATAW_fields = [ + ("cbSize", DWORD), + ("hWnd", HWND), + ("uID", UINT), + ("uFlags", UINT), + ("uCallbackMessage", UINT), + ("hIcon", HICON), + ("szTip", WCHAR * 128), + ("dwState", DWORD), + ("dwStateMask", DWORD), + ("szInfo", WCHAR * 256), + ("uVersion", UINT), + ("szInfoTitle", WCHAR * 64), + ("dwInfoFlags", DWORD), + ("guidItem", GUID), + ("hBalloonIcon", HICON), +] + +class NOTIFYICONDATAW(Structure): + _fields_ = NOTIFYICONDATAW_fields[:] + + +class NOTIFYICONDATAW_V3(Structure): + _fields_ = NOTIFYICONDATAW_fields[:-1] + + +class NOTIFYICONDATAW_V2(Structure): + _fields_ = NOTIFYICONDATAW_fields[:-2] + + +class NOTIFYICONDATAW_V1(Structure): + _fields_ = NOTIFYICONDATAW_fields[:6] + +NOTIFYICONDATA_V3_SIZE = sizeof(NOTIFYICONDATAW_V3) +NOTIFYICONDATA_V2_SIZE = sizeof(NOTIFYICONDATAW_V2) +NOTIFYICONDATA_V1_SIZE = sizeof(NOTIFYICONDATAW_V1) + +def get_NOTIFYICONDATAW(*largs): + notify_data = NOTIFYICONDATAW(*largs) + + # get shell32 version to find correct NOTIFYICONDATAW size + DllGetVersion = windll.Shell32.DllGetVersion + DllGetVersion.argtypes = [POINTER(DLLVERSIONINFO)] + DllGetVersion.restype = HRESULT + + version = get_DLLVERSIONINFO() + if DllGetVersion(version): + raise Exception('Cannot get Windows version numbers.') + v = MAKEDLLVERULL(version.dwMajorVersion, version.dwMinorVersion, + version.dwBuildNumber, version.dwPlatformID) + + # from the version info find the NOTIFYICONDATA size + if v >= MAKEDLLVERULL(6, 0, 6, 0): + notify_data.cbSize = sizeof(NOTIFYICONDATAW) + elif v >= MAKEDLLVERULL(6, 0, 0, 0): + notify_data.cbSize = NOTIFYICONDATA_V3_SIZE + elif v >= MAKEDLLVERULL(5, 0, 0, 0): + notify_data.cbSize = NOTIFYICONDATA_V2_SIZE + else: + notify_data.cbSize = NOTIFYICONDATA_V1_SIZE + return notify_data + + +CreateWindowExW = windll.User32.CreateWindowExW +CreateWindowExW.argtypes = [DWORD, ATOM, LPCWSTR, DWORD, INT, INT, INT, INT, + HWND, HMENU, HINSTANCE, LPVOID] +CreateWindowExW.restype = HWND + +GetModuleHandleW = windll.Kernel32.GetModuleHandleW +GetModuleHandleW.argtypes = [LPCWSTR] +GetModuleHandleW.restype = HMODULE + +WindowProc = WINFUNCTYPE(LRESULT, HWND, UINT, WPARAM, LPARAM) +DefWindowProcW = windll.User32.DefWindowProcW +DefWindowProcW.argtypes = [HWND, UINT, WPARAM, LPARAM] +DefWindowProcW.restype = LRESULT + + +class WNDCLASSEXW(Structure): + _fields_ = [ + ('cbSize', UINT), + ('style', UINT), + ('lpfnWndProc', WindowProc), + ('cbClsExtra', INT), + ('cbWndExtra', INT), + ('hInstance', HINSTANCE), + ('hIcon', HICON), + ('hCursor', HCURSOR), + ('hbrBackground', HBRUSH), + ('lpszMenuName', LPCWSTR), + ('lpszClassName', LPCWSTR), + ('hIconSm', HICON), + ] + +def get_WNDCLASSEXW(*largs): + wnd_class = WNDCLASSEXW(*largs) + wnd_class.cbSize = sizeof(WNDCLASSEXW) + return wnd_class + +RegisterClassExW = windll.User32.RegisterClassExW +RegisterClassExW.argtypes = [POINTER(WNDCLASSEXW)] +RegisterClassExW.restype = ATOM + +UpdateWindow = windll.User32.UpdateWindow +UpdateWindow.argtypes = [HWND] +UpdateWindow.restype = BOOL + +LoadImageW = windll.User32.LoadImageW +LoadImageW.argtypes = [HINSTANCE, LPCWSTR, UINT, INT, INT, UINT] +LoadImageW.restype = HANDLE + +Shell_NotifyIconW = windll.Shell32.Shell_NotifyIconW +Shell_NotifyIconW.argtypes = [DWORD, POINTER(NOTIFYICONDATAW)] +Shell_NotifyIconW.restype = BOOL + +DestroyIcon = windll.User32.DestroyIcon +DestroyIcon.argtypes = [HICON] +DestroyIcon.restype = BOOL + +UnregisterClassW = windll.User32.UnregisterClassW +UnregisterClassW.argtypes = [ATOM, HINSTANCE] +UnregisterClassW.restype = BOOL + +DestroyWindow = windll.User32.DestroyWindow +DestroyWindow.argtypes = [HWND] +DestroyWindow.restype = BOOL + +LoadIconW = windll.User32.LoadIconW +LoadIconW.argtypes = [HINSTANCE, LPCWSTR] +LoadIconW.restype = HICON