I would like to simulate the following events:
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.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?
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()