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}
Related
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.
Honestly, I do not feel like this should be happening, but it is.
self.marketList.bind('<<ListboxSelect>>', self.market_selected)
self.jobsList.bind('<<ListboxSelect>>', self.job_selected)
There really isn't any more interaction between these two functions. When you click on an item in the marketList box, it's supposed to bring up the jobs in the jobsList box. Currently, it is applying the binding two both boxes. When I click on a job entry in the jobsBox, it clears the jobs and my troubleshooting is showing that it's calling market_selected. I'm not sure why this is happening, but it's really messing with what I'm trying to do with it.
How can I ensure that my binding is on only one widget, and won't be applied to multiple widgets?
edit:
I'm told that this isn't enough code to reproduce the error.
This is all the relevant code.
As I said previously, self.market_selected is called when I click on anything in the jobsList
edit #2
I uploaded the entire script.
import MarketWizard
import JobWizard
import SpanWalkerDocuments as swd
import tkinter as tk
from tkinter import *
from tkinter import ttk
import SpanWalkerDocuments as SpanWalker
class SpanWalker:
def __init__(self):
self.root = tk.Tk()
self.root.title('Luke Spanwalker')
self.root.resizable(True, True)
self.MainFrame = Frame(self.root, bg='red')
self.sidebarFrame = Frame(self.root, bg='blue')
self.tabControl = ttk.Notebook(self.sidebarFrame)
self.marketFrame = Frame(self.tabControl)
self.clientFrame = Frame(self.tabControl)
self.jobsFrame = Frame(self.tabControl)
self.polesFrame = Frame(self.MainFrame, height=100, width=50)
self.tabControl.add(self.marketFrame, text="Markets")
self.tabControl.add(self.jobsFrame, text="Jobs")
self.tabControl.add(self.clientFrame, text="Clients")
#self.tabControl.add(self.polesFrame, text="Poles")
self.tabControl.pack(expand=1, fill="both")
self.MainFrame.grid(row=0, column=1)
self.sidebarFrame.grid(row=0, column=0)
#Awesome, our tabbed control is ready.
#Now, we need to make the listBox widgets that will actually display our data.
self.marketList = Listbox(self.marketFrame)
self.jobsList = Listbox(self.jobsFrame)
self.polesList = Listbox(self.polesFrame)
#Binding functions! Yay for binding functions!
self.marketList.bind('<<ListboxSelect>>', self.market_selected)
self.jobsList.bind('<<ListboxSelect>>', self.job_selected)
self.curMarket = ""
self.markets = []
self.jobs = []
self.poles = []
def UpdateMarkets (self):
if len(self.markets) > 0:
self.markets.clear()
self.marketList.delete(0, END)
for mark in swd.Market.objects:
self.markets.append(mark)
for m in range(0, len(self.markets)):
self.marketList.insert(m, self.markets[m].title)
def OpenMarketWizard(self):
mw = MarketWizard.MarketWizard()
mw.RunMarketWizard(self.root)
def market_selected(self, event):
print("Market Selected")
selection = self.marketList.curselection()
selectedMarket = ",".join([self.marketList.get(i) for i in selection])
self.PopulateJobs(selectedMarket)
def PopulateJobs(self, market):
self.jobs.clear()
self.jobsList.delete(0, END)
self.GetJobs(market)
for j in range(0, len(self.jobs)):
self.jobsList.insert(j, self.jobs[j].jobName)
def GetJobs(self, market):
marketJobs = []
jobs = []
if market =="":
return
for j in swd.Job.objects:
jobs.append(j)
print("Market = {0}".format(market))
for i in jobs:
if i.market == market:
self.jobs.append(i)
def job_selected(self, event):
print("Job Selected")
selection = self.jobsList.curselection()
selectedJob = ",".join([self.jobsList.get(i) for i in selection])
print("The selected job is: {0}".format(selectedJob))
def PopulateMarkets(self):
self.marketList.destroy()
for m in range(0, len(self.markets)):
marketList.insert(m, self.markets[m].title)
def OpenJobWizard(self):
jw = JobWizard.JobWizard()
jw.RunJobWizard(self.root)
#We need to pack everything that belongs in our tabbed function.
def marketListDisplay(self, show):
if show == True:
self.marketList.pack(fill="both")
self.UpdateMarkets()
self.newButton=Button(self.marketFrame, text="Open Market Wizard", command=lambda:self.OpenMarketWizard())
self.newButton.pack()
self.refreshButton=Button(self.marketFrame, text="Refresh Markets", command=lambda:self.UpdateMarkets())
self.refreshButton.pack()
else:
self.marketList.forget_pack()
def jobsListDisplay(self, show):
if show==True:
self.jobsList.pack()
self.newButton = Button(self.jobsFrame, text="Create New Job / Open Job Wizard", command=lambda:self.OpenJobWizard())
self.newButton.pack()
else:
self.jobsList.forget_pack()
def polesListDisplay(self, show):
if show==True:
self.polesList.pack()
else:
self.polesList.forget_pack()
#Query, why not put them all in a function?
sp = SpanWalker()
sp.marketListDisplay(True)
sp.polesListDisplay(True)
sp.jobsListDisplay(True)
The answer was given by acw1668. I just added exportselection=0 during the creation process for the listboxes, and it worked perfectly.
Sorry for the confusion, y'all, I've never really asked for help on here before.
Thanks for the help!
I'm trying to make a memory game and I'm trying to figure out how to call the action hide_text so that the parameter will be the button I clicked.
from tkinter import *
from random import shuffle
root = Tk()
a = list(range(1, 19))
a = list(a) + list(a)
shuffle(a)
def game():
count = 0
for i in a:
if count == 0:
posisonx = 0
posisony = 0
if count == 6:
count = 0
posisonx = 0
posisony += 1
button = Button(root,text=f' {i} ',
command=lambda: hide_text())
button.grid(row=posisony, column=posisonx)
def hide_text(s):
s.config(text='')
posisonx += 1
count += 1
game()
root.mainloop()
There are two problems to solve here. One is that you can't reference a button until it has been created. The other is correctly capturing the reference once it has.
If you want to pass a command involving a reference to the button, you have to split the creation into two lines:
button = Button(root,text=f' {i} ')
button.configure(command=...)
You need to bind button to the lambda that you create. There are a couple of ways to do this. If you just do command=lambda: hide_text(button), all the commands will point to the last button you create in the loop.
One trick is to use the fact that default arguments are bound when the function is created:
button.configure(command=lamda s=button: hide_text(s))
You should take def hide_text outside the loop in this case.
Another way is to capture a closure using a nested function. Put this outside the loop:
def get_hide_text(button):
def hide_text():
button.config(text='')
return hide_text
Then, in the loop, do
button.configure(command=get_hide_text(button))
I am trying to create the function 'count' that takes in an integer in the form of a variable, and adds 1 to it every time the return key is pressed, saving to the variable each time.
The argument needs to remain generic, because in the future this will run the same 'count' function on multiple variables depending on which button is pressed.
I've tried making messi a global variable by putting global messi at the top, but the same problem occurs.
import tkinter as tk
class PlayerStats:
def __init__(self, name, touches):
team = "Barcelona"
self.name = name
self.touches = touches
def count(number):
number = number + 1
print(number)
messi = PlayerStats("Messi",0)
root = tk.Tk()
root.bind('<Return>', lambda event :PlayerStats.count(messi.touches))
root.mainloop()
When I run this snippet, it iterates it once, from 0 to 1, and then resets always printing out 1.
Any thoughts on why this is happening and how to fix would be appreciated!!
You are not saving the result of the operation.
You're instanciating your PlayerStats class with a value of 0 for touches.
That value is then never mutated throughout your code.
When tkinter is calling your count method, it increments number but that variable never leaves the scope of the method, and is thus garbage collected.
To fix it, you should change your class to something like
import tkinter as tk
class PlayerStats:
def __init__(self, name, touches):
team = "Barcelona"
self.name = name
self.touches = touches
def count(self): # the first argument of a method is always a reference to the instance
self.touches += 1
print(self.touches)
messi = PlayerStats("Messi", 0)
root = tk.Tk()
root.bind('<Return>', lambda event: messi.count()) # You need to call the method on the instance you created.
root.mainloop()
Thanks for your help Dogeek. My functioning code from above now looks like this:
import tkinter as tk
class PlayerStats:
def __init__(self, name, touches):
team = "Barcelona"
self.name = name
self.touches = touches
def count(self, number):
self.touches += 1
print(number)
messi = PlayerStats("Messi",0)
root = tk.Tk()
root.bind('<Return>', lambda event :messi.count(messi.touches))
root.mainloop()
One thing this does not solve though is the ability to reuse that function for different variables. I am now trying to come up with an elegant way to do something like this:
import tkinter as tk
class PlayerStats:
def __init__(self, name, touches, shots):
team = "Barcelona"
self.name = name
self.touches = touches
self.shots = shots
def count(self, number):
self.number += 1
print(number)
messi = PlayerStats("Messi",0)
root = tk.Tk()
root.bind('<Return>', lambda event :messi.count(messi.touches))
root.bind('<s>', lambda event :messi.count(messi.shots))
root.mainloop()
where number represents either messi.shots or messi.touches depending on what key is pressed. I'd like to do this without recreating a bunch of nearly identical functions for each key.
I want to use the bot_create function with a button but I keep getting (on line 20) the problem "bots not defined" so I moved the function down below the button but got the problem "bot_create not defined".
I didn't get this problem using C++ and I'm new to Python. How should I arrange the functions?
import tkinter as tk
import numpy as np
import multiprocessing as mp
bots_max = 1000 # Maximum number of bots
bot = []
bot_count = 0
# Menu functions
def save_field():
pass
# Field functions
def field_clear():
pass
# Bots functions
def bots_create():
bot[bot_count] = bots
bot_count += 1
main = tk.Tk()
field_sides = 600
ctrls_width = 200
main.geometry("800x600")
main.resizable(0, 0)
main.title("Swarm Simulator v1.0")
# Controls menu on left side
button1 = tk.Button(main, text = "Button 1").pack(side = "left", command = bots_create())
class environment:
def __init__():
pass
class wall:
def __init__():
pass
# Bots
class bots:
alive = True
def __init__():
alive = True
# Field where bots live
field = tk.Canvas(main, width = field_sides, height = field_sides, bg = "white").pack(side = "right")
for particle in bots:
print("|")
main.mainloop()
Here's a version of your code that fixes all the syntactic problems, and so compiles (what I really mean is that my IDE now thinks its ok). It also runs, but I don't know if it does what you intended. See my comments in the code:
import tkinter as tk
import numpy as np
import multiprocessing as mp
# moved your class defs up to fix problems with accessing them before they are defined
class environment:
def __init__(self): # need a self param here
pass
class wall:
def __init__(self): # need a self param here
pass
# Bots
class bots:
alive = True
def __init__(self): # need a self param here
alive = True
bots_max = 1000 # Maximum number of bots
bot = []
# bot_count = 0 # this no longer does anything. use `len(bot)` to get the number of objects in the 'bot' list
# Menu functions
def save_field():
pass
# Field functions
def field_clear():
pass
# Bots functions
def bots_create():
# bot[bot_count] = bots # this will crash as it is referring to a non-existent location in the list
# also, your use of "bots" here makes no sense
# bot_count += 1 # this makes 'bot_count' a local variable, which is not what you want
bot.append(bots()) # not sure this is what you want, but this creates a new 'bots' object and adds it to the 'bot' list
main = tk.Tk()
field_sides = 600
ctrls_width = 200
main.geometry("800x600")
main.resizable(0, 0)
main.title("Swarm Simulator v1.0")
# Controls menu on left side
button1 = tk.Button(main, text = "Button 1").pack(side = "left", command = bots_create())
# Field where bots live
field = tk.Canvas(main, width = field_sides, height = field_sides, bg = "white").pack(side = "right")
for particle in bot: # maybe you want to iterate over the 'bot' list instead of the 'bots' type?
print("|")
main.mainloop()
As #khelwood says, it seems that you should swap the use of the names bot and bots per the way you are using them