Tracing a list with Tkinter - python

Tkinter has these variable classes: BooleanVar, DoubleVar, IntVar, StringVar. All of them have a trace() method that allows you to attach a callback that is called when the variable changes.
Is it possible (or is there a workaround) to trace a list? Specifically, I'm looking for a way to monitor a list so that I can change the elements of a Treeview.

The code below includes a test function (forceChange) that can be deleted, but demonstrates the rest of the code traces a python list variable. Since you're already using the tk event loop, I used that, but I've also tested it using the sched and time modules to schedule events when there is no GUI.
from tkinter import * import sys
class ListManager:
def __init__(self, root, listvar):
self.root = root
self.listvar = listvar
self.previous_value = listvar.copy()
# Create an event to watch for changes to the list.
self.watch_event = root.after(20, self.watchList)
# Create an event to change the list. This is for test purposes only.
self.change_event = root.after(200, self.forceChange)
def watchList(self):
''' Compare the previous list to the current list.
If they differ, print a message (or do something else).
'''
try:
if self.previous_value != self.listvar:
print("Changed! Was:", self.previous_value, " Is:", self.listvar)
self.previous_value = self.listvar.copy()
# Reschedule this function to continue to check.
self.root.after(20, self.watchList)
except Exception:
print("Variable has been destroyed")
self.root.after_cancel(self.change_event)
def forceChange(self):
try:
next = self.listvar[-1:][0]
except:
# Variable was destroyed.
return
if next == 11:
sys.exit()
next += 1
self.listvar.append(next)
self.root.after(500, self.forceChange)
if __name__ == '__main__':
root = Tk()
# This is the list we'll watch.
mylist = [1, 2, 3]
# Create a list manager
vlist = ListManager(root, mylist)
root.mainloop()

Related

Python Tkinter Label not responding

I am trying to make a loading and a GIF would be a lot helpful if it was supported in python tkinter. But since it is not supported, so I put all the frame-by-frame pictures in the list that makes a loading when played continuously (using assign_pic function) and then I created a label (called lab_loading) of which I change the picture after 200ms by calling the start_anim function. I am calling the assign_pic function in a loop which I think causes this error. See my source code below 👇 and the video I provided to understand this problem clearly.
Video: https://drive.google.com/file/d/1WHwZqvd8vXz-ehXbQ_fRtrKPEyFLKrVe/view?usp=sharing
Source code:
from time import sleep
from tkinter import Tk, Label
from PIL import ImageTk, Image
class Loading(Tk):
def __init__(self):
super().__init__()
self.title('Loading')
self.geometry('250x217')
self.address = getcwd()
self.imgs_list = []
self.loadingImgsList(self.address)
# This method Puts all the images in the list
def loadingImgsList(self, curAddress):
address = f'{curAddress}\\loading'
self.imgs_list = [(ImageTk.PhotoImage(Image.open(
f"{address}\\{i}.png"))) for i in range(1, 4)]
# This mehtod assigns the picture from the list via index (ind) number from the imgs_list list and
# updates the GUI when called.
def assign_pic(self, ind):
lab_loading.config(image=self.imgs_list[ind])
self.update_idletasks()
sleep(0.2)
def start_anim(self):
ind = 0
b = 0
while (b < 300):
if ind == 2:
ind = 0
else:
ind += 1
self.after(200, self.assign_pic, ind)
b += 1
if __name__ == "__main__":
root = Loading()
lab_loading = Label(root, image='')
lab_loading.pack()
root.start_anim()
root.mainloop()
I Tried to make start_anime function recursive but it was still the same. I don't know why this is happening. I also made the loop finite but it was still not working. So a solution to this problem or even a better suggestion would highly be appreciated.
you shouldn't be using sleep inside tk, as it blocks python from handling user actions.
the way you do animation in tk is by using the after method, to call a function that would update the canvas, this function will call after again, until the animation is complete.
# everything before this function should be here
self.ind = 0 #somewhere in __init__
def assign_pic(self):
if self.ind < len(imgs_list):
lab_loading.config(image=self.imgs_list[self.ind])
self.ind += 1
root.after(500,self.assign_pic) # time in milliseconds
else:
print("done") # do what you want after animation is done
if __name__ == "__main__":
root = Loading()
lab_loading = Label(root, image='')
lab_loading.pack()
root.after(100,root.assign_pic)
root.mainloop()
the after function schedules the given function after a certain delay, during which the GUI is free to respond to any action.
Edit: after method takes argument in milliseconds not in seconds, i had the input in it in seconds instead of milliseconds, it's now fixed.

How can I return the index of a tkinter Listbox selection back to the function where the bind was called?

