How to show every change to an class object in Tkinter? - python

I have this skeleton of a game in two files.
First file mygui.py:
from tkinter import Tk, Label, Button
from mygame import p1, play_game
def rungame():
play_game()
gui_widgets()
root = Tk()
def gui_widgets():
health_label = Label(root, text=f"health: {p1.health}")
health_label.grid(column=0, row=0)
mana_label = Label(root, text=f"mana: {p1.mana}")
mana_label.grid(column=0, row=1)
mybtn = Button(root, text="RUN GAME", command=rungame)
mybtn.grid(column=0, row=2)
gui_widgets()
root.mainloop()
Seccond file mygame:
import time
class Player:
def __init__(self, health, mana):
self.health = health
self.mana = mana
def play_game():
p1.health -= 1
time.sleep(0.5)
p1.mana -= 3
time.sleep(0.5)
p1.mana += 1
p1 = Player(10, 15)
The game shows the begining statistic on gui and after clicking button it shows the end result. I want gui to show every p1 atribute change made in play_game() as they are happening. How would I do this?
Edit: I am keen on keeping two seperate files so that program would be more scalable.
I am not sure if this is possible, but I would figure that if I could run gui_widgets() continuosly (for example every 0,1 secconds) It would probalby work as intended. Still I understand that this may be impossible since tkinter seems to wait everytime when play_game()is running, witch takes at least one seccond to finish. So in that case they should probalby run alongside each other (if that even is a thing).

There maybe lot of ways for it.
Here's one way
Define __setattr__ method to send signal to buffer to update GUI.
sleep in main thread may block your GUI to update, so another thread used here.
A fixed and short timer to fresh/update GUI from the content of buffer.
# mygui.py
import threading
from tkinter import Tk, Label, Button
from mygame import p1, play_game, buffer
def rungame():
threading.Thread(target=play_game, args=(), daemon=True).start()
def update():
if buffer:
attribute, value = buffer.pop(0)
if attribute == 'health':
health_label.configure(text=f"health: {value}")
health_label.update()
elif attribute == 'mana':
mana_label.configure(text=f'mana: {value}')
mana_label.update()
root.after(100, update)
def gui_widgets():
health_label = Label(root, text=f"health:", width=20)
health_label.grid(column=0, row=0)
mana_label = Label(root, text=f"mana:", width=20)
mana_label.grid(column=0, row=1)
mybtn = Button(root, text="RUN GAME", command=rungame)
mybtn.grid(column=0, row=2)
return health_label, mana_label
root = Tk()
health_label, mana_label = gui_widgets()
root.after(100, update)
root.mainloop()
#my game.py
import time
class Player(object):
def __init__(self, health, mana):
self.health = health
self.mana = mana
def __setattr__(self, attribute, value):
super().__setattr__(attribute, value)
buffer.append((attribute, value))
def play_game():
for i in range(10):
p1.health = i
time.sleep(0.2)
p1.mana = 9 - i
time.sleep(0.2)
buffer = []
p1 = Player(10, 15)

Related

Python Tkinter "stop" a while loop inside function

