【问题标题】:How to use callbacks for changes for multiple tkinter checkbuttons如何使用回调来更改多个 tkinter 检查按钮
【发布时间】:2021-07-06 05:12:40
【问题描述】:

我有多个 tk.Checkbuttons 在单击(或更改)时应该共享相同的回调。复选框的变量(状态)应该在状态更改时触发的回调中可用(并用于所有检查按钮)。有很多这样的文章,其中大多数处理状态变量的并行列表。因为我想避免单独的变量列表,因为 checkboxex 的数量会因数据变化而变化,所以我在两个版本中创建了自己的 Checkbutton 类,但两个版本都有缺点,所以我需要一些建议。

这两个类都包含一个状态变量的实例来保存状态(避免管理状态的单独变量列表)并在单击时触发操作。所以我的问题与回调有关。

第一个类显示在这个最小化的运行示例中:

#!/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()

这里的缺点是回调仅在单击时调用,而不是在我调用 .state.set(bool) 更改状态时调用(参见倒数第二行)。有没有办法(除了我使用跟踪的第二个解决方案)来解决这个问题?优点是我可以使用传递给它的事件在回调中清楚地识别调用实例。

第二种解决方案类似,其优点是当状态变量发生变化时也会调用回调,而无需单击小部件。缺点是我必须通过将跟踪变量命名为与实例相同的名称来传递 Checkbutton 的实例名称以在回调中识别它。另外,我不确定这种识别调用实例的方式是否保存。

我的第二个解决方案的最小化示例:

#!/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()

即使这是我的首选解决方案: 通过传递给跟踪回调的第一个参数的 eval 确定调用实例是否保存(假设正确的名称被传递给构造函数)?有没有更好的方法来识别呼叫者?例如通过以某种方式传递事件以能够识别调用者,类似于第一个解决方案?

提前感谢您的帮助。

编辑:

按照 acw1668 的提示,我将 myCheckButton 类从第一个解决方案更改为:

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)

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

最终解决方案

这是自定义检查按钮的完整最终解决方案。感谢 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()

【问题讨论】:

  • 据我了解您想自动为复选框分配回调?这也将是独一无二的?
  • 为什么不循环创建它们? (二等)
  • 不,我想在回调中获得一种保存方式,以在跟踪回调中找出触发调用的复选框实例。在首选示例#2中,我找到了一种方法,但我想知道是否有更好的方法来做到这一点。
  • 您可以在您的第一个解决方案中定义一个函数set() 并更新self.state 并调用回调。然后使用这个函数而不是调用.state.set(...)
  • @acw1668,感谢您的提示。这是我缺少的提示。我更新(编辑)了我的初始请求。

标签: python python-3.x tkinter callback tkinter.checkbutton


【解决方案1】:

我还是有点困惑,但这就是你想要的:

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

使用这样的函数来模拟用户与复选框的交互(选中用于选择,取消选中用于取消选择)

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

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

你只需要找到一种方法来调用这些函数

完整的代码示例:

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

请注意,使用按钮会触发复选框命令,而仅使用 .select/.deselect 不会这样做

【讨论】:

  • 绑定函数已经包含在我的第一个代码中。它在我单击检查按钮时有效,但在状态以编程方式更改时无效。
  • @Armin 我编辑了我的代码。您可以使用.invoke() 来模拟用户交互并切换复选框,因此您必须将其设置为相反,因为调用会将其设置为与其状态相反的状态。因为它模拟用户交互,所以它也会触发事件(或者至少应该)
  • @Armin 进行了更多编辑,因为设计有点不完整
  • 谢谢,这也是一个好方法。唯一的问题是:如果有更多的检查按钮使用相同的命令=切换,我怎么知道回调中哪个检查按钮被切换?使用 .bind(...) 在执行 .invoke() 时不会调用回调。使用 command=... 不提供包含导致事件的小部件的事件参数。
  • @Armin 我再次编辑了代码(第二个(示例))以包含此类功能
猜你喜欢
  • 2021-02-08
  • 1970-01-01
  • 2016-11-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多