Which code does the mainloop() processes infinitely until any event occurs? - python

To understand my question kindly follow the paragraphs written below:
What code does the mainloop processes infinitely? Like does it read the code of the entire program again and again?
consider the code:
from tkinter import *
window = Tk()
print("lol")
print("Hello World")
window.mainloop()
the output didn't print "Hello World" or "lol" infinite number of times, so the mainloop() doesn't loop the code of the current module.
Now consider this code:
from tkinter import *
print("lol")
window = Tk()
print("Hello World")
while True:
window.update()
Now, even this code executes the same output, so now we can consider the mainloop() loops the code "window.update()" infite number of times, but more efficiently(somehow).
Now the first question arises what does the window.update() function do to update the values in the GUI, does it re-read the code from top to bottom again, or how does the update function update the GUI widget vaules.
The second question is :
I read this article
"Mainloop in Python Tkinter is an infinite loop of the application window which runs forever so that we can see the still screen.
The application window is like a frame that keeps on destroying every microsecond but the main loop keeps on creating a new updated window.
This process of destroying old window screens and creating a new one happens so fast that human eyes don’t even realize it.
Since the process runs infinite time that is why we are able to see the application in front of us and when we close the window then the loop terminates or exits."
Now if this is true then to recreate an updated window the root.mainloop() must read the entire root GUI code again and again entirely or is there another explanation to it.
I have been trying to understand this for the past 6hrs and I have visited every site and I cannot find the solution for the life of me.
Regards,
Rashik

What code does the mainloop processes infinitely? Like does it read the code of the entire program again and again?
No.
Via this function, it calls this C code which has the embedded Tcl interpreter process one event, or wait for Tkinter_busywaitinterval before trying to process another event
Now, even this code executes the same output, so now we can consider the mainloop() loops the code "window.update()" infite number of times, but more efficiently(somehow).
window.update() calls TCL update, which is described to
[...] bring the application “up to date” by entering the event loop repeatedly until all pending events (including idle callbacks) have been processed.
Your infinite loop doesn't have a sleep, so it's spinning your CPU as hard as possible to do practically nothing.
[...] Does it re-read the code from top to bottom again, or how does the update function update the GUI widget vaules.
It certainly doesn't re-read your code. It processes any pending widget updates, which may have happened by running e.g. window.text("...") in e.g. a click callback or an .after() timeout, etc.
I read this article [...]
That article seems wrong and/or at least over-simplifies things.
This simple example clock should clarify how things work:
import time
import tkinter as tk
root = tk.Tk()
text = tk.Label(root)
text.pack()
def tick():
text["text"] = time.ctime() # update `text` widget's content
root.after(1000, tick) # schedule for this function to be called after 1 second
if __name__ == '__main__':
tick() # call the `tick` function once before entering main loop
root.mainloop()

Related

Alternative To Time.Sleep() for pausing a function

I created an example code because my original is too big and has private information(My own) in it.
While running a program from a Tkinter GUI, it runs the program but makes the GUI unresponsive because of time.sleep() blocking the GUI from updating.
I am trying to avoid using timers because it fires a different function after a duration instead of simply pausing the function and then continuing the same function.
Is there an alternative that does not block the GUI but still adds a delay inside of the function?
Example Code:
from tkinter import *
import time
wn = Tk()
wn.geometry("400x300")
MyLabel = Label(wn, text="This is a Status Bar")
MyLabel.pack()
def MyFunction():
Value = 1
while Value < 10:
print("Do something")
time.sleep(1) **# - here blocks everything outside of the function**
MyLabel.config(text=Value)
# A lot more code is under here so I cannot use a timer that fires a new function
Value = 1
MyButton = Button(wn, text="Run Program", command=MyFunction)
MyButton.pack()
wn.mainloop()
Edit: Thanks so much, you're answers were fast and helpful, I changed the code and added "wn.mainloop()" after the delay and replaced "time.sleep(1)" with wn.after(100, wn.after(10, MyLabel.config(text=Value))
here is the final code:
from tkinter import *
import time
wn = Tk()
wn.geometry("400x300")
MyLabel = Label(wn, text="This is a Status Bar")
MyLabel.pack()
def MyFunction():
Value = 0
while Value < 10:
print("Do something")
wn.after(10, MyLabel.config(text=Value))
Value += 1
wn.mainloop()
MyButton = Button(wn, text="Run Program", command=MyFunction)
MyButton.pack()
wn.mainloop()
The short answer is that you can use wn.after() to request a callback after a certain amount of time. That's how you handle it. You get a timer tick at a one-per-second rate, and you have enough state information to let you proceed to the next state, then you go back to the main loop.
Put another way, timers are exactly how you have to solve this problem.
Fundamentally, any callback function in Tkinter runs in the main GUI thread, and so the GUI thread will block until the function exits. Thus you cannot add a delay inside the function without causing the GUI thread to be delayed.
There are two ways to solve this. One would be to refactor your function into multiple pieces so that it can schedule the remaining work (in a separate function) via .after. This has the advantage of ensuring that all of your functions are running in the main thread, so you can perform GUI operations directly.
The other way is to run your function in a separate thread that is kicked off whenever your main callback is executed. This lets you keep all the logic inside the one function, but it can no longer perform GUI operations directly - instead, any GUI operations would have to go through an event queue that you manage from the main thread.
You can combine after() and wait_variable() to simulate time.sleep() without blocking tkinter from handling pending events and updates:
def tk_sleep(delay):
v = wn.IntVar()
# update variable "delay" ms later
wn.after(delay, v.set, 0)
# wait for update of variable
wn.wait_variable(v)
Using tk_sleep() in your while loop:
def MyFunction():
Value = 1
while Value < 10:
print("Do something")
tk_sleep(1000) # waits for one second
MyLabel.config(text=Value)
# A lot more code is under here so I cannot use a timer that fires a new function
Value += 1

