-1

I have multiple tk.Checkbuttons that should share the same callback when clicked (or changed). The variables (states) of the checkboxes should be available within the callback that is triggered on status change (and used for all checkbuttons). There are many articles like this, most of them dealing with parallel lists of state-variables. Because I want to avoid a separate variable list because the amount of checkboxex can change due to data change, I created my own Checkbutton class in two versions, but both versions have disadvantages, so this is the point I need some advice.

Both classes contain an instance of the state-variable to hold the state (avoiding management of a separate var-list for states) and trigger an action when clicked. So my problems are related to the callback.

The first class is shown in this minimized running example:

#!/usr/bin/env python3
from tkinter import *
import tkinter as tk
#-------------------------------------------------------------------------------
# myCheckButton: CheckButton containing state, that can be identified
#                in action(command) procedure
# Parameters:
# userdata: User defined, for example index of a database table row related to the CB
# action  : command to execute when clicked. Replaces command variable because
#           command does not provide the event information when called. Event
#           information is needed to get the checkbox data within action.
class myCheckButton(tk.Checkbutton):
  def __init__(self, parent, userdata, action, *args, **kwargs):
    # state holds the state of the CB (False = unchecked, True = checked)
    self.state = BooleanVar(value=False)
    # add the state variable to tk.Ckeckbutton args
    kwargs['variable'] = self.state
    # init tk.Checkbutton
    super().__init__(parent, *args, **kwargs)
    # bind action to myCheckButton using <Button-1> (left mouse button)
    self.bind('<Button-1>', action)
    # store userdata for usage in the action procedure
    self.userdata = userdata
#-------------------------------------------------------------------------------
def onCbClicked(event):
  # get the calling widget containing all data we need to know
  sender = event.widget
  # get the status of CB. "not" because action runs before status change
  status = not sender.state.get()
  # do something by using text, status and/or user defined variable
  if status == True:
    print("CB("  + sender["text"] + "): Data " + str(sender.userdata) + " is checked")
  else:
    print("CB("  + sender["text"] + "): Data " + str(sender.userdata) + " is unchecked")
#-------------------------------------------------------------------------------
# Main window defs
root = tk.Tk()
root.title("myCheckButton")
root.geometry('300x60')
# 1st instance of myCheckButton
mycb1 = myCheckButton(root, text="Test A", userdata=1, action=onCbClicked)
mycb1.grid(row=0, column=0)
# 2nd instance of myCheckButton
mycb2 = myCheckButton(root, text="Test B", userdata=2, action=onCbClicked)
mycb2.grid(row=1, column=0)
# just for example: Set state of mycb2 to true
mycb2.state.set(True)
root.mainloop()

The disadvantage here is that the callback is called only when clicked, but not when I call .state.set(bool) to change the status (see second last line). Is there a way (besides my second solution using trace) to solve that? The advantage is that I can identify the calling instance clearly within the callback using the event passed to it.

The second solution is similar, with the advantage that the callback is also called when the state variable changes without clicking the widget. The disadvantage is that I have to pass the instance name of the Checkbutton to identify it within the callback by naming the traced variable same as the instance. In addition, I'm not sure if this way to identify the calling instance is save.

Minimized example of my second solution:

#!/usr/bin/env python3
from tkinter import *
import tkinter as tk
#-------------------------------------------------------------------------------
# myCheckButton: CheckButton containing state, that can be identified
#                in action(command) procedure
# Parameters:
# instName: Name of the CB instance to use in onCbChange
# userdata: User defined, for example index of a database table row related to the CB
# action  : command to execute when status changes.
class myCheckButton(tk.Checkbutton):
  def __init__(self, parent, instName, userdata, action, *args, **kwargs):
    # state holds the state of the CB (False = unchecked, True = checked)
    # the name of the internal variable is set to the instance name of this class.
    self.state = BooleanVar(value=False, name=instName)
    # Trace the state variable to call action procedure when it changes.
    self.state.trace("w", action)
    # add the state variable to tk.Ckeckbutton args
    kwargs['variable'] = self.state
    # init tk.Checkbutton
    super().__init__(parent, *args, **kwargs)
    # store userdata for usage in the action procedure
    self.userdata = userdata
