简体   繁体   中英

How to use callbacks for changes for multiple tkinter checkbuttons

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()

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM