【问题标题】:How to make tkinter frames in a loop and update object values?如何在循环中制作 tkinter 帧并更新对象值?
【发布时间】:2019-10-10 22:25:43
【问题描述】:

我有一个名为Bones 的类,我的skeleton 字典中有5 个Bones。但是在我的实际实现中,有 300 多个骨骼,这就是我今天在 stackoverflow 上问这个问题的原因。

每个Bone 都有:

  • ID: 用于识别骨骼的 int
  • w:w 位置(在-11 之间浮动)
  • x:x 位置(在-11 之间浮动)
  • y:y 位置(在-11 之间浮动)
  • z:z 位置(在-11 之间浮动)

Bone.py

INCREMENT = 0.01

class Bone:
    def __init__(self, boneId, w, x, y, z):
        self.id = boneId
        self.w = w
        self.x = x
        self.y = y
        self.z = z

    def shouldChangePos(self, num):
        if (num >= 1 or num <= -1):
            return False
        return True

    def incrW(self):
        if(self.shouldChangePos(self.w)):
            self.w = self.w + INCREMENT

    def decrW(self):
        if(self.shouldChangePos(self.w)):
            self.w = self.w - INCREMENT

    def incrX(self):
        if(self.shouldChangePos(self.x)):
            self.x = self.x + INCREMENT

    def decrX(self):
        if(self.shouldChangePos(self.x)):
            self.x = self.x - INCREMENT

    def incrY(self):
        if(self.shouldChangePos(self.y)):
            self.y = self.y + INCREMENT

    def decrY(self):
        if(self.shouldChangePos(self.y)):
            self.y = self.y - INCREMENT

    def incrZ(self):
        if(self.shouldChangePos(self.z)):
            self.z = self.z + INCREMENT

    def decrZ(self):
        if(self.shouldChangePos(self.z)):
            self.z = self.z - INCREMENT

问题说明

我正在尝试制作一个看起来像这样的tkinter GUI:

传说:

  • 绿色 - 代表Frame(只是我的注释来解释)
  • 红色 - 是对象的属性(只是我的注释来解释)
  • 黑色 - 是对象的方法(只是我的注释来解释)
  • 蓝色 - 是显示给我的文本和按钮

如您所见,它显示了IDwxyz。在它的下方,有一个+按钮和一个-按钮。每次单击这些按钮时,我想减少对象中的相应值并更新显示的tkinter 数字。我知道如何手动执行此操作,但根据我的要求,我有 300+ Bones。我无法手动制作这些框架。

如何在循环中创建这些框架并在单击 +- 按钮时更新 GUI 和对象上显示的值?强>


main.py

from tkinter import *
from tkinter import ttk
from Bone import *

skeleton = {
    1: Bone(-0.42, 0.1, 0.02, 0.002, 0.234),
    4: Bone(4, 0.042, 0.32, 0.23, -0.32),
    11: Bone(11, 1, -0.23, -0.42, 0.42),
    95: Bone(95, -0.93, 0.32, 0.346, 0.31),
}


root = Tk()
root.geometry('400x600')

boneID = Label(root, text="ID: 1")
boneID.grid(row=1, column=1, sticky=W, padx=(0, 15))

w = Label(root, text="-0.42")
w.grid(row=1, column=2, sticky=W)

x = Label(root, text="0.02")
x.grid(row=1, column=4, sticky=W)

y = Label(root, text="0.002")
y.grid(row=1, column=6, sticky=W)

z = Label(root, text="0.234")
z.grid(row=1, column=8, sticky=W)

wPlusBtn = Button(root, text="+")
wPlusBtn.grid(row=2, column=2)
wMinusBtn = Button(root, text="-")
wMinusBtn.grid(row=2, column=3, padx=(0, 15))

xPlusBtn = Button(root, text="+")
xPlusBtn.grid(row=2, column=4)
xMinusBtn = Button(root, text="-")
xMinusBtn.grid(row=2, column=5, padx=(0, 15))

yPlusBtn = Button(root, text="+")
yPlusBtn.grid(row=2, column=6)
yMinusBtn = Button(root, text="-")
yMinusBtn.grid(row=2, column=7, padx=(0, 15))

zPlusBtn = Button(root, text="+")
zPlusBtn.grid(row=2, column=8)
zMinusBtn = Button(root, text="-")
zMinusBtn.grid(row=2, column=9, padx=(0, 15))

root.mainloop()