How to stop a thread in python?

I am a beginner in Python.
I use to code with Tkinter. I want to improve my programs with the extension "threading".
However, in some of my programs, if i leave the Tkinter window, the program is still running.
I would like to know how stop it. I guess it's the thread which is still running. I tried different ways to close the thread, but the only issue i found is to create an error (and it is not a nice solution according to me) :).
I would like to know if an instruction like the following one exist to stop the thread.
Thread(target=FinTemps).stop()
(I know that 'stop' is not working)
and if it is possible to stop the Thread with a loop 'while', I tried this and i don't understand why it doesn't work :
import tkinter as tk
from threading import Thread as Th
from time import *
global end
end=False
begining=time()
def screen():
global end
while end==False:
chrono=time()-beginning
if chrono<5:
sentence.set("Hey !")
elif chrono<8:
sentence.set("What's up ?")
window=tk.Tk()
window.geometry("{}x{}".format(fenêtre.winfo_screenwidth(), window.winfo_screenheight()))
window.title("Test Tkinter & Thread")
sentence=tk.StringVar()
sentence.set("none")
GreatTitle=tk.Label(window, textvariable=sentence, width=500)
Th(target=screen).start()
GreatTitle.pack(expand=1)
window.mainloop()
end=True
Thank you ;)
(And sorry if my english is not wonderful (I'm french) or if my code or explainations weren't very understandable or without following traditional presentation).
Have a good day :)
What you are doing is not safe. tkinter is not thread-safe, and running any tkinter API cross-thread risks corruption of various kinds.
Bright side is, you don't need threads here. In real code, you wouldn't use a thread at all, you'd just schedule the changes to occur on tkinter's event loop with the after method, simplifying your code to just:
import tkinter as tk
window=tk.Tk()
window.geometry("{}x{}".format(window.winfo_screenwidth(), window.winfo_screenheight()))
window.title("Test Tkinter & Thread")
sentence=tk.StringVar()
sentence.set("none")
GreatTitle=tk.Label(window, textvariable=sentence, width=500)
GreatTitle.pack(expand=1)
window.after(0, sentence.set, "Hey !") # Happens almost immediately, as in thread case
window.after(5000, sentence.set, "What's up ?") # Delays five seconds, as in thread case
window.mainloop()
This is also far more efficient than what you had; as written, your thread would spin forever, constantly looping without pause for as long as it was allowed to do so, keeping a core constantly running at 100%; the GUI itself would run slower because it couldn't do much of its work while the other thread held the GIL. Using .after, no meaningful resources are consumed at any time aside from the moment the event fires and the sentence is updated.
It may be that your while loop checks end too frequent, it may cause race condition when you try to set end to True after the root window is destroyed.
You can try adding a short sleep inside the while loop to reduce the chance of race condition:
while end == False:
...
sleep(0.1)

Problems with update and update_idletasks

I've been learning python for a month now and run into my first brick wall. I have a large art viewer GUI program and at one point want to put an image on screen with a countdown counter-approx every 5 secs. I thought of a code such as the one below The problem is that this uses update and all my reading says that update is bad (starts a new event loop (?)) and that I should use update_idletasks. when I replace update with update_idletasks in the code below the countdown button is not visible until it reaches single figures, update superficially works fine. But also the q bound key calls the subroutine but has no effect
from tkinter import *
import sys
import time
root = Tk()
def q_key(event):
sys.exit()
frame=Frame(root, padx=100, pady=100, bd=10, relief=FLAT)
frame.pack()
button=Button(frame,relief="flat",bg="grey",fg="white",font="-size 18",text="60")
button.pack()
root.bind("q",q_key)
for x in range(30, -1, -5) :
button.configure(text=str(x))
button.update()
print(x)
button.after(5000)
root.mainloop()
In this case you don't need update nor update_idletasks. You also don't need the loop, because tkinter is already running in a loop: mainloop.
Instead, move the body of the loop to a function, and call the function via after. What happens is that you do whatever work you want to do, and then schedule your function to run again after a delay. Since your function exits, tkinter returns to the event loop and is able to process events as normal. When the delay is up, tkinter calls your function and the whole process starts over again.
It looks something like this:
def show(x):
button.configure(text=x)
if x > 0:
button.after(5000, show, x-5)
show(30)

