Skip to content

Prepare Tkinter 9.1 screenreader support #145578

Open

Description

@ThingamabobsCoding

Feature or enhancement

Proposal:

Tcl/Tk adds support for screen-readers in Tk 9.1:
https://www.tcl-lang.org/software/tcltk/9.1.html

The related TIP 733 can be found here:
https://core.tcl-lang.org/tips/doc/trunk/tip/733.md

The C-Api documentation is provided at least here:
https://github.com/tcltk/tk/blob/main/doc/accessible.n

And the actual implementation is here:
https://github.com/tcltk/tk/blob/main/generic/tkAccessibility.c

Even though default values for standard widgets will be present custom widgets drawn by a Canvas can't be foreseen and a user might wish to alternate the defaults.
https://core.tcl-lang.org/tk/tktview/1110144ae9

Since tkinter is a thin wrapper around tk the appropriated API should be something like this:

def accessible(window, **kwargs):
    sub = [
        'role', 'name', 'description', 'value',
        'state', 'action', 'help string'
        ]
    parse = ()
    for cmd in sub:
        if val:= kwargs.pop(cmd, None):
            parse = parse + (cmd, val)
    if kwargs:
        raise ValueError
    window._tk.call('accessible', str(window), *parse)

Might best be placed into Misc.

Has this already been discussed elsewhere?

No response given

Links to previous discussion of this feature:

No response

Linked PRs

