I have a problem in implementing a fast reader program with tkinter, and that is because it hangs after ~5 seconds.
I know that is because of the while loop but I didn't figured out a way to solve it. Maybe looping outside of the root mainloop?
Here is the code:
import tkinter
import tkinter.ttk
import tkinter.filedialog
import tkinter.messagebox
class FastReader(object):
SLEEP_TIME = 250
def __init__(self):
self.filename = self.getFile()
self.running = True
def window(self):
self.root = tkinter.Tk()
self.root.wm_title("Fast Reader.")
self.text = tkinter.ttk.Label(self.root, text = "word")
self.text.grid(row = 0, column = 0)
# Stop button
self.stop = tkinter.ttk.Button(self.root, text = "Stop", command = lambda: self.close())
self.stop.grid(row = 1, column = 1, columnspan = 2)
while self.running:
for word in self.getWords():
self.text.after(FastReader.SLEEP_TIME)
self.text.config(text = word)
self.text.update_idletasks()
self.root.update_idletasks()
self.root.mainloop()
def close(self):
self.running = False
def getFile(self):
file_ = tkinter.filedialog.askopenfilename()
return file_
def getWords(self):
with open(self.filename) as file_:
for line in file_:
for word in line.strip("\n").split(" "):
yield word
if __name__ == "__main__":
fr = FastReader()
fr.window()
Your program appears to hang because you process all of the words before you give the event loop a chance to update the display, then you put a call to the event loop in an infinite loop.
A good rule of thumb is to never have your own infinite loop in a Tkinter GUI. You already have an infinite loop running, so take advantage of it.
What you should do is write a function that puts the next word in the label, and then calls itself again in the future. It looks somewhat recursive, but it isn't quite. You're simply adding something on to the list of things that the event loop must do. Because it's not strictly recursive, you don't have to worry about running out of stack space.
A simple first step is to first read in all of the words at once. Then, your program removes the first word in the list and displays it in the label. If the list is not empty after that, schedule the command to run again after a delay. It looks something like this:
def showNextWord(self):
word = self.words.pop(0)
self.text.configure(text=word)
if len(self.words) > 0:
self.root.after(self.SLEEP_TIME, self.showNextWord)
You can then replace your while statement with two statements: one to get the complete list of words, and one to display the first word:
self.words = self.getWords()
self.showNextWord()
Of course, you need to modify your getWords() to return the whole list at once. You can use a generator, but it adds a tiny bit of complexity that doesn't seem necessary, unless you're planning on displaying millions of words (which, at four per second, would run for more than several days).
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 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)
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()
I am trying to create a Python script to highlight specific patterns in a .txt file. To do this, I have altered a script which used Tkinter to highlight a given set of data. However, the files I tend to get it to process are around 10000 lines, which results in slow scrolling as I think it renders everything - whether it is on the screen or not (correct me if I'm wrong). Is it possible to alter my code such that it renders the output in a more efficient way? I have tried searching for a means to do this, but have not found anything myself.
My code is as follows:
from Tkinter import *
class FullScreenApp(object):
def __init__(self, master, **kwargs):
self.master=master
pad=3
self._geom='200x200+0+0'
master.geometry("{0}x{1}+0+0".format(
master.winfo_screenwidth()-pad, master.winfo_screenheight()-pad))
master.bind('<Escape>',self.toggle_geom)
def toggle_geom(self,event):
geom=self.master.winfo_geometry()
print(geom,self._geom)
self.master.geometry(self._geom)
self._geom=geom
root = Tk()
app = FullScreenApp(root)
t = Text(root)
t.pack()
#Import file
with open('data.txt') as f:
for line in f:
t.insert(END, line)
#Search terms - Leave blank if not required
search_term0 = '0xCAFE'
search_term1 = '0x0011'
search_term2 = '0x961E'
search_term3 = '0x0000'
search_term4 = ''
#Assigns highlighted colours for terms not blank
t.tag_config(search_term0, background='red')
if search_term1 != '':
t.tag_config(search_term1, background='red')
if search_term2 != '':
t.tag_config(search_term2, background='red')
if search_term3 != '':
t.tag_config(search_term3, background='red')
if search_term4 != '':
t.tag_config(search_term4, background='red')
#Define search
#Requires text widget, the keyword, and a tag
def search(text_widget, keyword, tag):
pos = '1.0'
while True:
idx = text_widget.search(keyword, pos, END)
if not idx:
break
pos = '{}+{}c'.format(idx, len(keyword))
text_widget.tag_add(tag, idx, pos)
#Search for terms that are not blank
search(t, search_term0, search_term0)
if search_term1 != '':
search(t, search_term1, search_term1)
if search_term2 != '':
search(t, search_term2, search_term2)
if search_term3 != '':
search(t, search_term3, search_term3)
if search_term4 != '':
search(t, search_term4, search_term3)
root.mainloop()
An example of the data in a file is given in the following link: here
Many thanks for your time, it is really appreciated.
Assuming MCVE is the following:
import tkinter as tk
def create_text(text_len):
_text = list()
for _ in range(text_len):
_text.append("{}\n".format(_))
_text = "".join(_text)
return _text
if __name__ == '__main__':
root = tk.Tk()
txt = tk.Text(root)
txt.text = create_text(10000)
txt.insert('end', txt.text)
txt.pack()
root.mainloop()
Analysis
Based on this I don't think it is a rendering issue. I think it's an issue with having a fixed rate of registering <KeyPress> events. Meaning that the number of events registered per second is fixed, even though the hardware may be capable of registering at a faster rate. A similar regulation should be true also for the mouse-scroll event.
Solutions for rendering
Perhaps slicing text for a buffer proportion of txt['height'] would help. But isn't that how Tk supposed to be rendering anyway?
Solutions for rendering unrelated issue
If a step would be defined as the cursor's movement to the previous or the next line, for every registered event of Up or Down; then scrolling_speed = step * event_register_frequency.
By increasing the step size
An easy workaround would be to simply increase the step size, as in increasing the number of lines to jump, for each registration of the key bind.
But there's already such default behavior, assuming the page length > 1 line, Page Up or Page Down has a step size of a page. Which makes the scrolling speed increase, even though the event registration rate remains the same.
Alternatively, a new event handler with a greater step size may be defined to call multiple cursor movements for each registration of Up and Down, such as:
import tkinter as tk
def create_text(text_len):
_text = list()
for _ in range(text_len):
_text.append("{}\n".format(_))
_text = "".join(_text)
return _text
def step(event):
if txt._step_size != 1:
_no_of_lines_to_jump = txt._step_size
if event.keysym == 'Up':
_no_of_lines_to_jump *= -1
_position = root.tk.call('tk::TextUpDownLine', txt, _no_of_lines_to_jump)
root.tk.call('tk::TextSetCursor', txt, _position)
return "break"
if __name__ == '__main__':
root = tk.Tk()
txt = tk.Text(root)
txt.text = create_text(10000)
txt.insert('end', txt.text)
txt._step_size = 12
txt.bind("<Up>", step)
txt.bind("<Down>", step)
txt.pack()
root.mainloop()
By mimicking keypress event registry rate increase:
As mentioned in here actually modifying keypress registry rate is out of scope of Tk. Instead, it can be mimicked:
import tkinter as tk
def create_text(text_len):
_text = list()
for _ in range(text_len):
_text.append("{}\n".format(_))
_text = "".join(_text)
return _text
def step_up(*event):
_position = root.tk.call('tk::TextUpDownLine', txt, -1)
root.tk.call('tk::TextSetCursor', txt, _position)
if txt._repeat_on:
root.after(txt._repeat_freq, step_up)
return "break"
def step_down(*event):
_position = root.tk.call('tk::TextUpDownLine', txt, 1)
root.tk.call('tk::TextSetCursor', txt, _position)
if txt._repeat_on:
root.after(txt._repeat_freq, step_down)
return "break"
def stop(*event):
if txt._repeat_on:
txt._repeat_on = False
root.after(txt._repeat_freq + 1, stop)
else:
txt._repeat_on = True
if __name__ == '__main__':
root = tk.Tk()
txt = tk.Text(root)
txt.text = create_text(10000)
txt.insert('end', txt.text)
txt._repeat_freq = 100
txt._repeat_on = True
txt.bind("<KeyPress-Up>", step_up)
txt.bind("<KeyRelease-Up>", stop)
txt.bind("<KeyPress-Down>", step_down)
txt.bind("<KeyRelease-Down>", stop)
txt.pack()
root.mainloop()
By both increasing step-size and mimicking registry rate increase
import tkinter as tk
def create_text(text_len):
_text = list()
for _ in range(text_len):
_text.append("{}\n".format(_))
_text = "".join(_text)
return _text
def step_up(*event):
_no_of_lines_to_jump = -txt._step_size
_position = root.tk.call('tk::TextUpDownLine', txt, _no_of_lines_to_jump)
root.tk.call('tk::TextSetCursor', txt, _position)
if txt._repeat_on:
root.after(txt._repeat_freq, step_up)
return "break"
def step_down(*event):
_no_of_lines_to_jump = txt._step_size
_position = root.tk.call('tk::TextUpDownLine', txt, _no_of_lines_to_jump)
root.tk.call('tk::TextSetCursor', txt, _position)
if txt._repeat_on:
root.after(txt._repeat_freq, step_down)
return "break"
def stop(*event):
if txt._repeat_on:
txt._repeat_on = False
root.after(txt._repeat_freq + 1, stop)
else:
txt._repeat_on = True
if __name__ == '__main__':
root = tk.Tk()
txt = tk.Text(root)
txt.text = create_text(10000)
txt.insert('end', txt.text)
txt._step_size = 1
txt._repeat_freq = 100
txt._repeat_on = True
txt.bind("<KeyPress-Up>", step_up)
txt.bind("<KeyRelease-Up>", stop)
txt.bind("<KeyPress-Down>", step_down)
txt.bind("<KeyRelease-Down>", stop)
txt.pack()
root.mainloop()
So this is solved by something called multi threading. A computer can do multiple tasks at once, otherwise, your web experience wouldn't be the same. Here is a simple function that demonstrates mulit-threading
from threading import Thread
def execOnDifferentThread(funct=print, params=("hello world",)):
t = Thread(target=funct, args=params)
t.start()
Now note, this might not be the best example, but all you have to do to run a function in parallel now, is execOnDifferentThread(funct=A, params=B) where A is a function name, and B is a tuple of arguments that will be passed on to your function. Now, I don't want to write your code for you, but using this function, you can multi-thread certain parts of your code to make it faster. If you are truly stuck, just comment where and ill help. But please try on your own first, now that you have the power of multi-threading on your hands
What I am trying to do is, have a list of characters,each of which is a procedure, then I want to pick randomly (or pseudo randomly, it doesn't matter) from this list, and execute that procedure, then I want to be able to run the it again, and not get the same value,for example if I have five values I want to be able to run it 5 times, then the 6th time I run it, it returns nothing. Here is the code:
from Tkinter import*
from random import randint
Characters=[Percy,Annabeth,Leo,Chuck,Sarah]
Used = []
def callback():
end = len(Characters)-1
rando = randint(0,end)
Characters[rando]
for i in Characters:
if Characters[rando] in Used:
print 'This has already been used'
else:
Characters[rando]()
Used.append(Characters[rando])
break
game = Tk()
game.geometry('50x50+700+100')
Button1 = Button(game,text = '1',command =lambda:callback() )
Button1.pack(side=LEFT)
game.mainloop()
I am trying to get
callback()
to run properly, I have tried what you see but I have also tried
if Characters[rando] in Used:
print 'This has already been used'
else:
Characters[rando]
Used.append(Characters[rando])
in both cases it will run the same procedure multiple times, for example, 'Leo' might be executed 3 times in a row. I searched for hours for a way to do this, but I couldn't find one.
First, I would shuffle the Characters:
Characters = [Percy,Annabeth,Leo,Chuck,Sarah]
random.shuffle(Characters)
Now when you run your callback, you pop one character out:
def callback():
try:
C = Characters.pop() #popping the last one is more efficient than the first.
except IndexError:
return None
return C()
Since this destroys Characters, you may want to keep a copy of it around to reset if you need to:
random.shuffle(Characters)
Characters_save = Characters[:]
def reset_characters():
Characters[:] = Characters_save[:]
Completely untested - but you could implement a basic class:
from random import shuffle
class CallNext(object):
def __init__(self, vals):
self.vals = vals[:]
shuffle(self.vals)
self.iter = iter(self.vals)
def __call__(self):
try:
next(self.iter)()
except StopIteration as e:
pass # or do something smarter?
Another option instead of catching StopIteration would be to use:
next(self.iter, lambda: None)()
And then have:
Button1 = Button(game, text='1', command=CallNext(Characters) )