Why does my progress bar work with "print" command and not with tkinter?

I would like to understand why this code:
import time
for i in range(1,11):
print(i)
time.sleep(1)
shows (as it should!) numbers from 1 to 10, each every 1 second, while this code:
from tkinter import *
import time
root = Tk()
for i in range(1,11):
Label(root, text = i).grid(row=0, column=i-1, padx=5, pady =5)
time.sleep(1)
root.mainloop()
waits for 10 seconds, and then displays a window with the 10 numbers (instead of adding them one by one).
I am aware this is a silly question, but I really can't understand! Many Thanks! Alessandro
Most GUI's work differently to what you expect.
They work in an asynchronous way, which means, that you setup your windows and start an event loop.
This event loop will display all widgets, labels, etc, that you set up before calling the event loop and wait for any events (GUI events like mouse or keyboard events, timer events and perhaps network events).
When any event is encountered code associated to that event will be called and this code can request to change the GUI (show or hide elements, change labels or attributes of graphical widgets) However the change to the GUI will only be performed when you give control back to the event loop (when the code handling an event finished)
In your given code you change a label in a for loop with sleep statements, but only after the for loop is finished your main loop is being called and this is the moment, where the final state of your GUI will be displayed.
So what you encounter is a know issue for almost all GUI / asynhronous kind of applications.
You have to rewrite your code such, that you start a timer event, and when the timer event fires a function will set a label and increase the counter by 1. And if the counter is not 11 it will restart another timer
This is because the time.sleep function is before the root.mainloop function.
root.mainloop is what causes the window to appear on-screen and start doing things. Instead, I'd recommend using window.after, as that tells the window to run a function after some time when it's on-screen.
Here's an example of a modification you could make (it's not that good but it works):
from tkinter import *
import time
root = Tk()
progress = 0
end = 10
def update_progress():
global progress
progress += 1
Label(root, text = progress).grid(row=0, column=progress-1, padx=5, pady =5)
if progress < end: root.after(1000,update_progress) # Tell the window to call this function in 1000ms (1 second)
root.after(0,update_progress) # Tell the window to run the update_progress function 0ms after now.
root.mainloop()
I'd recommend looking at gelonida's answer for an explanation of why your original code didn't work, and what you need to keep in mind when programming with GUIs in the future.

Killing a multiprocess pool using a tkinter button

I'm building an application that does thousands (possibly millions) of calculations based off of what the user inputs. Because these calculations can take a long time, I want to use Python's multiprocessing module. Here is my dilemma; I want to set up a tkinter window with a cancel button to stop the calculations running throughout the processors I set up using Pool. I tried using threads to allow the popup window to run, but some funny things happen when I run the code.
When I press the 'start' button, the Pool starts going as expected. However, my popup window does not show even though it is on its own thread. Even weirder, when I tell my IDE (PyCharm) to stop the program, the popup window shows and the calculations are still running until I either press the 'exit' button from the popup window, or kill the program altogether.
So my questions are: Why won't my popup window show even though it is on its own thread? Am I utilizing the multiprocessing module correctly? Or is the problem something totally different?
import tkinter as tk
from tkinter import ttk
import random
import threading
import time
from multiprocessing import Pool
def f(x):
print(x*x)
time.sleep(random.randrange(1,5)) # simulates long calculations
# Extra math stuff here...
def processor():
global calc
calc = Pool(4)
calc.map(f, [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30])
print("Done")
def calculation_start():
p = threading.Thread(target=progress_window) # Thread to open a new window for the user to cancel a calculation
p.start()
processor() # Calls the function to run the calculations
def progress_window():
# This is the window that pops up to let a user cancel a calculation.
popup = tk.Toplevel()
endButton = ttk.Button(popup, text="End", command=lambda :(calc.terminate()))
endButton.pack()
master = tk.Tk()
startButton = ttk.Button(master, text="Start", command=calculation_start)
startButton.pack()
tk.mainloop()
EDIT:
I tried switching the processor function to a thread instead of the progress_window function.
def calculation_start():
p = threading.Thread(target=processor) # Thread for the function to run the calculations
p.start()
progress_window() # Open a new window for the user to cancel a calculation
My popup window appears and the 'end' button looks like it stops the processor when pressed, but it never continues past that point. It's like it's stuck at calc.map(f, [1,2,3,...] in the processor() function.
From the Pool.map documentation:
It blocks until the result is ready.
That means that, yes, the work is being done on other threads, but the main thread (the one calling map) will not execute anything else until the other threads are complete. You want to use map_async and (for a long-running, interactive process), provide a callback for what to do when the pool is finished (probably hide the pop-up).
If you want your program to simply show the pop-up then exit, you want to use the AsyncResult returned by map_async. Set up an idle handler using after_idle (See tkinter universal widget methods and remember you'll need to manually re-add the handler after every call). After each idle call, you could call either AsyncResult.ready() or AsyncResult.get(<timeout>) to see if the pool has completed. Once it's finished, you're safe to process the results and exit.

Categories