Skip to content

Commit fd7a29e

Browse files
gh-145578: Add tkinter support for the Tk 9.1 accessibility API
The accessible property of widgets returns an Accessible object whose attributes are exposed to assistive technologies such as screen readers and whose methods register custom widgets with the platform accessibility API. The tk_check_screenreader() method reports whether a screen reader is running. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2092192 commit fd7a29e

5 files changed

Lines changed: 202 additions & 0 deletions

File tree

Doc/library/tkinter.rst

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1894,6 +1894,31 @@ Base and mixin classes
18941894

18951895
.. versionadded:: next
18961896

1897+
The following members support assistive technologies such as screen
1898+
readers.
1899+
They require Tk 9.1 or newer.
1900+
Tk automatically exposes the standard widgets to the platform
1901+
accessibility API;
1902+
the :attr:`!accessible` attributes allow overriding what is exposed,
1903+
and the :class:`Accessible` methods allow registering custom widgets.
1904+
If no assistive technology is active when the application starts,
1905+
setting the attributes has no effect,
1906+
reading them returns ``0``,
1907+
and the methods do nothing (except :meth:`!tk_check_screenreader`).
1908+
1909+
.. method:: tk_check_screenreader()
1910+
1911+
Return whether a screen reader is currently running.
1912+
1913+
.. versionadded:: next
1914+
1915+
.. attribute:: accessible
1916+
1917+
The accessible attributes of the widget,
1918+
as an :class:`Accessible` object.
1919+
1920+
.. versionadded:: next
1921+
18971922
The methods with the ``busy_`` prefix manage the busy state of a window,
18981923
which shows a busy cursor and ignores user input.
18991924

@@ -2323,6 +2348,51 @@ Base and mixin classes
23232348
.. versionadded:: 3.11
23242349

23252350

2351+
.. class:: Accessible()
2352+
2353+
The accessible attributes of a widget,
2354+
returned by the :attr:`~Misc.accessible` property of the widget.
2355+
The attributes are exposed to assistive technologies
2356+
such as screen readers.
2357+
Requires Tk 9.1 or newer.
2358+
2359+
Setting an attribute overrides what Tk exposes for the widget;
2360+
reading an attribute that has not been set raises
2361+
:exc:`~tkinter.TclError`.
2362+
The supported roles are ``Button``, ``Canvas``, ``Checkbutton``,
2363+
``Combobox``, ``Entry``, ``Label``, ``Listbox``, ``Menu``,
2364+
``Notebook``, ``Progressbar``, ``Scale``, ``Scrollbar``,
2365+
``Spinbox``, ``Table``, ``Text`` and ``Tree``.
2366+
2367+
.. versionadded:: next
2368+
2369+
.. attribute:: role
2370+
name
2371+
description
2372+
value
2373+
state
2374+
action
2375+
help
2376+
2377+
The accessible attributes of the widget.
2378+
2379+
.. method:: add_object()
2380+
2381+
Register the widget with the platform accessibility API.
2382+
This is only needed for custom widgets;
2383+
the standard widgets are registered automatically.
2384+
2385+
.. method:: emit_selection_change()
2386+
2387+
Notify the platform accessibility API that the selection in the
2388+
widget has changed.
2389+
2390+
.. method:: emit_focus_change()
2391+
2392+
Notify the platform accessibility API that the widget has received
2393+
focus.
2394+
Not available on macOS.
2395+
23262396

23272397
.. class:: Wm()
23282398

Doc/whatsnew/3.16.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,15 @@ tkinter
358358
report the user idle time.
359359
(Contributed by Serhiy Storchaka in :gh:`151881`.)
360360

361+
* Added :mod:`tkinter` support for the Tk 9.1 accessibility API: the
362+
:attr:`~tkinter.Misc.accessible` property of widgets returns an
363+
:class:`~tkinter.Accessible` object whose attributes are exposed to
364+
assistive technologies such as screen readers and whose methods register
365+
custom widgets with the platform accessibility API, and the
366+
:meth:`~tkinter.Misc.tk_check_screenreader` method reports whether a
367+
screen reader is running.
368+
(Contributed by Serhiy Storchaka in :gh:`145578`.)
369+
361370
* Added new window-management methods :meth:`~tkinter.Misc.winfo_isdark`
362371
(dark mode detection), :meth:`~tkinter.Wm.wm_iconbadge` (application icon
363372
badge) and :meth:`~tkinter.Wm.wm_stackorder` (toplevel stacking order).

Lib/test/test_tkinter/test_misc.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,42 @@ def test_tk_inactive(self):
599599
# Resetting the timer returns None and does not raise.
600600
self.assertIsNone(self.root.tk_inactive(reset=True))
601601

