How to Make Tkinter Frames in a Loop and Update Object Values

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

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 script
for 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.

Tkinter window not updating till after the loop is done

You made a very good first attempt and you were almost there. I have made some changes in your code, most importantly introduced a root object (tk.Tk()) so I can do root.update() in order to redraw to sort_canvas in a new method blip_canvas. To avoid some 'flickering' rather to destroy the canvas each time it is better to delete the 'bar' elements only.
Further I took the liberty to change some of the variable names to make it a bit more Pythonic (should make use of underscores rather than capitals) and added the if _name__ == '__main__' statement.

Have a look at the code below.

import tkinter as tk
from tkinter import ttk
import random

class SortingVisualizer:

def __init__(self):
self.root = tk.Tk()
self.root.wm_title("Sorting Visualizer")
self.root.wm_minsize(width=600, height=500)
self.root.wm_resizable(width=False, height=False)

self.top_frame = tk.Frame(self.root)
self.top_frame.grid(row=0, sticky='w')

self.sort_options = ['Select Algorithm', 'Bubble sort', 'Quicksort', 'Merge sort']
self.option_var = tk.StringVar()
self.option_drop = ttk.OptionMenu(
self.top_frame, self.option_var, *self.sort_options)
self.option_drop.config(width=15)
self.option_drop.grid(row=0, column=1, sticky='ew')

self.sort_button = ttk.Button(
self.top_frame, text="Sort", command=self.bubble_sort)
self.sort_button.grid(row=0, column=2, sticky='w')

self.gen_button = ttk.Button(
self.top_frame, text="Generate New Array", command=self.new_array)
self.gen_button.grid(row=0, column=0)

self.sort_canvas = tk.Canvas(self.root)
self.bars = []

def new_array(self):
self.generate_array()
self.blip_canvas()

def generate_array(self):
self.array = []
self.num_operations = 0
i = 0
while i < 15:
height = random.randint(15, 200)
self.array.append(height)
i = i + 1

def draw_canvas(self):
label = "Number of Operations: " + str(self.num_operations)
self.num_label = tk.Label(self.top_frame, text=label)
self.num_label.grid(row=1)

self.sort_canvas = tk.Canvas(self.root, width=600, height=450)
self.sort_canvas.grid(row=1)
self.sort_canvas.create_line(15, 15, 585, 15)

bar_width = 20
bar_gap = bar_width + 10
start_x = 30
start_y = 15
self.bars = []
for bar_height in self.array:
x1 = start_x + bar_width
y1 = start_y + bar_height
self.bars.append(self.sort_canvas.create_rectangle(
start_x, start_y, x1, y1*2, fill='green'))
start_x = start_x + bar_gap

def blip_canvas(self):
self.sort_canvas.delete(self.bars)
self.draw_canvas()
self.root.update()
self.root.after(200)

def bubble_sort(self):
n = len(self.array)
for i in range(n):
for j in range(n-i-1):
if self.array[j] > self.array[j+1]:
self.array[j], self.array[j+1] = self.array[j+1], self.array[j]
self.num_operations += 1
self.blip_canvas()

def start(self):
tk.mainloop()


if __name__ == '__main__':
app = SortingVisualizer()
app.start()

Note in bubble_sort you do not need the temp variable to swap the values of array[j] and array[j+1]

Rather than to use time.sleep(0.2) to set a delay I have used:

self.root.update()
self.root.after(200)

as suggested in Update button after delay

You can also stick to your original code and just make a few changes.

1) Change the sortButton

self.sortButton = ttk.Button(self.topFrame, text = "Sort", command=self.bubbleSort)

2) Indent the bubbleSort method to be aligned with SortingVisualizer

3) Change method redrawCanvas to:

    def redrawCanvas(self):
self.sortCanvas.destroy()
self.drawCanvas()
self.update()
self.after(300)

and

4) in bubbleSort make the call to redrawCanvas:

        for j in range(0, n-i-1):
if self.array[j]>self.array[j+1]:
temp = self.array[j]
self.array[j] = self.array[j+1]
self.array[j+1] = temp
self.numOperations += 1
self.redrawCanvas()

et voila, it will work!

Tkinter adding entry values to list through a loop

This code snippet answers your question.

Use bind to link your Entry widget to Return key press and function answer.

Then use method get on the widget connected to Return key press to extract the contents of the Entry widget.

import tkinter as tk

master = tk.Tk()

def answer( ev ):
print(ev.widget.get())

for a in range( 4 ):
e = tk.Entry(master, font = "Helvetica 14 normal")
e.pack(fill = "both", expand = True)
e.bind("<Return>", answer)

e.focus()

master.mainloop()

Python Tkinter, Update every second

Yes, you will need to create a method to update the values, and add that method to the tkinter mainloop with after. I'd recommend naming it something else, because update is already a tkinter method.

As a totally untested guess:

class Screen(tk.Frame):
def __init__(self, ADC, parent, controller):
tk.Frame.__init__(self, parent)
self.ADC = ADC
lbl = ttk.Label(self, text = "Test Display", background = "grey")
lbl.grid(column = 7, row = 8)
self.lblTestDisplay = ttk.Label(self, foreground = "lime", background = "black")
self.lblTestDisplay.grid(column = 7, row = 9, sticky = "ew")

self.adc_update() # start the adc loop

def adc_update(self):
displayText = self.ADC.ReadValues() #this method returns a list of values
for i in range(len(displayText)):
displayText[i] = round(displayText[i], 2)
self.lblTestDisplay.config(text = str(displayText)) # update the display
self.after(1000, self.adc_update) # ask the mainloop to call this method again in 1,000 milliseconds

Note I also split your label creation into 2 lines, one to initialize and one to layout. The way you had it is very bad; it leads to the variable assigned to None which leads to bugs.

Add images inside frame with loop in tkinter

The script is from @flavio-moraes logically works, however with a mix of the answers I got, I made the final script which fullfills my needs.

pc_images=[]
for i in range(int(len(obj))):
...
pc_images.append(PhotoImage(file=img_src))

live_pc_position = 10
for pc in pc_images:
#create the image
computer_icon = Label(black_background, image=pc, bg="black")
computer_icon.img = pc_images
computer_icon.place(x=live_pc_position, y=10)

# update values so the next image will load to the right
live_pc_position = live_pc_position + 40

How to make Python Tkinter GUI checks if an other thread has finished to make GUI changes?

You can use .wait_variable() function to replace the while loop because .wait_variable() does not block tkinter mainloop():

  • create a tkinter variable: var1 = StringVar()
  • pass this variable to databaseGetDF class
  • call .wait_variable(var1) instead of the while loop
  • update this variable when the thread task completes

Example:

class databaseGetDF(Thread):
def __init__(self, filepath, notify_var):
super().__init__()
self.filepath = filepath
self.notify_var = notify_var
self.df = None

def run(self):
self.df = db.addCollectionData(self.filepath, True)
# update self.notify_var
self.notify_var.set('')

...
# create a tkinter variable
var1 = StringVar()
# pass the variable to databaseGetDF class
download_thread = databaseGetDF(filename, var1)
download_thread.start()
# wait for the thread completes
root.wait_variable(var1) # assume root is the root window
# thread completed, so you can get the dataframe
df = download_thread.df
...


Related Topics



Leave a reply



Submit