Search code examples
pythontkinterttk

How to detect a change in state of a ttk.Button caused by an event and how to use the .state method?


I would like to simulate the following events:

  1. When the app frame resizes, it will toggle a change in state in a ttk.Button widget (i.e. self.bn1). If it is not in a disabled state, it will change to a disabled state, and vice versa.
  2. When the state of self.bn1 is toggled, it will similarly toggle a change in state in self.bn2 but in an opposite sense. That is, if self.bn1 is disabled, self.bn2 will be enabled, and vice versa. The is the key objective.

For objective 2, I want to use the following approach (I think this is the correct way but do correct me if I am wrong):

self.bn1.bind("<Activate>", self._set_bn2_disabled)
self.bn1.bind("<Deactivate>", self._set_bn2_enabled)

with the intention to learn how to use the Activate and Deactivate event types. Their documentation is given in here.

Below is my test code.

import tkinter as tk
from tkinter import ttk

class App(ttk.Frame):

    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent
        self.after_id = None
        self._create_widget()
        self._create_bindings()
        
    def _create_widget(self):
        self.bn1 = ttk.Button(self, text="B1")
        self.bn1.grid(row=0, column=0, padx=5, pady=5)
        self.bn2 = ttk.Button(self, text="B2")
        self.bn2.grid(row=1, column=0, padx=5, pady=5)
       
    def _create_bindings(self):
        self.bind("<Configure>", self._schedule_event)
        self.bind("<<FrameMoved>>", self._change_bn1_status)
        self.bn1.bind("<Activate>", self._set_bn2_disabled)
        self.bn1.bind("<Deactivate>", self._set_bn2_enabled)

    # Event handlers
    def _schedule_event(self, event):
        if self.after_id:
            self.after_cancel(self.after_id)
        self.after_id = self.after(500, self.event_generate, "<<FrameMoved>>")
        
    def _change_bn1_status(self, event):
        print(f"_change_bn1_status(self, event):")
        print(f"{event.widget=}")
        print(f"{self.bn1.state()=}")
        if self.bn1.state() == () or  self.bn1.state() == ('!disable'):
            self.bn1.state(('disable'))
        elif self.bn1.state() == ('disable'):
            self.bn1.state(['!disable'])

    def _set_bn2_disabled(self, event):
        self.bn2.state(['disabled'])
        print(f"{self.bn2.state()=}")

    def _set_bn2_enabled(self, event):
        self.bn2.state(['!disabled'])
        print(f"{self.bn2.state()=}")
               

if __name__ == '__main__':
    root = tk.Tk()
    app = App(root)
    app.pack(fill="both", expand=True)
    root.mainloop()

However, it is experiencing an error with the state command.

_change_bn1_status(self, event):
event.widget=<__main__.App object .!app>
self.bn1.state()=()
Exception in Tkinter callback
Traceback (most recent call last):
  File "/usr/lib/python3.10/tkinter/__init__.py", line 1921, in __call__
    return self.func(*args)
  File "/home/user/Coding/test.py", line 36, in _change_bn1_status
    self.bn1.state(('disable'))
  File "/usr/lib/python3.10/tkinter/ttk.py", line 588, in state
    return self.tk.splitlist(str(self.tk.call(self._w, "state", statespec)))
_tkinter.TclError: Invalid state name d

Documentation for handling state query of a ttk widget is given by the .state method described in here.

Update:

Following the comment by @Tranbi, I have revised the self._change_bn1_status method to:

def _change_bn1_status(self, event):
    print(f"\nBefore: {self.bn1.state()=}")
    if self.bn1.state() == ():
        self.bn1.state(['disabled'])
    elif 'disabled' in self.bn1.state():
        self.bn1.state(['!disabled'])
    print(f"After: {self.bn1.state()=}")

The state of self.bn1 is toggling correctly but not self.bn2. How do I do this?


Solution

  • Incorporating the comments of @acw1668 and @Tranbi, and previous answer by @ByranOakley on virtual events, and further research, I have revised my test code to what is shown below. The key point is that virtual events have to be created/used to detect a change in the state of a ttk.Button that is caused by an event.

    When declaring a virtual event, I discovered that there needs to be consistency in the syntax. If it is to be declared from self.bn1, the .event_generate methods should be declared from it and its corresponding .bind method be declared from it too. However, if the .event_generate method is declared from self, the .bind method should be declared from self instead of self.bn1.

    import tkinter as tk
    from tkinter import ttk
    
    class App(ttk.Frame):
    
        def __init__(self, parent):
            super().__init__(parent)
            self.parent = parent
            self.after_id = None
            self._create_widget()
            self._create_bindings()
            
        def _create_widget(self):
            self.bn1 = ttk.Button(self, text="B1")
            self.bn1.grid(row=0, column=0, padx=5, pady=5)
            self.bn2 = ttk.Button(self, text="B2")
            self.bn2.grid(row=1, column=0, padx=5, pady=5)
           
        def _create_bindings(self):
            self.bind("<Configure>", self._schedule_event)
            self.bind("<<FrameMoved>>", self._change_bn1_status)
            self.bn1.bind("<<Enabled>>", self._set_bn2_disabled)  # new
            self.bn1.bind("<<Disabled>>", self._set_bn2_enabled)  # new
            
    
        # Event handlers
        def _schedule_event(self, event):
            if self.after_id:
                self.after_cancel(self.after_id)
            self.after_id = self.after(500, self.event_generate, "<<FrameMoved>>")
            
        def _change_bn1_status(self, event):
            print(f"\nBefore: {self.bn1.state()=}")
            if self.bn1.state() == () :
                self.bn1.state(['disabled'])  # corrected typo
                self.bn1.after_idle(self.bn1.event_generate, "<<Disabled>>")  # new
            elif 'disabled' in self.bn1.state():  # corrected syntax
                self.bn1.state(['!disabled'])  # corrected typo
                self.bn1.after_idle(self.bn1.event_generate, "<<Enabled>>")  # new
            print(f"After: {self.bn1.state()=}")
    
        def _set_bn2_disabled(self, event):
            self.bn2.state(['disabled'])
            print(f"\n{self.bn2.state()=}")
    
        def _set_bn2_enabled(self, event):
            self.bn2.state(['!disabled'])
            print(f"\n{self.bn2.state()=}")
                   
    
    if __name__ == '__main__':
        root = tk.Tk()
        app = App(root)
        app.pack(fill="both", expand=True)
        root.mainloop()
    

    Toggling state