【问题讨论】:

    标签: python python-3.x dictionary tkinter


    【解决方案1】:

    TL;DR - 将一个大问题分解为几个小问题,然后分别解决每个问题。


    主窗口

    首先查看 UI 的整体设计。您有两个部分:一个包含骨骼的面板和一个包含随机文本的面板。所以我要做的第一件事就是将这些面板创建为框架:

    root = tk.Tk()
    bonePanel = tk.Frame(root, background="forestgreen", bd=2, relief="groove")
    textPanel = tk.Frame(root, background="forestgreen", bd=2, relief="groove")
    

    当然,您还需要使用packgrid 将它们布置在窗口上。我推荐pack,因为只有两个框架并且它们是并排的。

    显示骨骼

    对于骨骼面板,每个骨骼似乎都有一行。因此,我建议创建一个类来表示每一行。它可以从 Frame 继承,并负责该行内发生的所有事情。通过从Frame 继承,您可以将其视为在屏幕上布局的自定义小部件。

    目标是让您的 UI 代码看起来像这样:

    bones = (
        Bone(boneId=1,  w=-0.42, x=0.02,  y=0.002, z=0.234),
        Bone(boneId=4,  w=0.042, x=0.32,  y=0.23,  z=-0.32),
        Bone(boneId=11, w=1,     x=-0.23, y=-0.42, z=0.42),
        ...
    )
    
    bonePanel = tk.Frame(root)
    for bone in bones:
        bf = BoneFrame(bonePanel, bone)
        bf.pack(side="top", fill="x", expand=True)
    

    同样,您可以根据需要使用grid,但pack 似乎是自然的选择,因为这些行是从上到下堆叠的。

    显示单个骨骼

    现在,我们需要解决每个 BoneFrame 的作用。它似乎由五个部分组成:一个显示 id 的部分,然后是四个几乎相同的属性部分。由于这些部分之间的唯一区别是它们所代表的属性,因此将每个部分表示为类的实例是有意义的。同样,如果该类继承自 Frame,我们可以将其视为自定义小部件。

    这一次,我们应该传入骨骼,也许还有一个字符串告诉它要更新哪个 id。

    所以,它可能开始看起来像这样:

    class BoneFrame(tk.Frame):
        def __init__(self, master, bone):
            tk.Frame.__init__(self, master)
    
            self.bone = bone
    
            idlabel = tk.Label(self, text="ID: {}".format(bone.id))
            attr_w = BoneAttribute(self, self.bone, "w")
            attr_x = BoneAttribute(self, self.bone, "x")
            attr_y = BoneAttribute(self, self.bone, "y")
            attr_z = BoneAttribute(self, self.bone, "z")
    

    pack 是一个不错的选择,因为这些部分都是从左到右排列的,但如果您愿意,可以使用 grid。唯一真正的区别是使用grid 需要多几行代码来配置行和列的权重。

    属性按钮和标签的小部件

    最后,我们必须解决BoneAttribute 类。这是我们最终添加按钮的地方。

    这很简单,并且遵循相同的模式:创建小部件,然后布置它们。不过,还有更多。我们需要连接按钮来更新骨骼,并且我们还需要在骨骼发生变化时更新标签。

    我不会详细介绍所有细节。您需要做的就是创建一个标签、几个按钮和按钮调用的函数。另外,我们需要一个函数来在值更改时更新标签。

    让我们从更新标签的函数开始。由于我们知道属性的名称,我们可以做一个简单的查找来获取当前值并更改标签:

    class BoneAttribute(tk.Frame):
        ...
        def refresh(self):
            value = "{0:.4f}".format(getattr(self.bone, self.attr))
            self.value.configure(text=value)
    

    这样,我们可以随时更新标签。

    现在只需定义按钮的功能即可。有更好的方法可以做到这一点,但一种简单直接的方法是只使用一些 if 语句。下面是增量函数的样子:

    ...
    plus_button = tk.Button(self, text="+", command=self.do_incr)
    ...
    
    def do_incr(self):
        if self.attr == "w":
            self.bone.incrW()
        elif self.attr == "x":
            self.bone.incrX()
        elif self.attr == "y":
            self.bone.incrY()
        elif self.attr == "z":
            self.bone.incrZ()
    
        self.refresh()
    

    do_decr 函数是相同的,只是它调用了一次递减函数。

    就是这样。这里的关键是将你的大问题分解成小问题,然后一次解决每个小问题。无论您有 3 个骨骼还是 300 个骨骼,您唯一需要编写的额外代码就是您最初创建骨骼对象的位置。 UI 代码保持不变。

    【讨论】:

      【解决方案2】:

      这里有两个问题:在循环中创建帧,以及在按下 +/- 按钮时更新值。

      为了处理框架问题,我建议您创建一个BoneFrame 类,该类包含与一个Bone 实例相关的所有小部件(按钮和标签)。 在那里,您还可以将按钮绑定到 Bone 方法,以便对值进行操作。 类似的东西 - 我相信你会知道如何使用其他变量和你想要的网格坐标来完成这个

      class BoneFrame(tk.Frame):
          def __init__(self, parent, bone):
              super().__init__(parent)
      
              # Create your widgets
              self.x_label = tk.Label(self, text=bone.x)
              self.x_decr_button = tk.Button(self, text="-", action=bone.decr_x)
              self.x_incr_button = tk.Button(self, text="+", action=bone.incr_x)
              ...
      
              # Then grid all the widgets as you want
              self.x_label.grid()
              ...
      

      然后,您可以轻松地遍历 Bones 的字典,每次都将 BoneFrame 实例化,并将 packgrid 该实例实例化到父容器。 也许你会想在BoneFrame.__init__ 的参数中添加一个bone_id 并在循环中传递它。

      # In your main script
      for bone_id, bone in skeleton.items():
          frame = BoneFrame(root, bone)
          frame.pack()
      

      目前,标签中的值永远不会更新。 那是因为我们只设置了一次它们的文本,然后我们就永远不会更新它们。 与其将按钮直接绑定到Bone 的方法,我们可以在BoneFrame 中定义更复杂的方法来实现更多逻辑,包括更新值,以及刷新小部件。 这是一种方法:

      class BoneFrame(tk.Frame):
          def __init__(self, parent, bone):
              super().__init__(parent)
      
              # Store the bone to update it later on
              self.bone = bone
      
              # Instantiate a StringVar in order to be able to update the label's text
              self.x_var = tk.StringVar()
              self.x_var.set(self.bone.x)
      
              self.x_label = tk.Label(self, textvariable=self.x_var)
              self.x_incr_button = tk.Button(self, text="+", action=self.incr_x)
      
              ...
      
          def incr_x(self):
              self.bone.incr_x()
              self.x_var.set(self.bone.x)
      

      所以我们需要一个StringVar 来更新标签的内容。 总而言之,我们没有将按钮绑定到bone.incr_x,而是将其绑定到self.incr_x,这允许我们在按下按钮时做任何我们想做的事情,即1.更改Bone实例中的值, 2.更新标签显示的值。

      【讨论】:

      • FWIW,您不必为了更新标签而创建 StringVar。您可以在没有每个标签的额外对象开销的情况下做到这一点。
      • @Bryan Oakley 啊,我知道了!但是已经很久了,我用手机写了答案,所以我真的无法找到如何去做......谢谢!
      【解决方案3】:

      解决此类问题的常用方法是创建函数(或类方法)来执行代码的重复部分(即软件工程的DRY 原则)。

      具有讽刺意味的是,这样做本身可能有点乏味,因为我很快发现尝试将现有代码重构为这种方式 - 但下面的结果应该让您对如何完成它有一个很好的了解。

      除了减少您必须编写的代码量之外,它还简化了更改或添加增强功能,因为它们只需在一个地方完成。通常最棘手的事情是确定传递函数的参数,以便它们可以以通用方式完成需要完成的工作并避免硬编码值。

      from tkinter import *
      from tkinter import ttk
      from Bone import *
      
      skeleton = {
          1: Bone(1, -0.42, 0.02, 0.002, 0.234),
          4: Bone(4, 0.042, 0.32, 0.23, -0.32),
          11: Bone(11, 1, -0.23, -0.42, 0.42),
          95: Bone(95, -0.93, 0.32, 0.346, 0.31),
      }
      
      
      def make_widget_group(parent, col, bone, attr_name, variable, incr_cmd, decr_cmd):
          label = Label(parent, textvariable=variable)
          label.grid(row=1, column=col, sticky=W)
      
          def incr_callback():
              incr_cmd()
              value = round(getattr(bone, attr_name), 3)
              variable.set(value)
      
          plus_btn = Button(parent, text='+', command=incr_callback)
          plus_btn.grid(row=2, column=col)
      
          def decr_callback():
              decr_cmd()
              value = round(getattr(bone, attr_name), 3)
              variable.set(value)
      
          minus_btn = Button(parent, text='-', command=decr_callback)
          minus_btn.grid(row=2, column=col+1, padx=(0, 15))
      
      
      def make_frame(parent, bone):
          container = Frame(parent)
      
          boneID = Label(container, text='ID: {}'.format(bone.id))
          boneID.grid(row=1, column=1, sticky=W, padx=(0, 15))
      
          parent.varW = DoubleVar(value=bone.w)
          make_widget_group(container, 2, bone, 'w', parent.varW, bone.incrW, bone.decrW)
      
          parent.varX = DoubleVar(value=bone.x)
          make_widget_group(container, 4, bone, 'x', parent.varX, bone.incrX, bone.decrX)
      
          parent.varY = DoubleVar(value=bone.y)
          make_widget_group(container, 6, bone, 'y', parent.varY, bone.incrY, bone.decrY)
      
          parent.varZ = DoubleVar(value=bone.z)
          make_widget_group(container, 8, bone, 'z', parent.varZ, bone.incrZ, bone.decrZ)
      
          container.pack()
      
      
      if __name__ == '__main__':
      
          root = Tk()
          root.geometry('400x600')
      
          for bone in skeleton.values():
              make_frame(root, bone)
      
          root.mainloop()
      

      运行截图:

      顺便说一句,我注意到Bone.py 模块的代码中有很多重复,这可能会以类似的方式减少。

      【讨论】:

        猜你喜欢
        • 2020-04-08
        • 1970-01-01
        • 2017-02-13
        • 2013-12-15
        • 2022-09-23
        • 1970-01-01
        • 1970-01-01
        • 2021-02-25
        • 1970-01-01
        相关资源
        最近更新 更多