0

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?

Sun Bear
  • 7,594
  • 11
  • 56
  • 102
  • 1
    You have several typos: it should be `disabled` and not `disable`. Also `self.bn1.state(['disabled'])` instead of `self.bn1.state(('disable'))` (square brackets). Finally `self.bn1.state()` returns a tuple so you comparisons won't work. Try `'!disabled' in self.bn1.state()` instead – Tranbi Jul 12 '23 at 08:55
  • @Tranbi Thanks for the corrections. I fixed mistakes 1, 2 & 3. But I can't get `self.bn2` to toggle its state. How can I do that? Am i using a wrong event type? – Sun Bear Jul 12 '23 at 09:06
  • If those events `` and `` are not generated by `self.bn1` due to change of state, the state of `self.bn2` will not be changed because the corresponding callback is not executed. Better change state of `self.bn2` inside `_change_bn1_status()`. – acw1668 Jul 12 '23 at 09:35
  • @acw1668 I hear you. However, this test code's main objective is to understand how to detect state changes in a `ttk.Button` widget that is caused by the event triggered in another widget (which in the real case is at a different hierarchical level). I was hoping that binding the `` and `` event types to `self.bn1` was the right way to do this but I just can't figure out why it is not working. I have looked into the event types specified in `/usr/lib/python3.10/tkinter/__init__.py` but have not found an availble alternative. – Sun Bear Jul 12 '23 at 10:17
  • @acw1668 I just came across the [explanation](https://www.tcl.tk/man/tcl/TkCmd/bind.html#M8) of `` and `` in tcl documentation. Looks like it is to be used for tracking the active state of the top-level window and is not appropriate to track the `disabled` and `!disabled` state of a widget. – Sun Bear Jul 12 '23 at 10:39

1 Answers1

0

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

Sun Bear
  • 7,594
  • 11
  • 56
  • 102