Activity

  1. ThingamabobsCoding commented on Mar 6, 2026

    @ThingamabobsCoding
    Author

    After looking around, the above code won't work. The implementation actually looks a little bit inconvenient:

    https://github.com/tcltk/tk/blob/main/library/accessibility.tcl#L648

    	    ::tk::accessible::set_acc_role $w $role
    	    ::tk::accessible::set_acc_name $w $name
    	    ::tk::accessible::set_acc_description $w $description
    	    ::tk::accessible::set_acc_value $w $value
    	    ::tk::accessible::set_acc_state $w $state
    	    ::tk::accessible::set_acc_action $w $action 
    

    So maybe a whole new class makes more sense. Sorry for the confusion

    List of exported procs are:
    [tk::accessible::set_acc_role]
    [tk::accessible::set_acc_name]
    [tk::accessible::set_acc_description]
    [tk::accessible::set_acc_value]
    [tk::accessible::set_acc_state]
    [tk::accessible::set_acc_action]
    [tk::accessible::set_acc_help]
    [tk::accessible::get_acc_role]
    [tk::accessible::get_acc_name]
    [tk::accessible::get_acc_description]
    [tk::accessible::get_acc_value]
    [tk::accessible::get_acc_state]
    [tk::accessible::get_acc_action]
    [tk::accessible::get_acc_help]
    [tk::accessible::add_acc_object]
    [tk::accessible::emit_selection_change]
    [tk::accessible::check_screenreader]
    [tk::accessible::emit_focus_change]

    not exported but useful:

    tk::accessible::speak
    tk::accessible::_init

  2. picnixz commented on Mar 6, 2026

    @picnixz
    Member

    @serhiy-storchaka Do we strive to support the latest tkinter or the minimal one in general?

  3. added
    stdlibStandard Library Python modules in the Lib/ directory
    on Mar 6, 2026
  4. ThingamabobsCoding commented on Mar 7, 2026

    @ThingamabobsCoding
    Author

    The following code should work and fit into Misc, it is similar implemented as _bind and provides some docstrings and comment.
    Hope this is sufficient. Also note that the methods are exported. It is my understanding that they are exported into the global namespace as every other ::tk and therefor it could be written as self._tk.call(method, str(self). Since I can not test it I leave it like this and let you do the testing.

    Also see:
    https://www.w3.org/WAI/WCAG22/Techniques/pdf/PDF12
    https://core.tcl-lang.org/tk/tktview/94473fdd48b3b0b0ce36d08da612ce13953c6b63

    def _accessible(self, method, value=None):
        if not value:
            return self._tk.eval(f'::tk::accessible::{method} {self}')
        return self._tk.eval(f'::tk::accessible::{method} {self} {value}')
    
    def accessible_set_acc_role(self, value):
        'Sets the role parameter for this widget'
        # Should be set to something that describes the widget e.g "Button" 
        return self._accessible('set_acc_role', value)
    
    def accessible_get_acc_role(self):
        'Gets the role parameter for this widget'
        return self._accessible('get_acc_role')
    
    def accessible_set_acc_name(self, value):
        'Sets the name parameter for this widget'
        # Should be set to something that identifies the widget e.g "SearchButton" 
        return self._accessible('set_acc_name', value)
    
    def accessible_get_acc_name(self):
        'Gets the name parameter for this widget'
        return self._accessible('get_acc_name')
    
    def accessible_set_acc_description(self, value):
        'Sets the description parameter for this widget'
        # Should be set to a short description for audio-description'
        return self._accessible('set_acc_description', value)
    
    def accessible_get_acc_description(self):
        'Gets the description parameter for this widget'
        return self._accessible('get_acc_description')
    
    def accessible_set_acc_value(self, value):
        'Sets the value parameter for this widget'
        # Should be set to something that identifies the functionality e.g "Search"
        return self._accessible('set_acc_value', value)
    
    def accessible_get_acc_value(self):
        'Gets the value parameter for this widget'
        return self._accessible('get_acc_value')
    
    def accessible_set_acc_state(self, value):
        'Sets the state parameter for this widget'
        # Should bet set to something like "selected"
        return self._accessible('set_acc_state', value)
    
    def accessible_get_acc_state(self):
        'Gets the state parameter for this widget'
        return self._accessible('get_acc_state')
    
    def accessible_set_acc_action(self, value):
        'Sets the action parameter of this widget'
        # Should be set to a callable object that can be invoked
        return self._accessible('set_acc_action', value)
    
    def accessible_get_acc_action(self):
        'Gets the action parameter of this widget'
        return self._accessible('get_acc_action')
    
    def accessible_set_acc_help(self, value):
        'Sets the help parameter of this widget'
        # Should be set to a short decription how to interact (handle/navigate)
        return self._accessible('set_acc_help', value)
    
    def accessible_get_acc_help(self):
        'Gets the help parameter of this widget'
        return self._accessible('get_acc_help')
    
    ## https://github.com/tcltk/tk/blob/main/library/accessibility.tcl#L1109
    ## def accessible_add_acc_object(self, value): 
    ##     'Activate the widget to be accessible'
    ##     # Canvases or Frames are not accessible by default
    ##     # replace value with self ?
    ##     return self._accessible('add_acc_object', value)
    
    def accessible_emit_selection_change(self):
        'Posts a notification to the screenreader that the selection is updated'
        return self._accessible('emit_selection_change')
    
    def accessible_check_screenreader(self):
        'Checks if there is a screenreader is currently running'
        return self._accessible('check_screenreader')
    
    def accessible_emit_focus_change(self):
        'Posts a notification to the screenreader that the focus is updated'
        return self._accessible('emit_focus_change')
    
  5. serhiy-storchaka commented on Mar 7, 2026

    @serhiy-storchaka
    Member

    Use call() instead of eval().

    For now, Tkinter does not provide any wrappers for commands with the "::tk::" prefix (perhaps because most of them were with the "::tk::unsupported::" prefix). The user can just use call(). They look like some implementation details. So, there is no currently any precedence for naming and organizing such methods. If we start adding such methods or functions, we should first add wrappers for older, more stable commands.

    Since the user always can use call(), I advise you not to rush, but to see how it goes.

  6. serhiy-storchaka commented on Mar 8, 2026

    @serhiy-storchaka
    Member

    It seems that these commands are also exposed as subcommands of the tk accessible command. That make them more public. But we should first add support for tk fontchooser and tk systray.

  7. ThingamabobsCoding commented on Mar 8, 2026

    @ThingamabobsCoding
    Author

    @serhiy-storchaka as far as I'm informed systray is available in tk 8.7, see TIP 325 and not available for me under python 3.14 and Windows 11 64bit

    About tk fontchooser I've looked into it and it is pretty different from other dialogs and makes it difficult to implement.
    However, I think sticking to the principle of been a thin wrapper and how tkinter.colorchooser.askcolor is implemented I would say the following should be sufficient.

    '''
    location: Lib/tkinter/fontchooser.py
    '''
    import tkinter as tk
    
    __all__ = ["Chooser", "askfont"]
    
    class Chooser:
    
        command = 'tk::fontchooser'
    
        def __init__(self, master=None, **options):
            if master is None:
                master = options.get('parent')
                if master is None:
                    master = tk._get_temp_root()
            self.master = master
            defaults = {
                'parent': str(master),
                'title':'',
                'font':'',
                'command': '',
                }
            defaults |= options
            self.configure(**defaults)
    
        @property
        def visible(self):
            return self.master.tk.call(self.command, 'configure', '-visible')
    
        def show(self) -> None:
            # This may or may not wait for the window
            # https://www.tcl-lang.org/man/tcl/TkCmd/fontchooser.html#M17
            self.master.tk.call(self.command, 'show')
    
        def hide(self) -> None:
            self.master.tk.call(self.command, 'hide')
    
        def configure(self, **options) -> None:
            self.master.tk.call(
                self.command, 'configure', *self.master._options(options))
    
    def askfont():
        var = tk.StringVar()
        w = var._root
        
        def set_when_finished(current):
            if not chooser.visible:
                var.set('after_idle')
                w.after_idle(lambda v=current:var.set(v))
    
        def ensure_break(event):
            if var.get() == '':
                return var.set('started')
            elif var.get() == 'started':
                w.after_idle(lambda: var.set('end'))
        
        chooser = Chooser(w, command=set_when_finished)
        # in case of "abort" or "x"
        w.bind('<<TkFontchooserVisibility>>', ensure_break)
        chooser.show()
        
        w.wait_variable(var)
        if var.get() != 'end':
            return w.tk.call('font', 'actual', var.get())
        return 'TkDefaultFont'
        
    
    if __name__ == '__main__':
        root = tk.Tk()
        tk.Label(root, text='Test', font=askfont()).pack()
        root.mainloop()
    
  8. ThingamabobsCoding commented on Mar 8, 2026

    @ThingamabobsCoding
    Author

    The above code should be checked if "ensure_break" works within askfont on all platforms. At least under windows there is no minimize button in the window decoration. I have no idea if that's the case for mac and X11 and how the current implementation would handle a case where you can minimize the window, but I believe it would break the code.

    EDIT:
    Since the fontchooser is implemented as transient window it should work cross-platform.

  9. serhiy-storchaka commented on Mar 9, 2026

    @serhiy-storchaka
    Member

    #72880 is a separate issue for fontchooser.

  10. serhiy-storchaka commented on Jul 7, 2026

    @serhiy-storchaka
    Member

    The simplest design is to just add 18 new methods in Misc. But this would pollute the namespace with a niche API. More object-oriented approach (although not common in Tkinter) -- only add an accessor to the wrapper object which groups all accessible commands. These two approaches are represented in #153246 and #153247 correspondingly. Note that tests are skipped in CI, because they require 9.1.

    9.1b1 is planned on September 2026, and 9.1.0 final on September 2026. This feature has already been frozen, but I think it is better to wait for a while.

  11. added 2 commits that reference this issue on Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-modulesC modules in the Modules dirstdlibStandard Library Python modules in the Lib/ directorytopic-tkintertype-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions