Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python Tkinter: Binding Keypress Event to Active Tab in ttk.Notebook

Summary

In a Python Tkinter application, when using ttk.Notebook, how do I bind a keypress event so that it only fires when the tab containing the frame generating the event is active (i.e., for a button hotkey, how do I only capture the event while the button is on the active tab)?

Detail

I am writing a Tkinter application (my first), which uses a ttk.Notebook object to manage multiple portions of the interface. I have multiple tabs, some of which have the "same" button on them, but which have different actions, depending on which tab is active (i.e., a "Save" button on one tab saves the items from that tab only, not from all tabs).

The intuitive way to do this is to bind the event to the frame, then the frame containing the "active" objects would catch the event, but this doesn't appear to work. However, if I bind the event to the root window, the same handler is called, regardless of the tab context.

I would think that this would be a common requirement, however I am unable to find information regarding how to do this.

I am using Python 3.4.3.

MCVE

Here is a minimum example, which demonstrates the behavior I have observed. It generates a main window with five tabs, each with an event binding for Alt-t, which should fire the event handler for the frame in that tab.

import tkinter as tk
from tkinter import ttk

class MyTab(ttk.Frame):
    """Frame to be added to each tab of the notebook.

    """

    def __init__(self, master, idx, *args, **kwargs):
        super().__init__(master, *args, **kwargs)
        self._button = ttk.Button(self, text='Tab {}'.format(idx),
                                  command=lambda *args, x=idx: self._handle_button(x, *args),
                                  underline=0)
        self.bind('<Alt-t>', lambda *args, x=idx: self._handle_button(x, *args))
        self._button.pack()
        self.pack()


    def _handle_button(self, x, *args):
        print('Button: Tab {}'.format(x))



class MainWdw(ttk.Frame):
    """Main application window.

    """

    def __init__(self, master, *args, **kwargs):
        super().__init__(master, *args, **kwargs)
        self._nb = ttk.Notebook(self)

        # Generate several tabs and add a MyTab object to each.
        self._tabs = []
        for x in range(1, 6):
            t = MyTab(self, x)
            self._tabs.append(t)
            self._nb.add(t, text='Tab {}'.format(x))

        self._nb.pack(expand=1, fill='both')
        master.title('Sample')
        self.pack(expand=1, fill='both', padx=2, pady=2)



def main():
    root = tk.Tk()
    app = MainWdw(root)
    root.mainloop()



if __name__ == '__main__':
    main()
like image 720
Deacon Avatar asked Sep 21 '17 13:09

Deacon


1 Answers

This is actually not a very common requirement. Most GUIs don't switch between pages like this.

The reason that binding to the root window seems to work but binding to a frame doesn't is at least partly due to the fact that the root window is special. When you add a binding to a widget, you aren't actually binding to a widget. Instead, you are associating a binding with a binding tag that happens to have the same name as the widget.

Each widget gets a set of binding tags: the tag that is the same name as the widget, but also a tag for the internal class of the widget (which is what nearly all default bindings are associated with), a tag for the toplevel (or root) window, and the special tag "all". Thus, when you bind to the root window, all widgets inherit this binding because they all have a bind tag for the root window.

Since you only want the binding to fire on a frame, or when any descendant of the frame has focus, you can add a bind tag to all of the children of the frame, and then add a binding to that tag.

For example:

class MyTab(ttk.Frame):
    def __init__(...):
        ...
        tag = str(self)
        self._add_bindtag(self, tag)
        self.bind_class(tag, '<Alt-t>', lambda *args, x=idx: self._handle_button(x, *args))

    def _add_bindtag(self, widget, tag):
        bindtags = widget.bindtags()
        if tag not in bindtags:
            widget.bindtags((tag,) + bindtags)
        for child in widget.winfo_children():
            self._add_bindtag(child, tag)

For more information on bind tags, see these answers:

  • https://stackoverflow.com/a/11542200/7432
  • https://stackoverflow.com/a/3513906/7432

The canonical tcl/tk documentation for binding tags is here: http://tcl.tk/man/tcl8.5/TkCmd/bindtags.htm

like image 85
Bryan Oakley Avatar answered Sep 22 '22 11:09

Bryan Oakley