#-------------------------------------------------------------------------------
def onCbChange(*args):
  # get the calling widget containing all data we need to know from *args.
  # This requires the name of the traced variable is the same as name of the widget instance.
  sender = eval(args[0])
  # get the status of CB.
  status = sender.state.get()
  # do something by using text, status and/or user defined variable
  if status == True:
    print("CB("  + sender["text"] + "): Data " + str(sender.userdata) + " is checked")
  else:
    print("CB("  + sender["text"] + "): Data " + str(sender.userdata) + " is unchecked")
#-------------------------------------------------------------------------------
# Main window defs
root = tk.Tk()
root.title("myCheckButton")
root.geometry('300x60')
# 1st instance of myCheckButton
mycb1 = myCheckButton(root, text="Test A", instName="mycb1", userdata=1, action=onCbChange)
mycb1.grid(row=0, column=0)
# 2nd instance of myCheckButton
mycb2 = myCheckButton(root, text="Test B", instName="mycb2", userdata=2, action=onCbChange)
mycb2.grid(row=1, column=0)
# just for example: Set state of mycb2 to true
mycb2.state.set(True)
root.mainloop()

Even if this is my preferred solution: Is it save to determine the calling instance by eval of the first argument that is passed to the trace-callback (assumed the correct name is passed to the constructor)? Is there a better way to identify the caller? For example by passing the event somehow to be able to identify the caller similar to the first solution?

Thanks in advance for any help.

Edit:

Following acw1668's hint, I changed the myCheckButton Class from the 1st solution to:

class myCheckButton(tk.Checkbutton):
  def __init__(self, parent, userdata, action, *args, **kwargs):
    # state holds the state of the CB (False = unchecked, True = checked)
    self.state = BooleanVar(value=False)
    # add the state variable to tk.Ckeckbutton args
    kwargs['variable'] = self.state
    # init tk.Checkbutton
    super().__init__(parent, *args, **kwargs)
    # bind action to myCheckButton using <Button-1> (left mouse button)
    self.bind('<Button-1>', action)
    # store userdata for usage in the action procedure
    self.userdata = userdata
    # store action
    self.action = action
    # add widget property
    self.widget = self
  def set(self, status):
    # only when status changes:
    if status != self.state.get():
      # callback before status change
      self.action(self)
      # change status
      self.state.set(status)

I'm not sure if the way to pass the "event" to the callback from the set procedure is the best, but it works when calling .set(bool).

Final solution

Here is the complete final solution for the custom checkbutton. Thanks to Matiiss and acw1668:

#!/usr/bin/env python3
from tkinter import *
import tkinter as tk
#-------------------------------------------------------------------------------
# myCheckButton: CheckButton containing state, that can be identified
#                in action(command) procedure
# Parameters:
# userdata: User defined, for example index of a database table row related to the CB
class myCheckButton(tk.Checkbutton):
  def __init__(self, parent, userdata, *args, **kwargs):
    # state holds the state of the CB (False = unchecked, True = checked)
    self.state = BooleanVar(value=False)
    # add the state variable to tk.Ckeckbutton args
    kwargs['variable'] = self.state
    # init tk.Checkbutton
    super().__init__(parent, *args, **kwargs)
    # store userdata for usage in the action procedure
    self.userdata = userdata
  def set(self, status):
    # only when status changes:
    if status != self.state.get():
      if status == True: self.deselect()
      else:              self.select()
      self.invoke()
#-------------------------------------------------------------------------------
def onCbClicked(sender):
  # get the status of CB.
  status = sender.state.get()
  # do something by using text, status and/or user defined variable
  if status == True:
    print("CB("  + sender["text"] + "): Data " + str(sender.userdata) + " is checked")
  else:
    print("CB("  + sender["text"] + "): Data " + str(sender.userdata) + " is unchecked")
