I am trying to create something simple using Tkinter. In it, I am creating a series of frames and then using frame.grid() putting them inside of another frame. I am able to retrieve the frame and print out information about it, but I am unable to change the color.
I have looked through other posts and was unable to find a solution. I have tried:
Updating the color using .config(bg=color)/.configure(bg=color)
Updating the color using frame['bg']=
Placing the function before or after the creation of the frames.
I am able to edit the color of the frames when I am calling a function through the <Button-1> event, but when I am running a different function, it is unable to change the colors.
I am using Replit, here is the important code:
window = tk.Tk()
window.wm_attributes('-fullscreen',True)
window.config(bg='#111111')
height = window.winfo_screenheight()
width = window.winfo_screenwidth()
#window.geometry(f'{height}x{height}')
rows = height//gridSize
columns = height//gridSize
frame = tk.Frame(master=window, bg='#111111', width=height, height=height, pady=10)
frame.pack(side=tk.BOTTOM)
def getNumLiveNeighbors(cellRow, cellColumn):
numLiveNeighbors = 0
# print(cellRow, cellColumn)
r = -2
for i in range(3):
c = -2
r += 1
for a in range(3):
c += 1
if(r==0)and(c==0):
continue
nRow, nCol = cellRow+r, cellColumn+c
if(((nRow<0)or(nRow>=gridSize))or(nCol<0)or(nCol>=gridSize)):
continue
neighbor = frame.grid_slaves(row=nRow, column = nCol)[0]
if(neighbor['bg']=='#ffffff'):
numLiveNeighbors += 1
#neighbor.config(bg='#252525')
return numLiveNeighbors
def toggleColor(event):
widget = event.widget
if(widget['bg']=='#ffffff'):
widget.config(bg='#222222')
else:
widget.config(bg='#ffffff')
info = widget.grid_info()
print(getNumLiveNeighbors(info['row'], info['column']))
print(event.x, event.y)
return
for i in range(gridSize):
window.columnconfigure(i, weight = 1)
window.rowconfigure(i, weight = 1)
for i in range(gridSize):
cellGrid[i] = {}
for j in range (gridSize):
box = tk.Frame(master=frame, bg='#222222', relief=tk.RAISED, width=columns, height=columns)
cellGrid[i][j] = box
box.bind('<Button-1>', toggleColor)
box.grid(row=i,column=j,sticky='nsew')
def runGame(event):
frameCount = 0
print('Running game!')
while(True):
print(f'Frame #{frameCount}')
frameCount += 1
for row in range(gridSize):
for col in range(gridSize):
time.sleep(0.5)
cell = frame.grid_slaves(row=row, column=col)[0]
numNeighbors = getNumLiveNeighbors(row, col)
print(numNeighbors)
if(numNeighbors<2):
print('Less than 2, do nothing')
#cell.config(bg='#222222')
elif(numNeighbors==3):
print('3 Neighbors, make cell alive!')
cell['bg']='#ffffff'
elif(numNeighbors>3):
print('Greater than 3 neighbors, kill the cell')
cell['bg']='#222222'
print(cell['bg'])
window.bind('<Return>', runGame)
If thats too much to read I can break it down a bit more and explain what I'm doing with each function/line.
The main goal of this was to create a cellular automata like Conway's Game of Life, but I've been stuck trying to update the color of the "cells".
You CANNOT use an infinite loop like that. Tk, like all GUI frameworks, is event driven. When you create a window or make a change, that doesn't actually do any drawing. It just sends a message. That message will be dispatched and acted upon when you get back to the main loop. In your case, you never exit your loop, so your handler never returns, the main loop never runs, and your UI freezes.
Instead of an infinite loop with time.sleep(0.5), you need to use window.after to request a timer callback. Do one generation and return (after calling window.after again). That way, the main loop stays in control, and all will be well.
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.
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 want to add another widget, in this case a scale widget, to a menu widget in tkinter.
Right now the only solutions I see are creating a new command and open a new window with the scale widget or creating the scale widget elsewhere. Both don't seem too appealing to me.
Any ideas how to archive this are welcome :)
You cant add a scrollbar to it, but I have coded something similar to this. Its a hacky way and maybe its hard to understand but I can try to explain.
Note as Bryan mentioned in the linked Thread, this seems to be a a Windows only solution.
import tkinter as tk
def my_first_function():
print('first')
def my_second_function():
print('second')
def check_for_scroll(event):
check = root.call(event.widget, "index","active")
if check == 0: #index of button up
scroll_up()
root.after(100,lambda e=event:check_for_scroll(e)) # check again after 100ms
if check == file_menu.index('end'):
scroll_down()
root.after(100,lambda e=event:check_for_scroll(e))
def scroll_up():
index_of_first_command=1
index_of_last_command=1
label_of_last_command = file_menu.entrycget(index_of_first_command, 'label')
try:
for i, k in enumerate(dict_of_commands):
if k == label_of_last_command:
previous_command_label = list(dict_of_commands)[i-1]
previous_command = list(dict_of_commands.values())[i-1]
if i != 0: #avoid to get the last as first
file_menu.delete(index_of_first_command) #first before pull down button
file_menu.insert_command(index_of_first_command,
label=previous_command_label,
command=previous_command)
except Exception as e:
print(e)
def scroll_down():
index_of_first_command=1
index_of_last_command=1
label_of_last_command = file_menu.entrycget(index_of_last_command, 'label')
try:
for i, k in enumerate(dict_of_commands):
if k == label_of_last_command:
next_command_label = list(dict_of_commands)[i+1]
next_command = list(dict_of_commands.values())[i+1]
file_menu.delete(index_of_first_command) #first after pull up button
file_menu.insert_command(index_of_last_command,
label=next_command_label,
command=next_command)
except:
pass
space = ' '
dict_of_commands = {'first' : my_first_function,
'second': my_second_function}
root = tk.Tk()
menubar = tk.Menu(root)
root.config(menu=menubar)
file_menu = tk.Menu(menubar,tearoff=0)
menubar.add_cascade(label='File', menu=file_menu)
file_menu.bind('<<MenuSelect>>', check_for_scroll)
file_menu.add_command(label=space+u'\u25B2'+space, font=["Arial", 8,'bold'])
file_menu.add_command(label='first', command=my_first_function)
file_menu.add_command(label=space+u'\u25BC'+space, font=["Arial", 8,'bold'])
root.mainloop()
So this code creates your window and a menubar on it as usal:
root = tk.Tk()
menubar = tk.Menu(root)
root.config(menu=menubar)
file_menu = tk.Menu(menubar,tearoff=0)
menubar.add_cascade(label='File', menu=file_menu)
file_menu.add_command(label=space+u'\u25B2'+space, font=["Arial", 8,'bold'])
file_menu.add_command(label='first', command=my_first_function)
file_menu.add_command(label=space+u'\u25BC'+space, font=["Arial", 8,'bold'])
root.mainloop()
Important for you, is this line here:
file_menu.bind('<<MenuSelect>>', check_for_scroll)
This line binds the event MenuSelect and it happens/triggers if your cursor hovers over a command of your menu. To this event I have bound a function called check_for_scroll and it looks like this:
def check_for_scroll(event):
check = root.call(event.widget, "index","active")
if check == 0: #index of button up
scroll_up()
root.after(100,lambda e=event:check_for_scroll(e)) # check again after 100ms
if check == file_menu.index('end'):
scroll_down()
root.after(100,lambda e=event:check_for_scroll(e))
The line below checks for the index of the command that has triggered the event. With this we check if its button of our interest like the first or last, with the arrows.
check = root.call(event.widget, "index","active")
if its the first for example this code here is executed:
if check == 0: #index of button up
scroll_up()
root.after(100,lambda e=event:check_for_scroll(e)) # check again after 100ms
it calls/triggers the function scroll_up and uses then the after method of tkinter to retrigger itself, like a loop. The scroll_up function is build like the scroll_down just in the opposite direction. Lets have a closer look:
def scroll_up():
index_of_first_command=1
index_of_last_command=1
label_of_last_command = file_menu.entrycget(index_of_first_command, 'label')
try:
for i, k in enumerate(dict_of_commands):
if k == label_of_last_command:
previous_command_label = list(dict_of_commands)[i-1]
previous_command = list(dict_of_commands.values())[i-1]
if i != 0: #avoid to get the last as first
file_menu.delete(index_of_first_command) #first before pull down button
file_menu.insert_command(index_of_first_command,
label=previous_command_label,
command=previous_command)
except Exception as e:
print(e)
In this function we need to know the first and the last position of commands, because we want to delete one and insert another on that position/index. To achieve this I had created a dictionary that contains the label and the the function of the command item of tkinter like below. (This could be created dynamically, but lets keep it for another question)
dict_of_commands = {'first' : my_first_function,
'second': my_second_function}
So we iterate over this enumerated/indexed dictionary in our function and check if the k/key/label is our item of interest. If true, we get the previous_command by listing the dictionary keys and get the extract the key before by this line:
next_command_label = list(dict_of_commands)[i+1]
similar to the value of the dictionary with this line:
next_command = list(dict_of_commands.values())[i+1]
After all we can delete one and insert one where we like to with this:
file_menu.delete(index_of_first_command) #first after pull up button
file_menu.insert_command(index_of_last_command,
label=next_command_label,
command=next_command)
I know that this code can improved by a lot but it seems hard enough to understand as it is. So dont judge me please.
I hope this solves your question, even if it isnt in the way you wanted to. But this code avoids you from hardly code a own menubar.
If there are questions left on my answer, let me know.
I'm using a Kvaser Leaf Professional and trying to read the can-bus data from a motorcycle through Python coding to be able to use the data to see where values change and which values stays constant.
At the moment I'm struggling to display the data on tkinter and to update it after each time the data is read/updated again, because it changes every few microseconds. Normal ways of using .after() doesn't seem to work 100%, or I'm just doing something wrong. Or is there a better way to display updated data than tkinter?
The other problem I have is when I display the tkinter it uses a root.mailoop() to run the window and when that happens it enters that main loop and doesn't exit it and run the rest of the program and also won't be able to update the existing data displayed on the window. Is there a way to bypass the root.mainloop() maybe?
Any insight will be helpful and/or small errors I've made in my code. I started using Python only 2 days ago and I'm still learning a lot and don't know all the in's and out's yet.
Thank you in advance.
Here is my whole program so far:
import keyboard
import time
import sys
import tkinter as tk
import binascii
from canlib import canlib, Frame
from canlib.canlib import ChannelData
root = tk.Tk()
def setUpChannel(channel,
openFlags=canlib.canOPEN_ACCEPT_VIRTUAL,
bitrate=canlib.canBITRATE_500K,
bitrateFlags=canlib.canDRIVER_NORMAL):
ch = canlib.openChannel(channel, openFlags)
print("Using channel: %s, EAN: %s" % (ChannelData(channel).channel_name,
ChannelData(channel).card_upc_no)
)
ch.setBusOutputControl(bitrateFlags)
ch.setBusParams(bitrate)
ch.busOn()
return ch
def tearDownChannel(ch):
ch.busOff()
ch.close()
def text(t):
tx = binascii.hexlify(t).decode('utf-8')
n = 2
txt = [tx[i:i+n] for i in range(0, len(tx), n)]
return txt
def counter():
while True:
try:
cnt = 1
frame = ch0.read()
firstID = frame.id
while True:
frame = ch0.read()
cnt += 1
if frame.id == firstID:
break
pass
except (canlib.canNoMsg):
break
except (canlib.canError):
print("Rerun")
pass
return cnt
print("canlib version:", canlib.dllversion())
ch0 = setUpChannel(channel=0)
ch1 = setUpChannel(channel=1)
frame = Frame(id_=100, data=[1, 2, 3, 4], flags=canlib.canMSG_EXT)
ch1.write(frame)
print(frame)
cnt = counter()
print("Counter: %d" %(cnt))
time.sleep(1)
while True:
while True:
try:
show = ""
i = 1
while i <= cnt:
frame = ch0.read()
show = show +("%s\t%s\n" %(frame.id, text(frame.data)))
i += 1
print(show)
T = tk.Text(root, height=6, width=80)
T.config(state="normal")
T.insert(tk.INSERT, show)
T.pack()
root.mainloop()
break
except (canlib.canNoMsg) as ex:
pass
except (canlib.canError) as ex:
print(ex)
pass
tearDownChannel(ch0)
tearDownChannel(ch1)
You can edited my code as you want, it will help a lot if I can see how to implement the code better.
You dont need to use multi-threading or anything convoluted. Just use the widget.after() method that calls another function/method of your choosing after the desired time. In the case below, the time is 1 second -> 1000 milliseconds. Just call the class before the mainloop and it will start working.
class UI:
def __init__(self, parent):
# variable storing time
self.seconds = 0
# label displaying time
self.label.configure(text="%i s" % self.seconds)
# start the timer
self.label.after(1000, self.refresh_label)
def refresh_label(self):
#refresh the content of the label every second
# increment the time
self.seconds += 1
# display the new time
self.label.configure(text="%i s" % self.seconds)
# request tkinter to call self.refresh after 1s (the delay is given in ms)
self.label.after(1000, self.refresh_label)
I am trying to figure out how to change a rectangle's color continuously, with a second between each change. Right now I have this simple function which makes a window with a square above a button, that changes the color of the square after every button click:
def junk():
def random_color():
red = int(random.random()*256)
green = int(random.random()*256)
blue = int(random.random()*256)
return '#' + ('{:0>#02X}'*3).format(red,green,blue)
def change_color():
c.itemconfig(r, fill=random_color())
x = Tkinter.Tk()
c = Tkinter.Canvas(master=x)
c['width'] = 400; c['height'] = 400
r = c.create_rectangle(0,0,400,400)
b = Tkinter.Button(master=x, command=change_color)
b['text'] = 'change color'
c.pack(); b.pack(); x.mainloop()
What I want is to be able to click once, and then have the colors change automatically. I know I want to use a CheckButton instead of a Button for this, so that one click will start the loop, and and the next click will stop it.
Also, this is not how I am structuring my "real" code, this is how I am testing from the IDLE shell. Defining the helper functions inside the junk function makes it easy to get at all the relevant code at once, without having the bloat of a full class. So please don't give me comments on style, this is quick and dirty on purpose.
TL;DR I'm not sure how to get a continuous loop running to change the color, while being able to start and stop the loop with a button click.
I figured it out. Before I show my solution, I want to correct a mistaken statement I made above: I don't to use a Checkbutton to make this work. I can make a normal button into a toggle button by changing the 'relief' option of the button. Here is my solution:
def junk():
def color_poll():
global alarm
c.itemconfig(r, fill=random_color())
if keep_going:
alarm = c.after(1000, color_poll)
def change_color():
global keep_going, alarm
if not keep_going:
keep_going = True
b['text']='STOP';b['fg']='red';b['relief']=Tkinter.SUNKEN
color_poll()
else:
keep_going = False; c.after_cancel(alarm); alarm = None
b['text']='GO';b['fg']='green';b['relief']=Tkinter.RAISED
x = Tkinter.Tk()
c = Tkinter.Canvas(master=x)
c['width'] = 400; c['height'] = 400
r = c.create_rectangle(0,0,400,400)
global keep_going, alarm
keep_going = False; alarm = None
b = Tkinter.Button(master=x, command=change_color)
b['text'] = 'GO';b['fg']='green';b['font']='Arial 16';b['relief']=Tkinter.RAISED
c.pack(); b.pack(); x.mainloop()
I'm using the same random_color function, but I moved it out because it out of the junk function because it didn't need to be there.