I have a Python program using Tkinter to show a value (var peso inside capturarpeso() function) in realtime.
But the while loop in capturarPeso() doesn't work, the loop only works the first time then the script is "waiting".
If I remove the TK component, it works perfectly. I simplified the script:
import tkinter as tk
from tkinter import *
import threading
import random
def capturarPeso():
global peso
while True:
peso = random.randrange(0, 101, 2)
print (peso)
return(peso)
def capturarPesoHilo():
hilo = threading.Thread(target=capturarPeso, name=None, group=None, args=(), kwargs=None, daemon=True)
hilo.start()
hilo.join()
class ActualizarPeso(Label):
def __init__(self, parent, *args, **kwargs):
Label.__init__(self, parent, *args, **kwargs)
self.tick()
def tick(self):
self.config(text= peso)
self.after(500, self.tick)
capturarPesoHilo()
window = tk.Tk()
window.title('Capturador pesos')
window.resizable(width=False, height=False)
pesoLabel = ActualizarPeso(window, font="Arial 60", fg="red", bg="black", width=8, height= 1)
pesoLabel.grid(row=15, column=0)
window.mainloop()
Any ideas on how to continue? Thank you
The function captuarPeso() has a return statement which will exit the while loop, this is why you only get 1 number printed to the screen.
Removing the return makes it so your program is stuck in that while loop which only prints peso because when you do hilo.join() to a thread it's actually waiting for the thread to exit before continuing, and since we got rid of the return in the first step, the thread never exits and so it's again stuck in a loop. To fix this I changed your while loop to while self.peso != -999: and after calling .mainloop() you set self.peso = -999 which will tell the program: the user has exited the Tkinter interface, exit my loop.
Since you used a class to put some of your tkinter gui in, why not put it all in? Generaly most people would put the entire tkinter interface in a class, I've gone ahead and restructured the program for you but tried to leave as much as the original by itself so you can analyze it and see how it works.
import tkinter as tk
import threading
import random
import time
class ActualizarPeso:
def __init__(self):
self.window = tk.Tk()
self.window.title('Capturador pesos')
self.window.resizable(width=False, height=False)
self.pesoLabel = self.crearLabel()
self.peso = 0
self.tick()
hilo1 = self.capturarPesoHilo()
self.window.mainloop()
self.peso = -999
hilo1.join()
def crearLabel(self):
pesoLabel = tk.Label(self.window, font="Arial 60", fg="red", bg="black", width=8, height=1)
pesoLabel.grid(row=15, column=0)
return pesoLabel
def tick(self):
self.pesoLabel.config(text=self.peso)
self.pesoLabel.after(500, self.tick)
def capturarPeso(self):
while self.peso != -999:
self.peso = random.randrange(0, 101, 2)
print(self.peso)
time.sleep(1)
def capturarPesoHilo(self):
hilo = threading.Thread(target=self.capturarPeso)
hilo.start()
return hilo
ActualizarPeso()
Let me know if you need something explained, and Happy Holidays!

Make a reminder which will remind every X minute using Tkinter

I am totally new in python GUI and Tkinter. Now i want an entry field where i can change the value or time of self.hide when i will execute this code. that means self.hide value will change from Entry field. In this code this value is statically set to 1 minute. need help from experts.
import Tkinter as Tk
import time
import tkMessageBox
class Window:
def __init__(self):
self.root = None
self.hide = 1 #minutes
self.show = 3 #seconds
def close(self):
self.root.destroy()
return
def new(self):
self.root = Tk.Tk()
self.root.overrideredirect(True)
self.root.geometry("{0}x{1}+0+0".format(self.root.winfo_screenwidth(), self.root.winfo_screenheight()))
self.root.configure(bg='black')
Tk.Label(self.root, text='Hello', fg='white', bg='black', font=('Helvetica', 30)).place(anchor='center', relx=0.5, rely=0.5)
#tkMessageBox.showinfo("Notification", "Your time is up. Time to do next job. . .")
Tk.Button(text = 'Close', command = self.close).pack()
self.root.after(self.show*1000, self.pp)
def pp(self):
if self.root:
self.root.destroy()
time.sleep(self.hide*60)
self.new()
self.root.mainloop()
return
Window().pp()
Try This. It may help you.
from Tkinter import *
import time
root = Tk()
def close():
root.destroy()
def show():
root.deiconify()
button.config(text = 'Close', command = close)
root.after(1000, hide)
def hide():
root.withdraw()
time_to_sleep = set_time_to_sleep.get()
time_to_sleep = float(time_to_sleep)
#print time_to_sleep
time.sleep(time_to_sleep)
show()
set_time_to_sleep = Entry(root)
set_time_to_sleep.pack(side=LEFT)
button = Button(text = 'Set Time', command = hide)
button.pack()
root.mainloop()
To summarise:
Instead of using the sleep function, use the after function. This will not freeze the GUI.
Set the "wait" time of the after function self.Entry.get(). This will collect the info you have put into the Entry.
For more info, look at these links. People smarter than myself give a very clear explication on how to use the functions.
Tkinter, executing functions over time
tkinter: how to use after method

