How to make tkinter frames in a loop and update object values? How to make tkinter frames in a loop and update object values? tkinter tkinter

How to make tkinter frames in a loop and update object values?


TL;DR - break your one big problem into several smaller problems, and then solve each problem separately.


The main window

Start by looking at the overall design of the UI. You have two sections: a panel holding bones, and a panel holding random text. So the first thing I would do is create these panels as frames:

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

Of course, you also need to use pack or grid to lay them out on the window. I recommend pack since there are only two frames and they are side-by-side.

Displaying bones

For the bone panel, you appear to have a single row for each bone. So, I recommend creating a class to represent each row. It can inherit from Frame, and be responsible for everything that goes on inside that row. By inheriting from Frame, you can treat it just like a custom widget with respect to laying it out on the screen.

The goal is for your UI code to look something like this:

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)

Again, you can use grid if you want, but pack seems like the natural choice since the rows are stacked top-to-bottom.

Displaying a single bone

Now, we need to tackle what each BoneFrame does. It appears to be made up of five sections: a section to display the id, and then four nearly identical sections for the attributes. Since the only difference between these sections is the attribute they represent, it makes sense to represent each section as an instance of a class. Again, if the class inherits from Frame we can treat it like it was a custom widget.

This time, we should pass in the bone, and perhaps a string telling it which id to update.

So, it might start out looking something like this:

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 is a good choice here since these sections are all lined up left-to-right, but you could use grid if you prefer. The only real difference is that using grid takes a couple more lines of code to configure row and column weights.

Widgets for the attribute buttons and labels

Finally, we have to tackle the BoneAttribute class. This is where we finally add the buttons.

It's pretty straight-forward and follows the same pattern: create the widgets, then lay them out. There's a bit more, though. We need to hook up the buttons to update the bone, and we also need to update the label whenever the bone changes.

I won't go into all of the details. All you need to do is to create a label, a couple of buttons, and functions for the buttons to call. Plus, we want a function to update the label when the value changes.

Let's start with tha function to update the label. Since we know the name of the attribute, we can do a simple lookup to get the current value and change the label:

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

With that, we can update the label whenever we want.

Now it's just a matter of defining what the buttons do. There are better ways to do it, but a simple, straight-forward way is to just have some if statements. Here's what the increment function might look like:

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

The do_decr function is identical, except that it calls once of the decrement functions.

And that's about it. The key point here is to break down your larger problem into smaller problems, and then tackle each smaller problem one at a time. Whether you have three bones or 300, the only extra code you have to write is where you initially create the bone objects. The UI code stays exactly the same.


There are two issues here: creating the frames in a loop, and updating the values upon a press on the +/- buttons.

To handle the frame issue, I suggest that you create a BoneFrame class that holds all the widgets (buttons and labels) related to one Bone instance.There, you can also bind the buttons to the Bone methods so as to act on the values.Something like that - I'm sure you'll know how to complete this with the other variables and the grid coordinates you want

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

Then you can easily iterate over your dict of Bones, instantiate BoneFrame every time, and pack or grid that instance to a parent container.Maybe you'll want to add a bone_id to the parameters of BoneFrame.__init__ and pass it in the loop.

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

For now, the values in the label never update.That's because we just set their text once, and then we never update them.Rather than binding the buttons directly to methods of Bone, we can define more complex methods in BoneFrame that achieve more logic, including updating the values, and also refreshing the widgets.Here's one way to do it:

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)

So we need a StringVar to update the content of the label.To sum it up, instead of binding the button to bone.incr_x, we bind it to self.incr_x, which allows us to do whatever we want upon a button press, that is 1. change the value in the Bone instance, and 2. update the value displayed by the label.


A usual way to address this kind of problem is to create functions (or class methods) to perform the repetitious bits of the code (i.e. the DRY principle of software engineering).

Ironically, doing this can itself be a little tedious as I quickly discovered trying to refactor your existing code to be that way — but below is the result which should give you a good idea of how it can be done.

Besides reducing the amount of code you have to write, it also simplifies making changes or adding enhancements because they only have be done in one spot. Often the trickiest thing is determining what arguments to pass the functions so they can do what it needs to be done in a generic way and avoiding hardcoded values.

from tkinter import *from tkinter import ttkfrom 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()

Screenshot of it running:

Screenshot of it running show multiple rows create in a for loop

BTW, I noticed a lot of repetition in the Bone.py module's code, which could probably be reduced in a similar manner.