Skip to content

Instantly share code, notes, and snippets.

@UserUnknownFactor
Created August 30, 2025 14:38
Show Gist options
  • Save UserUnknownFactor/eb7511045483d62f4b9b0e0abcd6e1a7 to your computer and use it in GitHub Desktop.
Save UserUnknownFactor/eb7511045483d62f4b9b0e0abcd6e1a7 to your computer and use it in GitHub Desktop.
Just another mod of Win10 Python toast notifier
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