简体   繁体   English

如何使用回调来更改多个 tkinter 复选按钮

[英]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).我有多个 tk.Checkbuttons 在单击(或更改)时应该共享相同的回调。 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.因为我想避免单独的变量列表,因为 checkboxex 的数量会因数据变化而变化,所以我在两个版本中创建了自己的 Checkbutton class,但两个版本都有缺点,所以这就是我需要一些建议的点。

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.这两个类都包含一个状态变量的实例来保存 state(避免管理单独的状态变量列表)并在单击时触发操作。 So my problems are related to the callback.所以我的问题与回调有关。

The first class is shown in this minimized running example:第一个 class 显示在这个最小化的运行示例中:

#!/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).这里的缺点是回调仅在单击时调用,而不是在我调用.state.set(bool) 更改状态时调用(见倒数第二行)。 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.第二种解决方案类似,其优点是当 state 变量发生变化而不单击小部件时也会调用回调。 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.缺点是我必须通过将跟踪变量命名为与实例相同的名称来传递 Checkbutton 的实例名称以在回调中识别它。 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)?即使这是我的首选解决方案:是否可以通过 eval 传递给跟踪回调的第一个参数来确定调用实例(假设正确的名称被传递给构造函数)? 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:按照 acw1668 的提示,我将 myCheckButton Class 从第一个解决方案更改为:

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).我不确定从 set 过程将“事件”传递给回调的方法是否是最好的,但它在调用.set(bool) 时有效。

Final solution最终解决方案

Here is the complete final solution for the custom checkbutton.这是自定义检查按钮的完整最终解决方案。 Thanks to Matiiss and acw1668:感谢 Matiiss 和 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请注意,仅使用.select / .deselect时使用按钮触发复选框命令不会这样做

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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