#-------------------------------------------------------------------------------
# Main window defs
root = tk.Tk()
root.title("myCheckButton")
root.geometry('300x60')
# 1st instance of myCheckButton
mycb1 = myCheckButton(root, text="Test A", userdata=1, command=lambda: onCbClicked(mycb1))
mycb1.grid(row=0, column=0)
# 2nd instance of myCheckButton
mycb2 = myCheckButton(root, text="Test B", userdata=2, command=lambda: onCbClicked(mycb2))
mycb2.grid(row=1, column=0)
# just for example: Set state of mycb2 to test status change callback
mycb2.set(True)
root.mainloop()
Armin
  • 117
  • 2
  • 10
  • So as far as I understand You want to automatically assign a callback to a Checkbox? that will also be unique? – Matiiss Apr 10 '21 at 18:30
  • why don't You create them in a loop? (the second class) – Matiiss Apr 10 '21 at 18:36
  • No, I want to get a save way within the callback to find out what checkbox-instance was triggering the call, within the trace-callback. In the preferred example #2 I found a way, but I wonder if there is a better method to do this. – Armin Apr 10 '21 at 18:38
  • 1
    You can define a function `set()` in your first solution and update `self.state` and call the callback as well. Then use this function instead of calling `.state.set(...)`. – acw1668 Apr 10 '21 at 23:55
  • @acw1668, thanks for this hint. This was the missing hint for me. I updated (edited) my initial request. – Armin Apr 11 '21 at 12:36

1 Answers1

1

I am still a bit confused, but is this what You want:

from tkinter import Tk, Checkbutton


root = Tk()

c = Checkbutton(root, text='Apple')
c.pack()
c.bind('<Button-1>', lambda e: print(e.widget))

root.mainloop()

Use functions like this to simulate user interaction with checkboxes (check is for selecting and uncheck is for deselecting)

def check():
    c.deselect()
    c.invoke()
    

def uncheck():
    c.select()
    c.invoke()

You just have to find a way to call those functions

The complete code example:

from tkinter import Tk, Checkbutton, Button, IntVar


def toggle(widget):
    state = var.get()
    if state == 0:
        print('off')
    if state == 1:
        print('on')
    print(widget)


def check():
    c.deselect()
    c.invoke()


def uncheck():
    c.select()
    c.invoke()


root = Tk()

var = IntVar()

c = Checkbutton(root, text='Apple', variable=var, command=lambda: toggle(c))
c.pack()

Button(root, text='On', command=check).pack()
Button(root, text='Off', command=uncheck).pack()

root.mainloop()

notice that using buttons triggers checkbox command while just using .select/.deselect wouldn't do that

Matiiss
  • 5,970
  • 2
  • 12
  • 29
  • the bind function is already contained in my first code. It works when I click the checkbutton, but not when the state changes programmatically. – Armin Apr 11 '21 at 11:52
  • 1
    @Armin I edited my code. You can use `.invoke()` to simulate user interaction and toggle the checkbox so before You have to set it to the opposite because invoke will set it to the opposite of what its state is. since it simulates user interaction it will also trigger events (or at least it should) – Matiiss Apr 11 '21 at 14:37
  • @Armin made more edits since the design was a bit incomplete – Matiiss Apr 11 '21 at 14:43
  • Thanks, this is also a good approach. The only questin is: If there are more checkbuttons using the same command=toggle, how do I know which checkbutton is toggled in the callback? Using .bind(...) the callback is not called when doing .invoke(). Using command=... does not provide the event parameter that contains the widget that caused the event. – Armin Apr 11 '21 at 15:29
  • @Armin I edited the code once again (the second (example)) to include such functionality – Matiiss Apr 11 '21 at 15:44
  • Thanks a lot, this is the solution that covers everything I need. – Armin Apr 11 '21 at 16:02