dialog.rst
tkinter.messagebox.rst
tkinter.scrolledtext.rst
+ tkinter.systray.rst
tkinter.dnd.rst
tkinter.ttk.rst
idle.rst
: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.
--- /dev/null
+: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.
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.
--- /dev/null
+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()
--- /dev/null
+"""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)
--- /dev/null
+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.