602+
@requires_tk(9, 1)
603+
def test_accessible(self):
604+
# Without an active screen reader every "tk accessible" call is
605+
# a no-op returning 0, so only test that the calls do not raise
606+
# and check the values when a screen reader is active.
607+
try:
608+
sr = self.root.tk_check_screenreader()
609+
except tkinter.TclError:
610+
# Tk 9.1a0 or Tk compiled without accessibility support
611+
# (e.g. without ATK on X11).
612+
self.skipTest('the "tk accessible" command is not supported')
613+
self.assertIsInstance(sr, bool)
614+
f = tkinter.Frame(self.root)
615+
acc = f.accessible
616+
self.assertIsInstance(acc, tkinter.Accessible)
617+
self.assertIs(f.accessible, acc)
618+
if sr:
619+
# Reading an attribute that has not been set raises TclError.
620+
with self.assertRaises(tkinter.TclError):
621+
acc.help
622+
for attr in ('name', 'description', 'value', 'state',
623+
'action', 'help'):
624+
with self.subTest(attr=attr):
625+
setattr(acc, attr, 'spam')
626+
result = getattr(acc, attr)
627+
if sr:
628+
self.assertEqual(result, 'spam')
629+
acc.role = 'Label'
630+
result = acc.role
631+
if sr:
632+
self.assertEqual(result, 'Label')
633+
acc.add_object()
634+
acc.emit_selection_change()
635+
if self.root._windowingsystem != 'aqua':
636+
acc.emit_focus_change()
637+
602638
def test_wait_variable(self):
603639
var = tkinter.StringVar(self.root)
604640
self.assertEqual(self.root.waitvar, self.root.wait_variable)

Lib/tkinter/__init__.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,25 @@ def tk_inactive(self, reset=False, *, displayof=0):
818818
else:
819819
return self.tk.getint(self.tk.call(args))
820820

821+
def tk_check_screenreader(self):
822+
"""Return whether a screen reader is currently running.
823+
824+
Requires Tk 9.1 or newer."""
825+
return self.tk.getboolean(self.tk.call(
826+
'tk', 'accessible', 'check_screenreader'))
827+
828+
@property
829+
def accessible(self):
830+
"""The accessible attributes of the widget, as an Accessible
831+
object.
832+
833+
Requires Tk 9.1 or newer."""
834+
try:
835+
return self._accessible_object
836+
except AttributeError:
837+
self._accessible_object = Accessible(self)
838+
return self._accessible_object
839+
821840
def wait_variable(self, name):
822841
"""Wait until the variable is modified.
823842
@@ -2307,6 +2326,67 @@ def yview_scroll(self, number, what):
23072326
self.tk.call(self._w, 'yview', 'scroll', number, what)
23082327

23092328

2329+
def _accessible_attribute(name):
2330+
"""Internal function. Create an Accessible attribute property."""
2331+
def getter(self):
2332+
widget = self._widget
2333+
return widget.tk.call('tk', 'accessible', 'get_acc_' + name,
2334+
widget._w)
2335+
def setter(self, value):
2336+
widget = self._widget
2337+
widget.tk.call('tk', 'accessible', 'set_acc_' + name,
2338+
widget._w, value)
2339+
return property(getter, setter,
2340+
doc=f'The accessible {name} of the widget.')
2341+
2342+
2343+
class Accessible:
2344+
"""The accessible attributes of a widget.
2345+
2346+
Returned by the accessible property of a widget. The attributes
2347+
are exposed to assistive technologies such as screen readers.
2348+
Requires Tk 9.1 or newer.
2349+
2350+
If no assistive technology is active when the application starts,
2351+
setting an attribute has no effect and reading it returns 0.
2352+
Reading an attribute that has not been set raises TclError.
2353+
"""
2354+
2355+
def __init__(self, widget):
2356+
self._widget = widget
2357+
2358+
def __repr__(self):
2359+
return f'<Accessible object of {self._widget!r}>'
2360+
2361+
role = _accessible_attribute('role')
2362+
name = _accessible_attribute('name')
2363+
description = _accessible_attribute('description')
2364+
value = _accessible_attribute('value')
2365+
state = _accessible_attribute('state')
2366+
action = _accessible_attribute('action')
2367+
help = _accessible_attribute('help')
2368+
2369+
def add_object(self):
2370+
"""Register the widget with the platform accessibility API."""
2371+
widget = self._widget
2372+
widget.tk.call('tk', 'accessible', 'add_acc_object', widget._w)
2373+
2374+
def emit_selection_change(self):
2375+
"""Notify the platform accessibility API that the selection
2376+
in the widget has changed."""
2377+
widget = self._widget
2378+
widget.tk.call('tk', 'accessible', 'emit_selection_change',
2379+
widget._w)
2380+
2381+
def emit_focus_change(self):
2382+
"""Notify the platform accessibility API that the widget has
2383+
received focus.
2384+
2385+
Not available on macOS."""
2386+
widget = self._widget
2387+
widget.tk.call('tk', 'accessible', 'emit_focus_change', widget._w)
2388+
2389+
23102390
class Wm:
23112391
"""Provides functions for the communication with the window manager."""
23122392

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Added :mod:`tkinter` support for the Tk 9.1 accessibility API: the
2+
:attr:`~tkinter.Misc.accessible` property of widgets returns an
3+
:class:`~tkinter.Accessible` object whose attributes are exposed to
4+
assistive technologies such as screen readers and whose methods register
5+
custom widgets with the platform accessibility API, and the
6+
:meth:`~tkinter.Misc.tk_check_screenreader` method reports whether a
7+
screen reader is running.

0 commit comments

Comments
 (0)