Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Doc/library/tk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Doc/library/tkinter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
72 changes: 72 additions & 0 deletions Doc/library/tkinter.systray.rst
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,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`.)

xml
---

Expand Down
134 changes: 134 additions & 0 deletions Lib/test/test_tkinter/test_systray.py
Original file line number Diff line number Diff line change
@@ -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()
130 changes: 130 additions & 0 deletions Lib/tkinter/systray.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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.