Created
August 30, 2025 14:38
-
-
Save UserUnknownFactor/eb7511045483d62f4b9b0e0abcd6e1a7 to your computer and use it in GitHub Desktop.
Just another mod of Win10 Python toast notifier
This file contains hidden or 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
from __future__ import absolute_import | |
from __future__ import print_function | |
from __future__ import unicode_literals | |
__all__ = ['ToastNotifier'] | |
# ############################################################################# | |
# ########## Libraries ############# | |
# ################################## | |
# standard library | |
import logging | |
import threading | |
from time import time, sleep | |
from os import path | |
from pkg_resources import Requirement | |
from pkg_resources import resource_filename | |
# 3rd party modules | |
from win32api import GetModuleHandle | |
from win32api import PostQuitMessage | |
from win32api import PostMessage | |
from win32con import CW_USEDEFAULT | |
from win32con import IDI_APPLICATION | |
from win32con import IMAGE_ICON | |
from win32con import LR_DEFAULTSIZE | |
from win32con import LR_LOADFROMFILE | |
from win32con import WM_DESTROY | |
from win32con import WM_USER | |
from win32con import WS_OVERLAPPED | |
from win32con import WS_SYSMENU | |
from win32gui import CreateWindow | |
from win32gui import DestroyWindow | |
from win32gui import LoadIcon | |
from win32gui import LoadImage | |
from win32gui import NIF_ICON | |
from win32gui import NIF_INFO | |
from win32gui import NIF_MESSAGE | |
from win32gui import NIF_TIP | |
from win32gui import NIM_ADD | |
from win32gui import NIM_DELETE | |
from win32gui import NIM_MODIFY | |
from win32gui import RegisterClass | |
from win32gui import UnregisterClass | |
from win32gui import Shell_NotifyIcon | |
from win32gui import UpdateWindow | |
from win32gui import WNDCLASS | |
from win32gui import PumpMessages | |
# Magic constants | |
PARAM_DESTROY = 1028 | |
PARAM_CLICKED = 1029 | |
WM_CUSTOM_TIMER = WM_USER + 20 | |
WM_TIMER_CLOSE = WM_USER + 21 | |
#logging.basicConfig( | |
# level=logging.DEBUG, | |
# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | |
#) | |
# ############################################################################ | |
# ########### Classes ############## | |
# ################################## | |
class ToastNotifier(object): | |
"""Creates a Windows 10 toast notification""" | |
def __init__(self): | |
"""Initialize.""" | |
self._thread = None | |
self._hwnd = None | |
self._callback = None | |
self._timer = None | |
def _with_callback(self, callback=None): | |
""" | |
:param func: callable to decorate | |
:param callback: callable to run on mouse click within notification window | |
:return: callable | |
:example: | |
t = ToastNotifier() | |
def on_toast_click(): | |
print("The user clicked the toast notification!") | |
show_cb = t._with_callback(on_toast_click) | |
show_cb(title="Hello", msg="Click me!") | |
""" | |
def inner(self, *args, **kwargs): | |
kwargs.update({'callback': callback}) | |
self.show_toast(*args, **kwargs) | |
return inner | |
def _show_toast(self, title, msg, icon_path, duration, callback=None): | |
"""Notification settings. | |
:title: notification title | |
:msg: notification message | |
:icon_path: path to the .ico file to custom notification | |
:duration: delay in seconds before notification self-destruction or None | |
:callback: callback on click | |
""" | |
self._callback = callback # store callback in instance | |
self._hwnd = None # Reset hwnd | |
# Register the window class. | |
self.wc = WNDCLASS() | |
self.hinst = self.wc.hInstance = GetModuleHandle(None) | |
self.wc.lpszClassName = str("PythonTaskbar" + str(time()).replace('.', '')) | |
self.wc.lpfnWndProc = self.win_proc | |
try: | |
self.classAtom = RegisterClass(self.wc) | |
except Exception as e: | |
logging.error("Some trouble with classAtom ({})".format(e)) | |
return | |
STYLE = WS_OVERLAPPED | WS_SYSMENU | |
ICON_FLAGS = LR_LOADFROMFILE | LR_DEFAULTSIZE | |
self._hwnd = CreateWindow(self.classAtom, "Taskbar", STYLE, | |
0, 0, CW_USEDEFAULT, CW_USEDEFAULT, | |
0, 0, self.hinst, None) | |
UpdateWindow(self._hwnd) | |
# icon | |
hicon = None | |
if icon_path is not None and path.isfile(icon_path): | |
icon_path = path.realpath(icon_path) | |
try: | |
hicon = LoadImage(self.hinst, icon_path, IMAGE_ICON, 0, 0, ICON_FLAGS) | |
except Exception as e: | |
logging.error("Error loading custom icon ({}): {}".format(icon_path, e)) | |
hicon = None | |
if hicon is None: | |
default_icon_path = path.join(path.dirname(path.realpath(__file__)), "data", "python.ico") | |
if path.isfile(default_icon_path): | |
try: | |
hicon = LoadImage(self.hinst, default_icon_path, IMAGE_ICON, 0, 0, ICON_FLAGS) | |
except Exception as e: | |
logging.error("Error loading default icon ({}): {}".format(default_icon_path, e)) | |
hicon = None | |
if hicon is None: | |
try: | |
hicon = LoadIcon(0, IDI_APPLICATION) | |
except Exception as e: | |
logging.error("Error loading system icon: {}".format(e)) | |
# Taskbar icon | |
flags = NIF_ICON | NIF_MESSAGE | NIF_TIP | |
nid = (self._hwnd, 0, flags, WM_CUSTOM_TIMER, hicon, "Tooltip") | |
Shell_NotifyIcon(NIM_ADD, nid) | |
Shell_NotifyIcon(NIM_MODIFY, (self._hwnd, 0, NIF_INFO, | |
WM_CUSTOM_TIMER, | |
hicon, "Balloon Tooltip", msg, 200, | |
title)) | |
if duration is not None and duration > 0: | |
def close_notification(): | |
sleep(duration) | |
if self._hwnd: | |
try: | |
PostMessage(self._hwnd, WM_TIMER_CLOSE, 0, 0) | |
except Exception as e: | |
logging.error("Error posting timer close message: {}".format(e)) | |
self._timer = threading.Thread(target=close_notification) | |
self._timer.daemon = True | |
self._timer.start() | |
PumpMessages() | |
self._cleanup() | |
def _cleanup(self): | |
"""Clean up resources""" | |
self._timer = None | |
self._hwnd = None | |
try: | |
if hasattr(self, 'classAtom') and hasattr(self, 'hinst'): | |
UnregisterClass(self.wc.lpszClassName, self.hinst) | |
except Exception as e: | |
logging.error("Error unregistering class: {}".format(e)) | |
def _destroy_window(self): | |
"""Safely destroy the notification window""" | |
if self._hwnd: | |
try: | |
nid = (self._hwnd, 0) | |
Shell_NotifyIcon(NIM_DELETE, nid) | |
DestroyWindow(self._hwnd) | |
except Exception as e: | |
logging.error("Error destroying window: {}".format(e)) | |
finally: | |
self._hwnd = None | |
def show_toast(self, title="Notification", msg="Here comes the message", | |
icon_path=None, duration=5, threaded=False, callback=None): | |
"""Notification settings. | |
:title: notification title | |
:msg: notification message | |
:icon_path: path to the .ico file to custom notification | |
:duration: delay in seconds before notification self-destruction or None (for no auto-close) | |
:callback: callable to be called on notification click | |
""" | |
if not threaded: | |
self._show_toast(title, msg, icon_path, duration, callback) | |
else: | |
if self.notification_active(): | |
# Have an active notification, let it finish so we don't spam them | |
return False | |
self._thread = threading.Thread( | |
target=self._show_toast, | |
args=(title, msg, icon_path, duration, callback) | |
) | |
self._thread.daemon = True | |
self._thread.start() | |
return True | |
def notification_active(self): | |
"""See if we have an active notification showing""" | |
if self._thread is not None and self._thread.is_alive(): | |
# We have an active notification, let it finish so we don't spam them | |
return True | |
return False | |
def win_proc(self, hwnd, msg, wparam, lparam): | |
"""Window procedure to handle messages""" | |
if msg == WM_CUSTOM_TIMER: | |
if lparam == PARAM_CLICKED: | |
if self._callback: | |
try: | |
self._callback() | |
except Exception as e: | |
logging.error("Error in callback: {}".format(e)) | |
PostMessage(self._hwnd, WM_TIMER_CLOSE, 0, 0) | |
return 0 | |
elif msg == WM_TIMER_CLOSE: | |
if self._hwnd: | |
nid = (self._hwnd, 0) | |
Shell_NotifyIcon(NIM_DELETE, nid) | |
DestroyWindow(self._hwnd) | |
return 0 | |
elif msg == WM_DESTROY: | |
self._hwnd = None | |
PostQuitMessage(0) | |
return 0 | |
return 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment