diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index ca034fcdc09b439..fb97c12e48059d1 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -1894,6 +1894,31 @@ Base and mixin classes .. versionadded:: next + The following members support assistive technologies such as screen + readers. + They require Tk 9.1 or newer. + Tk automatically exposes the standard widgets to the platform + accessibility API; + the :attr:`!accessible` attributes allow overriding what is exposed, + and the :class:`Accessible` methods allow registering custom widgets. + If no assistive technology is active when the application starts, + setting the attributes has no effect, + reading them returns ``0``, + and the methods do nothing (except :meth:`!tk_check_screenreader`). + + .. method:: tk_check_screenreader() + + Return whether a screen reader is currently running. + + .. versionadded:: next + + .. attribute:: accessible + + The accessible attributes of the widget, + as an :class:`Accessible` object. + + .. versionadded:: next + The methods with the ``busy_`` prefix manage the busy state of a window, which shows a busy cursor and ignores user input. @@ -2323,6 +2348,51 @@ Base and mixin classes .. versionadded:: 3.11 +.. class:: Accessible() + + The accessible attributes of a widget, + returned by the :attr:`~Misc.accessible` property of the widget. + The attributes are exposed to assistive technologies + such as screen readers. + Requires Tk 9.1 or newer. + + Setting an attribute overrides what Tk exposes for the widget; + reading an attribute that has not been set raises + :exc:`~tkinter.TclError`. + The supported roles are ``Button``, ``Canvas``, ``Checkbutton``, + ``Combobox``, ``Entry``, ``Label``, ``Listbox``, ``Menu``, + ``Notebook``, ``Progressbar``, ``Scale``, ``Scrollbar``, + ``Spinbox``, ``Table``, ``Text`` and ``Tree``. + + .. versionadded:: next + + .. attribute:: role + name + description + value + state + action + help + + The accessible attributes of the widget. + + .. method:: add_object() + + Register the widget with the platform accessibility API. + This is only needed for custom widgets; + the standard widgets are registered automatically. + + .. method:: emit_selection_change() + + Notify the platform accessibility API that the selection in the + widget has changed. + + .. method:: emit_focus_change() + + Notify the platform accessibility API that the widget has received + focus. + Not available on macOS. + .. class:: Wm() diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index e8c75d2f571061a..d5b01aa57090e34 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -358,6 +358,15 @@ tkinter report the user idle time. (Contributed by Serhiy Storchaka in :gh:`151881`.) +* Added :mod:`tkinter` support for the Tk 9.1 accessibility API: the + :attr:`~tkinter.Misc.accessible` property of widgets returns an + :class:`~tkinter.Accessible` object whose attributes are exposed to + assistive technologies such as screen readers and whose methods register + custom widgets with the platform accessibility API, and the + :meth:`~tkinter.Misc.tk_check_screenreader` method reports whether a + screen reader is running. + (Contributed by Serhiy Storchaka in :gh:`145578`.) + * Added new window-management methods :meth:`~tkinter.Misc.winfo_isdark` (dark mode detection), :meth:`~tkinter.Wm.wm_iconbadge` (application icon badge) and :meth:`~tkinter.Wm.wm_stackorder` (toplevel stacking order). diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index 5f75b8d245b3e99..500854ecc3e5c91 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -599,6 +599,42 @@ def test_tk_inactive(self): # Resetting the timer returns None and does not raise. self.assertIsNone(self.root.tk_inactive(reset=True)) + @requires_tk(9, 1) + def test_accessible(self): + # Without an active screen reader every "tk accessible" call is + # a no-op returning 0, so only test that the calls do not raise + # and check the values when a screen reader is active. + try: + sr = self.root.tk_check_screenreader() + except tkinter.TclError: + # Tk 9.1a0 or Tk compiled without accessibility support + # (e.g. without ATK on X11). + self.skipTest('the "tk accessible" command is not supported') + self.assertIsInstance(sr, bool) + f = tkinter.Frame(self.root) + acc = f.accessible + self.assertIsInstance(acc, tkinter.Accessible) + self.assertIs(f.accessible, acc) + if sr: + # Reading an attribute that has not been set raises TclError. + with self.assertRaises(tkinter.TclError): + acc.help + for attr in ('name', 'description', 'value', 'state', + 'action', 'help'): + with self.subTest(attr=attr): + setattr(acc, attr, 'spam') + result = getattr(acc, attr) + if sr: + self.assertEqual(result, 'spam') + acc.role = 'Label' + result = acc.role + if sr: + self.assertEqual(result, 'Label') + acc.add_object() + acc.emit_selection_change() + if self.root._windowingsystem != 'aqua': + acc.emit_focus_change() + def test_wait_variable(self): var = tkinter.StringVar(self.root) self.assertEqual(self.root.waitvar, self.root.wait_variable) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 83d11a7695ffbe3..06927e7e03f54e9 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -818,6 +818,25 @@ def tk_inactive(self, reset=False, *, displayof=0): else: return self.tk.getint(self.tk.call(args)) + def tk_check_screenreader(self): + """Return whether a screen reader is currently running. + + Requires Tk 9.1 or newer.""" + return self.tk.getboolean(self.tk.call( + 'tk', 'accessible', 'check_screenreader')) + + @property + def accessible(self): + """The accessible attributes of the widget, as an Accessible + object. + + Requires Tk 9.1 or newer.""" + try: + return self._accessible_object + except AttributeError: + self._accessible_object = Accessible(self) + return self._accessible_object + def wait_variable(self, name): """Wait until the variable is modified. @@ -2307,6 +2326,67 @@ def yview_scroll(self, number, what): self.tk.call(self._w, 'yview', 'scroll', number, what) +def _accessible_attribute(name): + """Internal function. Create an Accessible attribute property.""" + def getter(self): + widget = self._widget + return widget.tk.call('tk', 'accessible', 'get_acc_' + name, + widget._w) + def setter(self, value): + widget = self._widget + widget.tk.call('tk', 'accessible', 'set_acc_' + name, + widget._w, value) + return property(getter, setter, + doc=f'The accessible {name} of the widget.') + + +class Accessible: + """The accessible attributes of a widget. + + Returned by the accessible property of a widget. The attributes + are exposed to assistive technologies such as screen readers. + Requires Tk 9.1 or newer. + + If no assistive technology is active when the application starts, + setting an attribute has no effect and reading it returns 0. + Reading an attribute that has not been set raises TclError. + """ + + def __init__(self, widget): + self._widget = widget + + def __repr__(self): + return f'' + + role = _accessible_attribute('role') + name = _accessible_attribute('name') + description = _accessible_attribute('description') + value = _accessible_attribute('value') + state = _accessible_attribute('state') + action = _accessible_attribute('action') + help = _accessible_attribute('help') + + def add_object(self): + """Register the widget with the platform accessibility API.""" + widget = self._widget + widget.tk.call('tk', 'accessible', 'add_acc_object', widget._w) + + def emit_selection_change(self): + """Notify the platform accessibility API that the selection + in the widget has changed.""" + widget = self._widget + widget.tk.call('tk', 'accessible', 'emit_selection_change', + widget._w) + + def emit_focus_change(self): + """Notify the platform accessibility API that the widget has + received focus. + + Not available on macOS.""" + widget = self._widget + widget.tk.call('tk', 'accessible', 'emit_focus_change', widget._w) + + class Wm: """Provides functions for the communication with the window manager.""" diff --git a/Misc/NEWS.d/next/Library/2026-07-06-14-38-02.gh-issue-145578.aX11yQ.rst b/Misc/NEWS.d/next/Library/2026-07-06-14-38-02.gh-issue-145578.aX11yQ.rst new file mode 100644 index 000000000000000..d32d6a80238fdd3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-06-14-38-02.gh-issue-145578.aX11yQ.rst @@ -0,0 +1,7 @@ +Added :mod:`tkinter` support for the Tk 9.1 accessibility API: the +:attr:`~tkinter.Misc.accessible` property of widgets returns an +:class:`~tkinter.Accessible` object whose attributes are exposed to +assistive technologies such as screen readers and whose methods register +custom widgets with the platform accessibility API, and the +:meth:`~tkinter.Misc.tk_check_screenreader` method reports whether a +screen reader is running.