https://github.com/python/cpython/commit/342e5a421841ad3e6614b71cba3b59ded67e3aa9 commit: 342e5a421841ad3e6614b71cba3b59ded67e3aa9 branch: main author: Serhiy Storchaka <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-07-11T08:34:13+03:00 summary:
gh-153259: Add tkinter.systray -- system tray icon and notifications (GH-153260) Add the tkinter.systray module with the SysTrayIcon class wrapping the "tk systray" command and the notify() function wrapping "tk sysnotify" (Tk 8.7/9.0 and newer). Co-authored-by: Claude Opus 4.8 <[email protected]> files: A Doc/library/tkinter.systray.rst A Lib/test/test_tkinter/test_systray.py A Lib/tkinter/systray.py A Misc/NEWS.d/next/Library/2026-07-06-14-05-40.gh-issue-153259.sysTr1.rst M Doc/library/tk.rst M Doc/library/tkinter.rst M Doc/whatsnew/3.16.rst diff --git a/Doc/library/tk.rst b/Doc/library/tk.rst index fa3c7e910ce21f..c835f2518f7bf6 100644 --- a/Doc/library/tk.rst +++ b/Doc/library/tk.rst @@ -36,6 +36,7 @@ alternative `GUI frameworks and tools <https://wiki.python.org/moin/GuiProgrammi dialog.rst tkinter.messagebox.rst tkinter.scrolledtext.rst + tkinter.systray.rst tkinter.dnd.rst tkinter.ttk.rst idle.rst diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index f72912632deb92..e989385aa80275 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -155,6 +155,9 @@ The modules that provide Tk support include: :mod:`tkinter.simpledialog` Basic dialogs and convenience functions. +:mod:`tkinter.systray` + System tray icon and desktop notifications. + :mod:`tkinter.ttk` Themed widget set introduced in Tk 8.5, providing modern alternatives for many of the classic widgets in the main :mod:`!tkinter` module. diff --git a/Doc/library/tkinter.systray.rst b/Doc/library/tkinter.systray.rst new file mode 100644 index 00000000000000..22ecbf5b71d6aa --- /dev/null +++ b/Doc/library/tkinter.systray.rst @@ -0,0 +1,72 @@ +:mod:`!tkinter.systray` --- System tray icon and notifications +============================================================== + +.. module:: tkinter.systray + :synopsis: System tray icon and desktop notifications + +**Source code:** :source:`Lib/tkinter/systray.py` + +.. versionadded:: next + +-------------- + +The :mod:`!tkinter.systray` module provides the :class:`SysTrayIcon` class +as an interface to the system tray (or taskbar) icon, +and the :func:`notify` function which sends a desktop notification. +They require Tk 8.7/9.0 or newer. + +Only one system tray icon is supported per Tcl interpreter. + +.. class:: SysTrayIcon(master=None, *, exists=False, **options) + + The class implementing the system tray icon. + + With *exists* false (the default), a new icon is created; + creating a second one raises :exc:`~tkinter.TclError`. + With *exists* true, the instance refers to the already-existing icon + instead of creating one, reconfiguring it with any given options. + + The supported configuration options are: + + * *image* --- the image displayed in the system tray + (required when creating an icon). + On Windows, it must be a :class:`!PhotoImage`. + * *text* --- the text displayed in the tooltip of the icon. + * *button1* --- a callback that is called without arguments + when the icon is clicked with the left mouse button. + * *button3* --- a callback that is called without arguments + when the icon is clicked with the right mouse button. + + .. method:: configure(**options) + config(**options) + + Query or modify the options of the system tray icon. + With no arguments, return a dict of all option values. + With a string argument, return the value of that option. + Otherwise, set the given options. + + .. method:: cget(option) + + Return the value of the given option of the system tray icon. + + .. method:: exists() + + Return whether the system tray icon exists. + + .. method:: destroy() + + Destroy the system tray icon. + A new icon can be created afterwards. + + .. method:: notify(title, message) + + Send a desktop notification with the given title and message. + + +.. function:: notify(title, message, *, master=None) + + Send a desktop notification with the given title and message + without creating a system tray icon first. + On Windows, sending a notification requires an existing system + tray icon, which is also displayed in the notification; + use the :meth:`SysTrayIcon.notify` method instead. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index af4e7f6fda59fc..48573eff9827fe 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -461,6 +461,12 @@ tkinter them. (Contributed by Serhiy Storchaka in :gh:`59396`.) +* Added the :mod:`tkinter.systray` module which provides the + :class:`~tkinter.systray.SysTrayIcon` class as an interface to the system + tray icon and the :func:`~tkinter.systray.notify` function which sends a + desktop notification. They require Tk 8.7/9.0 or newer. + (Contributed by Serhiy Storchaka in :gh:`153259`.) + * :class:`tkinter.scrolledtext.ScrolledText` gained a *use_ttk* parameter to use the themed :mod:`tkinter.ttk` frame and scroll bar instead of the classic :mod:`tkinter` widgets. diff --git a/Lib/test/test_tkinter/test_systray.py b/Lib/test/test_tkinter/test_systray.py new file mode 100644 index 00000000000000..809034747a3ab8 --- /dev/null +++ b/Lib/test/test_tkinter/test_systray.py @@ -0,0 +1,134 @@ +import unittest +import tkinter +from tkinter.systray import SysTrayIcon, notify +from test.support import requires +from test.test_tkinter.support import (AbstractTkTest, + AbstractDefaultRootTest, + requires_tk, + setUpModule) # noqa: F401 + +requires('gui') + + +class SysTrayIconTest(AbstractTkTest, unittest.TestCase): + + def setUp(self): + super().setUp() + self.image = tkinter.PhotoImage(master=self.root, + width=16, height=16) + + def create(self, **kwargs): + try: + icon = SysTrayIcon(self.root, image=self.image, **kwargs) + except tkinter.TclError as e: + self.skipTest(f'cannot create a system tray icon: {e}') + self.addCleanup(self._destroy, icon) + return icon + + def _destroy(self, icon): + try: + icon.destroy() + except tkinter.TclError: + pass + + @requires_tk(8, 7) + def test_create(self): + icon = self.create(text='tooltip') + self.assertTrue(icon.exists()) + self.assertEqual(str(icon.cget('image')), str(self.image)) + self.assertEqual(icon.cget('text'), 'tooltip') + + @requires_tk(8, 7) + def test_create_requires_image(self): + with self.assertRaises(TypeError): + SysTrayIcon(self.root) + + @requires_tk(8, 7) + def test_exists_argument(self): + icon = self.create(text='tooltip') + # exists=True refers to the already-created icon without creating + # a new one (which would raise the singleton error). + icon2 = SysTrayIcon(self.root, exists=True) + self.assertTrue(icon2.exists()) + self.assertEqual(icon2.cget('text'), 'tooltip') + # It can reconfigure the existing icon. + SysTrayIcon(self.root, exists=True, text='new') + self.assertEqual(icon.cget('text'), 'new') + + @requires_tk(8, 7) + def test_singleton(self): + self.create() + with self.assertRaisesRegex(tkinter.TclError, + 'only one system tray icon'): + SysTrayIcon(self.root, image=self.image) + + @requires_tk(8, 7) + def test_configure(self): + icon = self.create(text='old') + icon.configure(text='new') + self.assertEqual(icon.cget('text'), 'new') + options = icon.configure() + self.assertIsInstance(options, dict) + self.assertLessEqual({'image', 'text', 'button1', 'button3'}, + options.keys()) + + @requires_tk(8, 7) + def test_callbacks(self): + clicks = [] + icon = self.create(button1=lambda: clicks.append(1)) + name = icon._command_names['button1'] + # The registered callback is called without arguments. + self.root.tk.call(name) + self.assertEqual(clicks, [1]) + # Replacing the callback deletes the old Tcl command. + icon.configure(button1=lambda: clicks.append(2)) + self.assertFalse(self.root.tk.call('info', 'commands', name)) + self.root.tk.call(icon._command_names['button1']) + self.assertEqual(clicks, [1, 2]) + # Removing the callback deletes the Tcl command. + name = icon._command_names['button1'] + icon.configure(button1=None) + self.assertNotIn('button1', icon._command_names) + self.assertFalse(self.root.tk.call('info', 'commands', name)) + + @requires_tk(8, 7) + def test_destroy(self): + icon = self.create(button1=lambda: None) + name = icon._command_names['button1'] + icon.destroy() + self.assertFalse(icon.exists()) + self.assertFalse(self.root.tk.call('info', 'commands', name)) + # A new icon can be created after the old one was destroyed. + icon2 = self.create() + self.assertTrue(icon2.exists()) + + @requires_tk(8, 7) + def test_notify(self): + if self.root._windowingsystem != 'x11': + self.skipTest('cannot safely send a native notification') + icon = self.create() + # Sends a real desktop notification. + icon.notify('Python test', 'tkinter.systray test notification') + + +class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase): + + @requires_tk(8, 7) + def test_systray(self): + root = tkinter.Tk() + image = tkinter.PhotoImage(master=root, width=16, height=16) + try: + icon = SysTrayIcon(image=image) + except tkinter.TclError as e: + root.destroy() + self.skipTest(f'cannot create a system tray icon: {e}') + self.assertIs(icon.master, root) + icon.destroy() + root.destroy() + tkinter.NoDefaultRoot() + self.assertRaises(RuntimeError, SysTrayIcon, image='none') + self.assertRaises(RuntimeError, notify, 'title', 'message') + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/tkinter/systray.py b/Lib/tkinter/systray.py new file mode 100644 index 00000000000000..680879c4f61aa8 --- /dev/null +++ b/Lib/tkinter/systray.py @@ -0,0 +1,130 @@ +"""Interface to the system tray icon and desktop notifications. + +The SysTrayIcon class gives access to the "tk systray" command and +the notify() function gives access to the "tk sysnotify" command. +They require Tk 8.7/9.0 or newer. + +Only one system tray icon is supported per Tcl interpreter. + +On Windows, sending a notification requires that the system tray icon +has been created first; the icon is also displayed in the +notification. +""" + +import tkinter + +__all__ = ["SysTrayIcon", "notify"] + + +class SysTrayIcon: + """The system tray icon. + + Only one system tray icon is supported per Tcl interpreter. + With exists false (the default) a new icon is created, and creating + a second one raises TclError. With exists true this refers to the + already-existing icon instead of creating one. + + Supported configuration options are: + + image: the image displayed in the system tray (required when + creating an icon; it must be a photo image on Windows) + text: the text displayed in the tooltip of the icon + button1: a callback that is called when the icon is clicked + with the left mouse button + button3: a callback that is called when the icon is clicked + with the right mouse button + """ + + def __init__(self, master=None, *, exists=False, **options): + if master is None: + master = tkinter._get_default_root('use the system tray icon') + self.master = master + self._command_names = {} + if exists: + # Refer to the already-existing icon, reconfiguring it if + # any options are given. + if options: + self._call('configure', options) + else: + if options.get('image') is None: + raise TypeError( + "the 'image' argument is required to create an icon") + self._call('create', options) + + def _call(self, subcommand, cnf): + # Call "tk systray" with the given options, registering and + # unregistering the callback commands as needed. + master = self.master + cnf = dict(cnf) + new_names = {} + for key in ('button1', 'button3'): + if key in cnf: + command = cnf[key] + if command is None: + cnf[key] = '' + elif callable(command): + new_names[key] = cnf[key] = master._register(command) + try: + master.tk.call('tk', 'systray', subcommand, + *master._options(cnf)) + except tkinter.TclError: + for name in new_names.values(): + master.deletecommand(name) + raise + for key in ('button1', 'button3'): + if key in cnf: + old_name = self._command_names.pop(key, None) + if old_name is not None: + master.deletecommand(old_name) + if key in new_names: + self._command_names[key] = new_names[key] + + def configure(self, cnf=None, **kw): + """Query or modify the options of the system tray icon. + + With no arguments, return a dict of all option values. With a + string argument, return the value of that option. Otherwise, + set the given options. + """ + if kw: + cnf = tkinter._cnfmerge((cnf, kw)) + elif cnf: + cnf = tkinter._cnfmerge(cnf) + tk = self.master.tk + if cnf is None: + items = tk.splitlist(tk.call('tk', 'systray', 'configure')) + return {items[i][1:]: items[i+1] for i in range(0, len(items), 2)} + if isinstance(cnf, str): + return tk.call('tk', 'systray', 'configure', '-' + cnf) + self._call('configure', cnf) + config = configure + + def cget(self, option): + """Return the value of the given option of the system tray icon.""" + return self.master.tk.call('tk', 'systray', 'configure', '-' + option) + + def exists(self): + """Return whether the system tray icon exists.""" + tk = self.master.tk + return tk.getboolean(tk.call('tk', 'systray', 'exists')) + + def destroy(self): + """Destroy the system tray icon.""" + self.master.tk.call('tk', 'systray', 'destroy') + for name in self._command_names.values(): + self.master.deletecommand(name) + self._command_names.clear() + + def notify(self, title, message): + """Send a desktop notification with the given title and message.""" + self.master.tk.call('tk', 'sysnotify', title, message) + + +def notify(title, message, *, master=None): + """Send a desktop notification with the given title and message. + + On Windows, the system tray icon must have been created first. + """ + if master is None: + master = tkinter._get_default_root('send a notification') + master.tk.call('tk', 'sysnotify', title, message) diff --git a/Misc/NEWS.d/next/Library/2026-07-06-14-05-40.gh-issue-153259.sysTr1.rst b/Misc/NEWS.d/next/Library/2026-07-06-14-05-40.gh-issue-153259.sysTr1.rst new file mode 100644 index 00000000000000..2355826ab4b2de --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-06-14-05-40.gh-issue-153259.sysTr1.rst @@ -0,0 +1,4 @@ +Added the :mod:`tkinter.systray` module which provides the +:class:`~tkinter.systray.SysTrayIcon` class as an interface to the system +tray icon and the :func:`~tkinter.systray.notify` function which sends a +desktop notification. They require Tk 8.7/9.0 or newer. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