I'm trying to write a program that returns the index of a selected Listbox item to the function where the bind function was called.
The below program simply prints the index of the selected item inside the get_index function once the item is clicked; however, I want the index to be returned to the main function rather than being contained in the get_index function. Is what I'm trying to accomplish even possible?
import tkinter as tk
def get_index(event):
selection = event.widget.curselection()
if selection:
index = selection[0]
print(index) # Want to return here instead of print
else:
pass
def main():
root = tk.Tk()
root.geometry("300x150")
my_list = tk.Listbox(root)
my_list.pack(pady=15)
options = [
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
]
for option in options:
my_list.insert("end", option)
my_list.bind("<<ListboxSelect>>", get_index)
root.mainloop()
if __name__ == "__main__":
main()
How can I return the index of a tkinter Listbox selection back to the function where the bind was called?
The short answer is: you can't.
Bound functions are not called in the context of the function where the binding was made. Anything a bound function returns will be ignored by tkinter.
Since you are not using classes, your only option is to use a global variable.

Python main script not waiting for user response in Tkinter GUI

I am brand new to using the Tkinter GUI and stuck while trying to make an interactive program that 1) solicits feedback from the user in the GUI, 2) waits for the user to respond and hit enter and 3) then uses the input to inform the next steps in the main script.
However, I am unable to make step 2 execute properly. At the function call waitforinput(), I would expect the main script to wait before moving on to the next lines which are test printouts. Instead, it prints the main script test lines with '' for result and then places an entry box that works. Why is this program moving to the next line before the waitforinput function is completed? Thanks!
import tkinter as tk
from tkinter import *
import threading, time
# assignments for input thread
WAIT_DELAY = 250 #milliseconds
lock = threading.Lock() # Lock for shared resources.
finished = False
result = ''
# Set up the graphical interface
root = tk.Tk(className='My Flashcards')
def main():
# request and wait for input from user
waitforinput()
Test = tk.Label(root, text = "result = " + result)
Test.pack()
Test.config(font = ('verdana', 24), bg ='#BE9CCA')
# sets background thread for getinput from user
def waitforinput():
global finished
with lock:
finished = False
t = threading.Thread(target=getinput)
t.daemon = True
root.after(WAIT_DELAY, check_status) # start waiting
t.start()
# checks to see if user has inputted
def check_status():
with lock:
if not finished:
root.after(WAIT_DELAY, check_status) # keep waiting
# solicits and returns a string from the user
def getinput():
# declaring string variable for storing name and password
answer_var = tk.StringVar()
# define a function that will get the answer and return it
def user_response(event = None):
answer = answer_entry.get()
global result
result = answer
global finished
finished = True # to break out of loop
# creating an entry for inputting answer using widget Entry
answer_entry = tk.Entry(root, width = 1, borderwidth = 5, bg ='#BE9CCA', textvariable = answer_var) ## could be global with args
# making it so that enter calls function
answer_entry.bind('<Return>', user_response)
# placing the entry
answer_entry.pack()
answer_entry.focus()
main()
root.mainloop()
'''
You don't need to use threads to wait for a response. Tkinter has methods specifically for waiting: wait_window and wait_variable.
wait_window waits for a window to be destroyed. This is typically used when creating a modal dialog. wait_variable can be used to wait until one of the special tkinter variable objects has been modified.
If you want your code to wait until a user has pressed the enter key in an entry, you can set a binding on that key to set a variable, and then wait for that variable to be set. Note: you normally don't want to use wait_variable for a variable used as the value of textvariable since the wait will stop as soon as a single character has been entered.
Your getinput function could look something like this, which can be called directly without the use of threads:
def getinput():
answer_var = tk.StringVar()
def user_response(event):
answer_var.set(answer_entry.get())
return
answer_entry = tk.Entry(root, width = 1, borderwidth = 5, bg ='#BE9CCA')
answer_entry.bind('<Return>', user_response)
answer_entry.pack()
answer_entry.focus()
answer_entry.wait_variable(answer_var)
return answer_var.get()
Once the statement answer_entry.wait_variable(answer_var) executes, tkinter will enter an event loop and won't return until answer_var has been set. You can then call get on the variable and return the value.
Here is how you can modify your main function to call this function:
def main():
result = getinput()
Test = tk.Label(root, text = "result = " + result)
Test.pack()
Test.config(font = ('verdana', 24), bg ='#BE9CCA')

Python self.attribute cannot be seen when calling class method

I have a problem related to a TKinter GUI I am creating, but the problem is not necessarily specific to this library.
Background
I am currently in the advanced stage of a python self-learning course. The learning module I am on is covering TKinter for creating interactive GUI's. I am making a game whereby randomly generated numbered buttons must be clicked in succession in the quickest time possible.
Brief: https://edube.org/learn/pcpp1-4-gui-programming/lab-the-clicker
Problem
Under my class, game_grid, I have created an instance variable; 'self.holder', a 25 entry dictionary of {Key : TkinterButtonObject} form
When calling this instance variable for use in a class method, I get the following error:
AttributeError: 'game_grid' object has no attribute 'holder'
I have a print statement under class init which proves this attribute has been successfully created. I have made sure my spacing and tabs are all OK, and have tried every location for this variable, including using as a class variable, and a global variable to no avail - as it is an semi-complex object. I don't see what difference it should make, but any ideas would be much appreciated. I am also aware this could be done without classes, but I am trying to adopt DRY principles and orthogonality in all of my programs.
Thanks in advance.
Full Code:
import tkinter as tk
from tkinter import*
import random
from tkinter import messagebox
import time
win = tk.Tk()
class game_grid:
def __init__(self, win):
self.last_number = 0
self.number_buttons = {}
self.row_count = 0
self.column_count = 0
#Generate a list of 25 random numbers
self.number_list = random.sample(range(0, 999), 25)
#Puts the numbers in a dictionary (number : buttonobject)
self.holder = {i: tk.Button(win, text = str(i), command = game_grid.select_button(self, i)) for i in self.number_list}
#pack each object into window by iterating rows and columns
for key in self.holder:
self.holder[key].grid(column = self.column_count, row = self.row_count)
if self.column_count < 4:
self.column_count += 1
elif self.column_count == 4:
self.column_count = 0
self.row_count += 1
print(self.holder)
def select_button(self, number):
if number > self.last_number:
self.holder[number].config(state=tk.DISABLED)
self.last_number = number
else:
pass
class stopclock():
def __init__(self):
#Stopclock variable initialisation
self.time_begin = 0
self.time_end = 0
self.time_elapsed= 0
def start(self):
if self.time_begin == 0:
self.time_begin = time.time()
return("Timer started\nStart time: ", self.time_begin)
else:
return("Timer already active")
def stop(self):
self.time_end = time.time()
self.time_elapsed = time_end - time_begin
return("Timer finished\nEnd time: ", time_begin,"\nTime Elapsed: ", time_elapsed)
play1 = game_grid(win)
win.mainloop()
Perhaps you meant:
command = self.select_button(self, i)
Update:
Though from research:How to pass arguments to a Button command in Tkinter?
It should be:
command = lambda i=i: self.select_button(i)
You call select_button from inside the dict comprehension of holder. select_button then tries to use holder, but it is not yet defined. You don't want to actually call select_button, but assign a function to the button, like that:
self.holder = {i: tk.Button(window, text=str(i), command=lambda i=i: self.select_button(i)) for i in self.number_list}

How to delete (or de-grid) Tkinter GUI objects stored in a list (Python)

I'm trying to make a program that creates some random amount of GUI objects in Tkinter and stores them in a list. Here (in the code below) I have a for loop that creates a random amount of radio buttons. Each time a radio button object is created it is stored in the list 'GUIobjects'. I do this because otherwise I have no way of accessing the GUI objects later on. What I need to know, now, is how to delete or de-grid the objects.
I have tried self.radioButton.grid_forget(), but this only de-grids the last object created. I'm not sure of there's a way to access each object in the list and use .grid_forget(). If there is, that would be an option.
For now all I need to know is how to delete or de-grid the GUI objects after I create all of them.
from tkinter import *
import random
class App(Tk):
def __init__(self):
Tk.__init__(self)
self.addButton()
def addButton(self)
GUIobjects = []
randInt = random.randint(3, 10)
self.radVar = IntVar()
for x in range(2, randInt):
self.radioButton = Radiobutton(self, text = "Button", variable = self.RadVar, value = x)
self.radioButton.grid(row = x)
print(GUIobjects)
# This is to show that one more object has been created than appears on the screen
self.radioButton.grid_forget()
# This de-grid's the last object created, but I need to de-grid all of them
def main():
a = App()
a.mainloop()
if __name__ == "__main__":
main()
As of right now, I try to de-grid the objects right after creating all of them. Once I find out how to de-grid each object, I will somehow need to make a button that de-grid's them (as compared to de-griding them right after they have been created). This button should be able to be put in a method other than 'addButtons' but still in the 'App' class.
Thanks!
You need to store references to each object. Create an empty list and append the reference to the list inside your loop.
self.radioButtons = []
for x in range(2, randInt):
self.radioButtons.append(Radiobutton(self, text = "Button", variable = self.RadVar, value = x))
self.radioButtons[-1].grid(row = x) # layout the most recent one
They won't be garbage collected unless you delete the reference as well.
for button in self.radioButtons:
button.grid_forget()
del self.radioButtons

Categories

Resources