mirror of
https://github.com/python/cpython.git
synced 2026-10-28 10:25:05 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
45fa07b025
commit
342e5a4218
7 files changed
+350
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user