tkinter button with changing txt when clicked

So I know what the problem is, I just don't know how to fix it:
self.health gets stored in the variable once and doesn't re-read after that. I've tried using: #property. But I only just learnt about #property yesterday, so either 1: I'm not using it properly, or 2: it can't be used in this situation.
import tkinter as tk
class button_health:
def __init__(self):
self.health = 5
def hit(self, event):
self.health -= 1
bob = button_health()
window = tk.Tk()
button = tk.Button(window, text = bob.health #I want this to update)
button.bind("<Button-1>", bob.hit)
button.pack()
window.mainloop()
What I'm aiming for is for the code to produce a simple tkinter button on screen which starts off saying "5", then when you click it, says "4", then click "3" etc.
Use a Tkinter IntVar variable to track changes to the health value. Hook that variable up to the button label using the textvariable attribute.
import tkinter as tk
class button_health:
def __init__(self, health=5):
self.health = tk.IntVar()
self.health.set(health)
def hit(self, event=None):
if self.health.get() >= 1: # assuming that you can't have negative health
self.health.set(self.health.get() - 1)
window = tk.Tk()
bob = button_health(8)
button = tk.Button(window, textvariable=bob.health, command=bob.hit)
#button = tk.Button(window, textvariable=bob.health)
#button.bind('<Button-1>', bob.hit)
button.pack()
window.mainloop()
Another way is to create your own button class as a subclass of Button and hook up an IntVar as a member of the class. This way you can easily create multiple independent buttons with different health values:
import tkinter as tk
class HealthButton(tk.Button):
def __init__(self, window, health=5, *args, **kwargs):
self.health = tk.IntVar()
self.health.set(health)
super(HealthButton, self).__init__(window, *args, textvariable=self.health, command=self.hit, **kwargs)
def hit(self):
if self.health.get() >= 1:
self.health.set(self.health.get() - 1)
window = tk.Tk()
buttons = [HealthButton(window, i) for i in range(10,15)]
for b in buttons:
b.pack()
window.mainloop()
Use button['text'] or button.config(text={text})
class button_health:
def __init__(self):
self.health = 5
def hit(self, event):
self.health -= 1
button['text'] = str(self.health)
or
class button_health:
def __init__(self):
self.health = 5
def hit(self, event):
self.health -= 1
button.config(text= str(self.health))
There is an argument called command in tk.Button. Binding the button-1 to a function isn't the best way to check if the user clicked the button. And even if you used bind, it should be binded to a function, not a class.
To change a button's text, you can set button['text'] to something.
import tkinter as tk
def button_health():
global health
health -= 1
button['text'] = str(health)
health = 5
window = tk.Tk()
button = tk.Button(window, text = health , command = button_health)
button.pack()
window.mainloop()
You can also avoid using a global statement by doing this:
import tkinter as tk
def button_health(but):
but.health -= 1
but['text'] = str(but.health)
window = tk.Tk()
button = tk.Button(window)
button.health = 5
button['text'] = str(button.health)
button['command'] = lambda: button_health(button)
button.pack()
window.mainloop()
another advantage for doing it this way is that it can keep the health of the button independent, so if you have multiple buttons, this will keep the counters for all the buttons different.

Python tkinter GUI freezing/crashing

from Tkinter import *
import tkFileDialog
import tkMessageBox
import os
import ttk
import serial
import timeit
import time
######################################################################################
class MyApp:
def __init__(self, parent):
########################################################
#Setup Frames
self.MiddleFrame = Frame(parent) #Middle Frame
self.MiddleFrame.pack()
#GLOBAL VARIABLES
self.chip_number = 0 #number of chip testing
###########################################
#Middle Frame setup
Label(self.MiddleFrame, text='Done').grid(row=8, column=1, sticky = E)
self.Done = Canvas(self.MiddleFrame, bg="yellow", width=10, height=10)
self.Done.grid(row=8, column=2)
Label(self.MiddleFrame, text='Chip Number:').grid(row=9, column=1, sticky = E)
#start button
self.button1 = Button(self.MiddleFrame,state=NORMAL, command= self.start_pre)
self.button1["text"]= "START"
self.button1.grid(row=1, column=2, sticky = E)
###########################################
#Action of Start Button
def start_pre(self):
x = 0
while x<10000:
self.start_button()
x=x+1
#Talking to Board
def start_button(self):
#increase chip count number and update
self.chip_number += 1
Label(self.MiddleFrame, text=str(self.chip_number)).grid(row=9, column=2, sticky = E)
#reset-yellow
self.reset_color()
print "Still Working", self.chip_number
self.Done.configure(background="green")
self.Done.update_idletasks()
###############################################################
#Color Boxes
#Reset
def reset_color(self):
self.Done.configure(background="yellow")
self.Done.update_idletasks()
###############################################################################################################
#Start Programs
root = Tk() #makes window
root.title("Interface")
myapp = MyApp(root) #this really runs program
root.mainloop() #keep window open
With my program, i first push the start button.
I will print "still working" and the GUi will update chip number and blink done light over and over. The start button go to function that will execute 10000 times. However after 3000 iterations, the gui freeze, but the program is still print "still working". How do I keep the gui from crashing?
There are many problems with your code. For one, this is fundamentally flawed:
while self.stop == True:
self.start_button()
time.sleep(0.5)
You simply can't expect a GUI to behave properly with code like that. As a general rule of thumb you should never have the main thread of a GUI call sleep. Causing sleep prevents the event loop from processing any events, including low level events such as requests to refresh the screen.
The use of sleep has been asked and answered many times on stackoverflow. You might find some of those questions useful. For example,
windows thinks tkinter is not responding
Python Tkinter coords function not moving canvas objects inside loop
How do widgets update in Tkinter?
Tkinter multiple operations
Python Tkinter Stopwatch Error
You have another problem that falls into the category of a memory leak. From that while loop, you call self.start_button() indefinitely. This happens about once a second, due to sleep being called for half a second in the loop, and another half a second in start_button.
Each time you call start_button, you create another label widget that you stack on top of all previous widgets in row 9, column 2. Eventually this will cause your program to crash. I'm surprised that it causes your program to fail so quickly, but that's beside the point.
My recommendation is to start over with a simple example that does nothing but update a label every second. Get that working so that you understand the basic mechanism. Then, once it's working, you can add in your code that reads from the serial port.
May I suggest that you start over with the following code? You can port in back to Python 2 if needed, but your program has been rewritten to use Python 3 and has been designed to use tkinter's ability to schedule future events with the after methods. Hopefully, you will find the code easier to follow.
import collections
import timeit
import tkinter
def main():
root = Application()
root.setup()
root.mainloop()
class Application(tkinter.Tk):
def setup(self):
mf = self.__middle_frame = tkinter.Frame(self)
self.__middle_frame.grid()
bf = self.__bot_frame = tkinter.Frame(self)
self.__bot_frame.grid()
self.__port_set = False
self.__chip_number = 0
self.__chip_pass_num = 0
self.__chip_fail_num = 0
self.__chip_yield_num = 0
self.__stop = True
self.__widgets = collections.OrderedDict((
('COT', 'Continuity Test'), ('CHE', 'Chip Erase'),
('ERT', 'Erase Test'), ('WRT', 'Write Test'),
('WIRT', 'Wire Reading Test'), ('WIT', 'Wire Reading Test'),
('WRAT', 'Write All Test'), ('DO', 'Done')))
for row, (key, value) in enumerate(self.__widgets.items()):
label = tkinter.Label(mf, text=value+':')
label.grid(row=row, column=0, sticky=tkinter.E)
canvas = tkinter.Canvas(mf, bg='yellow', width=10, height=10)
canvas.grid(row=row, column=1)
self.__widgets[key] = label, canvas
self.__cn = tkinter.Label(mf, text='Chip Number:')
self.__cn.grid(row=8, column=0, sticky=tkinter.E)
self.__display = tkinter.Label(mf)
self.__display.grid(row=8, column=1, sticky=tkinter.E)
self.__button = tkinter.Button(bf, text='START',
command=self.__start_pre)
self.__button.grid(sticky=tkinter.E)
def __start_pre(self):
self.__button['state'] = tkinter.DISABLED
self.__start_button(0)
def __start_button(self, count):
if count < 100:
self.__chip_number += 1
self.__display['text'] = str(self.__chip_number)
self.__widgets['DO'][1]['bg'] = 'yellow'
start_time = timeit.default_timer()
print('Still Working:', self.__chip_number)
self.after(500, self.__end_button, count)
else:
self.__button['state'] = tkinter.NORMAL
def __end_button(self, count):
self.__widgets['DO'][1]['bg'] = 'green'
self.after(500, self.__start_button, count + 1)
if __name__ == '__main__':
main()

Move tkinter label using button command

I am trying to recreate the boardgame monopoly using python and tkinter. I know how to place a label on a canvas or a frame, but how should I do this command is being run from another function in the class? I tried it using some function within the class Board, but then the error rises that the label, canvas, etc. are not defined as this happens in __init__(self,parent). How can I solve these errors? Or should I take a different approach to this? Hope I made my problem clear.
import tkFileDialog
from random import randint
class Board(Frame):
def __init__(self,parent):
##create the board
frame = Frame(parent)
frame.pack()
Frame.__init__(self,parent)
frame2 = Frame(frame)
frame2.pack()
c=Canvas(frame2,width=480,height=480)
c.pack(expand=YES,fill=BOTH)
c.background=PhotoImage(file='Board.gif')
c.create_image(0,0,image=c.background,anchor='nw')
##Add player 1
player1=PhotoImage(file='plane.gif')
label_player1 = Label(c,image=player1)
label_player1.image=player1
label_player1.place(x=430,y=420)
##Add player 2
player2=PhotoImage(file='car.gif')
label_player2 = Label(c,image=player2)
label_player2.image=player2
label_player2.place(x=430,y=450)
button = Button(frame, text="Next turn", command=self.next_turn)
button.pack()
button = Button(frame, text="Roll the dice", command=self.roll)
button.pack()
def roll(self):
number=randint(2,12)
if b==0:
self.place_player_down()
return number
def place_player_down(self):
for i in range(number+1):
h=int(430-i*30)
while h>=0:
player2=PhotoImage(file='car.gif')
label_player2 = Label(c,image=player2)
label_player2.image=player2
label_player2.place(x=h,y=420)
root = Tk()
board = Board(root)
board.pack()
root.mainloop()
The approach is correct (wrap your Tkinter widgets in a class with the event handler functions as methods), but you forgot to set the widgets as attributes of the class using the reference to self:
class Board(Frame):
def __init__(self,parent):
# ...
self.c = Canvas(frame2,width=480,height=480)
self.c.pack(expand=YES,fill=BOTH)
# ...
def place_player_down(self):
# Use 'self.c', not just 'c'
I think you want to do something similar with the value number, but it that case I would send it as an argument to place_player_down:
def roll(self):
number=randint(2,12)
if b==0:
self.place_player_down(number)
return number # Keep in mind that this value is returned but not used anymore
def place_player_down(self, number):
# Use 'number'

Categories

Resources