crate memory game in python - python

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

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.

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}

tkinter canvas can't seem to create a tag within a function

Making a basic game using tkinter only. I can use a tag in my main program fine, but when I create one in my function I'm ignored. Below is the problem part of the code. The alienship tag works fine. The tag is saved and is usable. But for the line create inside the function, no matter how I assign the tag, it isn't saved. I've tried tag/tags/string instead of a variable/itemconfig/using bob=/master.bob=
I'm out of ideas.
from tkinter import *
import random
master = Tk()
w = Canvas(master, width=600, height=800,bg='black')
w. pack()
bang = 0 #check if there's a shot active
shottag = "1"
alientag = "a"
def control(event):
global bang,shottag
key = event.keycode
if key == 38: #up arrow
if bang<3:
bang=bang+1
shottag=shottag+"1"
xy = w.coords(ship)
w.create_line(xy[0],700,xy[0],730,fill="white",width=3,tag=shottag)
# w.coords(shottag) would produce [] here -- i.e. no coords
shippic = PhotoImage(file = "ship.gif")
ship = w.create_image(300, 750, anchor=CENTER, image=shippic)
aliens = PhotoImage(file = "head.gif")
w.create_image(random.randint(40,560), 40, anchor=CENTER, image=aliens, tag=alientag)
w.after(100,moveals,alientag)
# this tag works fine
master.bind('<Key>',control)
You created a tag which has only numbers so it looks like "item handle" so it's not treated as tag.
Use char in tag - ie. shottag = "s" - and it will work.
From effbot.org The Tkinter Canvas Widget:
Tags are symbolic names attached to items. Tags are ordinary strings, and they can contain anything except whitespace (as long as they don’t look like item handles).
With variables used inside of a function, you have to return them. For example:
a = 5
b = 6
def func(a,b):
a += 5
b += 5
return a
a = func(a,b)
print(a) #10
print(b) #6, not 11
It doesn't change the variable inside the function, so you must return the variable and reassign it, like I did with a. b won't change unless you reassign it to the function value.
If you need to return multiple values:
return [a, b]
f = func(a, b)
a, b = f[0], f[1]

array not saving input

hey back again with the same code, well edited so it works better. anyway trying to add the button input into the array and that works. what doesn't work is the fact every time i call the function do() the values reset due to them being local. i tried to fix this by making it global(within the class) using the self.store array. this didn't seem to fix the problem so if someone could help would be much appreciated here is the relevant code
def __init__(self,master):#is the master for the button widgets
self.count=0
self.store=["0"]
frame=Frame(master)
frame.pack()
self.addition = Button(frame, text="+", command=self.add)#when clicked sends a call back for a +
self.addition.pack()
self.subtraction = Button(frame, text="-", command=self.sub)#when clicked sends a call back for a -
self.subtraction.pack()
self.equate = Button(frame, text="=", command=self.equate)#when clicked sends a call back for a =
self.equate.pack()
self.one = Button(frame, text="1", command=self.one)#when clicked sends a call back for a -
self.one.pack()
def add(self):
self.do("+")
self.count=self.count+1
def sub(self):
self.do("-")
self.count=self.count+1
def equate(self):
self.do("=")
def one(self):
self.do("1")
self.count=self.count+1
def do(self, X):#will hopefully colaborate all of the inputs
cont, num = True, 0
strstore="3 + 8"#temporarily used to make sure the calculating works
self.store=["2","1","+","2","3","4"]#holds the numbers used to calculate.
for num in range(1):
if X == "=":
cont = False
self.store[self.count]=X
print(self.store[self.count])
print(self.store[:])#test code
if cont == False:
print(self.eval_binary_expr(*(strstore.split())))
self.store=["2","1","+","2","3","4"]
If you initialize it this way in the do function, self.store will be reset to ["2","1","+","2","3","4"] every time you call do(X)
Initialize it outside of the do function if you don't want it to be overwritten.
But if you want to add a value to a list, use append(), extend, or += operator:
self.store+=["2","1","+","2","3","4"]
(if you want it to be done only once, do it in the constructor, __init__ function!)
also,
self.store[self.count]=X
If you try to append to the END of the list self.store, you should just do:
self.store.append(X)
This way, you won't need to count anything, with the risk of forgetting an incrementation and replacing a value by X instead of appending X.
As said above, range(1), it's 0...
Let's do it another way:
def do(self, X):
cont, num = True, 0
liststore=['3', '+', '8']#splitting your string.
#initialize str.store elsewhere!
if X == "=":
cont = False
self.store.append("X")
print(self.store[-1])#getting the last
print(self.store)#the same as self.store[:]
if cont == False:
print(self.eval_binary_expr(*(liststore)))
Simpler, and probably better.
Last thing: in Python, you are usually working with lists (like self.store), not arrays (which come from the module array)

tkinter button commands with lambda in Python

ive tried searching for a solution but couldn't find one that works. I have a 2d list of tkinter Buttons, and i want to change their Text when it is clicked by the mouse. I tried doing this:
def create_board(number):
print(number)
for i in range (0,number):
buttonList.append([])
for j in range(0,number):
print(i,j)
buttonList[i].append(Button(root, text = " ", command = lambda: update_binary_text(i,j)))
buttonList[i][j].pack()
Then when it is clicked it calls this function:
def update_binary_text(first,second):
print(first,second)
buttonList[first][second]["text"] = "1"
When i click a button, it simply does nothing, i had the program display the indexes of the button that was clicked, and they ALL show 4, 4 (this is when the variable number=5) Is there an solution to this?
this is my first python attempt for a class.
Thanks
You can fix this problem by creating a closure for i and j with the creation of each lambda:
command = lambda i=i, j=j: update_binary_text(i, j)
You could also create a callback factory with references to the button objects themselves:
def callback_factory(button):
return lambda: button["text"] = "1"
And then in your initialization code:
for j in range(0, number):
new_button = Button(root, text=" ")
new_button.configure(command=callback_factory(new_button))
new_button.pack()
buttonList.append(new_button)
Whenever I need a collection of similar widgets, I find it's simplest to enclose them in an object and pass a bound-method as callback rather than playing tricks with lambda. So, instead of having a list like buttonList[] with widgets, create an object:
class MyButton(object):
def __init__(self, i, j):
self.i = i
self.j = j
self.button = Button(..., command = self.callback)
def callback(self):
. . .
Now, you have a list buttonList[] of these objects, rather than the widgets themselves. To update the text, either provide a method for that, or else access the member directly: buttonList[i].button.configure(. . .) And when the callback is activated, it has the entire object and whatever attributes you might need in self.

Categories

Resources