I am building an app using tkinter and pytube, but I'm struggling with a feature: the progress bar.
The problem is, when I am using tkinter, because of the mainloop function, I can't use things like while, time.sleep... When the pytube function "download" is executed, the app freezes until the download is complete. For this reason, the progress bar doesn't load, just jumps to 100% complete when the app unfreezes at the end of download.
I've tried a threading function, to try to run the download function at the same time as the screen main loop updates. I've tried using the after function on tkinter, or some alternatives for mainloop. I also tried to run the download on another tk screen. Nothing works, the code is still running during the download, the progress is printed by a function in terminal, but as the tk screen freezes, I can't show it on screen, just in terminal.
How can I fix it?
ps: this is not the final version of this code, so it is not yet in OOP, but it is possible to understand
import sys
import threading
import time
from tkinter import *
from tkinter import filedialog, ttk, messagebox
from PIL import ImageTk, Image
from urllib.request import urlopen
from PIL.Image import Resampling
from pytube import YouTube
from io import BytesIO
import pyperclip
download_path = None
# -------------------- COMMANDS -------------------- #
def get_thumnail(url):
image_url = urlopen(url)
image_raw = image_url.read()
image_url.close()
image = Image.open(BytesIO(image_raw))
thumbnail = image.resize((300, 200), resample=Resampling.LANCZOS)
image_tk = ImageTk.PhotoImage(thumbnail)
return image_tk
def paste_url():
clipboard = pyperclip.paste()
video_url_entry.delete(0, END)
video_url_entry.insert(0, clipboard)
def select_path():
global download_path
download_path = filedialog.askdirectory()
destination_path_label_entry.config(text=download_path)
def show_progress_bar(s, chunk, bytes_remaining):
download_percent = int((s.filesize - bytes_remaining) / s.filesize * 100)
progressbar.after(200, progressbar.config(value=download_percent))
#progressbar.config(value=download_percent)
root.title(f'\rProgress: {download_percent} %')
print(f'\rProgress: {download_percent} %')
#threading.Thread(target=after).start()
def complete(s, file_path):
print(f'Download complete: {file_path}')
def download_video():
link = video_url_entry.get()
path = download_path
yt = YouTube(link)
threading.Thread(target=yt.register_on_progress_callback(show_progress_bar)).start()
new_icon = get_thumnail(yt.thumbnail_url)
main_image.configure(image=new_icon)
main_image.image = new_icon
is_ok = messagebox.askokcancel(title=yt.title,
message=f'Title: {yt.title}\n'
f'Length: {yt.length//60}m{yt.length&60}s\n'
f'Views: {yt.views}\n'
f'Author: {yt.author}')
if is_ok:
resolution = current_var.get()
if resolution == 'mp3':
video_stream = yt.streams.get_audio_only()
elif resolution == 'Highest':
video_stream = yt.streams.get_highest_resolution()
else:
video_stream = yt.streams.get_by_resolution(resolution)
print('start')
#threading.Thread(target=video_stream.download(path, filename_prefix=f'({resolution}) ')).start()
video_stream.download(path, filename_prefix=f'({resolution}) ')
print('done')
else:
main_image.configure(image=icon)
main_image.image = icon
# Screen settings
root = Tk()
icon = PhotoImage(file='logo.png')
root.iconphoto(False, icon)
root.title('Youtube Video Downloader')
root.config(padx=50, pady=50)
# Main Canvas
main_image = Label(root, image=icon, width=350, height=200)
main_image.grid(row=1, column=1, columnspan=3)
# Labels
video_url_label = Label(text='Video URL:')
video_url_label.grid(row=2, column=1)
done_label = Label(text='', width=47, fg='green')
done_label.grid(row=6, column=2, columnspan=2)
destination_path_label_entry = Label(width=40, bg='white')
destination_path_label_entry.grid(row=3, column=1, columnspan=2)
# Entry's
video_url_entry = Entry(width=35)
video_url_entry.focus()
video_url_entry.grid(row=2, column=2)
# Buttons
search_button = Button(text='Paste', width=15, command=paste_url)
search_button.grid(row=2, column=3)
destination_path_button = Button(text='Select Path', width=15, command=select_path)
destination_path_button.grid(row=3, column=3)
current_var = StringVar(value='Resolution')
combobox = ttk.Combobox(textvariable=current_var, state='readonly', values=('mp3', '144p', '240p', '360p', '480p', '720p', '1080p', 'Highest'), width=9)
combobox.grid(row=4, column=1)
download_button = Button(text='Download as MP4', width=46, command=download_video)
download_button.grid(row=4, column=2, columnspan=2)
# Progress bar
progressbar = ttk.Progressbar(root, orient=HORIZONTAL, length=410, mode='determinate')
progressbar.grid(row=5, column=1, columnspan=3)
root.mainloop()
Problem of your solution is, that you're calling video_stream.download from within download_video function.
Once video_stream.download is called, it is blocking GUI, so nothing got updated, until video download is finished. You had commented line in your code threading.Thread(target=video_stream.download(path, filename_prefix=f'({resolution}) ')).start(), so you were trying to call download in the Thread, but that would yield the same result as target=video_stream.download(path, filename_prefix=f'({resolution}) ') would first download the video and only then start the Thread itself. You don't want that.
Have a look at Thread documentation, where you must use callable object (function or method) as a target without any arguments. Those must be passed to the Thread by using args and kwargs.
The rest of your code is fine and you were properly using .after as well to avoid calling GUI from different Thread, which you should never do.
Here is your modified code, now properly calling download Thread and updating progress bar as well:
import threading
from io import BytesIO
from tkinter import *
from tkinter import filedialog, ttk, messagebox
from urllib.request import urlopen
import pyperclip
from PIL import ImageTk, Image
from PIL.Image import Resampling
from pytube import YouTube
download_path = None
# -------------------- COMMANDS -------------------- #
def get_thumnail(url):
image_url = urlopen(url)
image_raw = image_url.read()
image_url.close()
image = Image.open(BytesIO(image_raw))
thumbnail = image.resize((300, 200), resample=Resampling.LANCZOS)
image_tk = ImageTk.PhotoImage(thumbnail)
return image_tk
def paste_url():
clipboard = pyperclip.paste()
video_url_entry.delete(0, END)
video_url_entry.insert(0, clipboard)
def select_path():
global download_path
download_path = filedialog.askdirectory()
destination_path_label_entry.config(text=download_path)
def show_progress_bar(s, chunk, bytes_remaining):
download_percent = int((s.filesize - bytes_remaining) / s.filesize * 100)
root.after(0, update_gui(download_percent))
print(f'\rProgress: {download_percent} %')
def update_gui(download_percent):
progressbar.config(value=download_percent)
root.title(f'\rProgress: {download_percent} %')
def complete(s, file_path):
print(f'Download complete: {file_path}')
def download_video():
link = video_url_entry.get()
path = download_path
yt = YouTube(link)
# Don't put callback into Thread
yt.register_on_progress_callback(show_progress_bar)
yt.register_on_complete_callback(complete)
new_icon = get_thumnail(yt.thumbnail_url)
main_image.configure(image=new_icon)
main_image.image = new_icon
is_ok = messagebox.askokcancel(title=yt.title,
message=f'Title: {yt.title}\n'
f'Length: {yt.length // 60}m{yt.length & 60}s\n'
f'Views: {yt.views}\n'
f'Author: {yt.author}')
if is_ok:
resolution = current_var.get()
if resolution == 'mp3':
video_stream = yt.streams.get_audio_only()
elif resolution == 'Highest':
video_stream = yt.streams.get_highest_resolution()
else:
video_stream = yt.streams.get_by_resolution(resolution)
# You cant just call video_stream.download() with params, because it will execute and block outside Thread
threading.Thread(target=video_stream.download,
kwargs={"output_path": path, "filename_prefix": f'({resolution}) '}).start()
else:
main_image.configure(image=icon)
main_image.image = icon
# Screen settings
root = Tk()
icon = PhotoImage(file='logo.png')
root.iconphoto(False, icon)
root.title('Youtube Video Downloader')
root.config(padx=50, pady=50)
# Main Canvas
main_image = Label(root, image=icon, width=350, height=200)
main_image.grid(row=1, column=1, columnspan=3)
# Labels
video_url_label = Label(text='Video URL:')
video_url_label.grid(row=2, column=1)
done_label = Label(text='', width=47, fg='green')
done_label.grid(row=6, column=2, columnspan=2)
destination_path_label_entry = Label(width=40, bg='white')
destination_path_label_entry.grid(row=3, column=1, columnspan=2)
# Entry's
video_url_entry = Entry(width=35)
video_url_entry.focus()
video_url_entry.grid(row=2, column=2)
# Buttons
search_button = Button(text='Paste', width=15, command=paste_url)
search_button.grid(row=2, column=3)
destination_path_button = Button(text='Select Path', width=15, command=select_path)
destination_path_button.grid(row=3, column=3)
current_var = StringVar(value='Resolution')
combobox = ttk.Combobox(textvariable=current_var, state='readonly',
values=('mp3', '144p', '240p', '360p', '480p', '720p', '1080p', 'Highest'), width=9)
combobox.grid(row=4, column=1)
download_button = Button(text='Download as MP4', width=46, command=download_video)
download_button.grid(row=4, column=2, columnspan=2)
# Progress bar
progressbar = ttk.Progressbar(root, orient=HORIZONTAL, length=410, mode='determinate')
progressbar.grid(row=5, column=1, columnspan=3)
root.mainloop()
I am trying to create a program then when started, shows a gif and a button that says: "Turn off system". When clicked, I want the gif to change to another gif and the button to be replaced with a button that says "Turn on system".
I made exactly this but when I click the button, the first GIF remains on the screen. You can however see the second GIF popping in and out when clicking rapidly.
EDIT* Due to a formatting issue I had to redefine my code, the def main(): you see in my code should not exist.
Here is my code:
import tkinter as tk
from PIL import Image, ImageTk
from tkinter import *
from tkinter import ttk
root = tk.Tk()
root.title('Camera System')
def update(ind):
frame = stgImg[ind]
ind += 1
if ind == frameCnt:
ind = 0
label.configure(image=frame)
root.after(100, update, ind)
def SystemOn():
stgImg = PhotoImage(file=(filename1.gif))
label.configure(image=stgImg)
label.image = stgImg
global b3
b3=tk.Button(root,text="Turn Off System",command=lambda:SystemOff())
b3.pack()
b1.pack_forget()
b2.pack_forget()
def SystemOff():
stgImg = PhotoImage(file=(filename2.gif))
label.configure(image=stgImg)
label.image = stgImg
global b2
b2=tk.Button(root,text="Turn On System",command=lambda:SystemOn())
b2.pack()
b1.pack_forget()
b3.pack_forget()
def main():
frameCnt = 12
root.geometry('1010x740+200+200')
stgImg = [PhotoImage(file=(filename1.gif),
format = 'gif -index %i' %(i)) for i in range(frameCnt)]
label=ttk.Label(root)
label.pack()
root.after(0, update, 0)
global b1
b1=tk.Button(root,text="Turn Off System",command=lambda:SystemOff())
b1.pack()
root.mainloop()
Here is a small program that achieves the affects described.
I've set up filedialog to load the two images and defined a button with message.
Two functions work together to replace the image and text.
import tkinter as tk
from tkinter import filedialog as fido
master = tk.Tk()
imageon = fido.askopenfilename(title ="Pick a Picture")
on = tk.PhotoImage(file = imageon)
imageoff = fido.askopenfilename(title ="Pick a Picture")
off = tk.PhotoImage(file = imageoff)
def turnOn():
button.config(image = on, text = "Turn on system", command = turnOff)
def turnOff():
button.config(image = off, text = "Turn off system", command = turnOn)
button = tk.Button(master, image = off, compound = "top")
button.grid(sticky = tk.NSEW)
turnOff()
master.mainloop()
Im making this game called:
IDLE PEN ,(MAKE PENS)
and every 1 second i get a bonus pen
how i get a bonus pen is doing this
Import time
While true
make a pen()
time.sleep(1)
but i have some code under the while true loop.
the code under the while true loop is like buttons to
upgrade the pens or make a pen
So how do i make the code under the while true loop work?
This is my game im happy for anyone to copy it
its not ready yet
import functools
import tkinter
import tkinter.messagebox
import time
from random import seed
from random import randint
# eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
window = tkinter.Tk()
window.title('Idle Pen')
def print_pen(number: int):
return f"Pens: {number}"
class pencount:
def __init__(self):
self.pencount = 0
self.text = tkinter.Text(height=1, width=30)
self.text.insert("1.0", print_pen(0))
self.text['state'] = 'disabled'
self.text.pack()
def changepencount(self, count):
if self.pencount + count < 0:
return
self.pencount = self.pencount + count
self.text['state'] = 'normal'
self.text.delete("1.0", "end")
self.text.insert("1.0", print_pen(self.pencount))
self.text['state'] = 'disabled'
self.text.pack()
pen = pencount()
changepenup = functools.partial(pen.changepencount, 1)
B = tkinter.Button(window, text="Make Pen", command=changepenup)
changependown = functools.partial(pen.changepencount, -100)
A = tkinter.Button(window, text='Penmaker', command=changependown)
Q = tkinter.Button(window, text="Quit", command=window.destroy)
U = tkinter.Button
# eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
B.pack()
A.pack()
Q.pack()
window.mainloop()
You could use threading to run your loop in separated thread and then main thread may run tkitner
OR you can use tkinter function after() (instead of while True) to run function with delay and this function should use again after() to run itself.
import tkinter as tk
# --- functions ---
def update():
global value
value += 1
text = f'make penny: {value}'
print(text)
label['text'] = text
# run again after 1s (1000ms)
root.after(1000, update)
# --- main ---
value = 0
root = tk.Tk()
label = tk.Label(root, text="make penny: 0")
label.pack()
button = tk.Button(root, text="Exit", command=root.destroy)
button.pack()
# run first time after 1s (1000ms)
root.after(1000, update)
root.mainloop()
I have a large code where a button press is supposed to run a code that will take roughly 15 seconds to complete. Within this time I want to display a label that says "Processing, please wait" or something of that sort. However in python, the whole GUI created using tkinter will freeze and unfreeze once the procedure is over. How do I get around to doing this? I created a smaller code so that I can explain easier.
from tkinter import *
from threading import Thread
import os
import sys
import time
master = Tk()
master.geometry("500x500")
master.resizable(False,False)
def tryout():
sign2.config(text = "AAA")
for x in range(5):
print(x)
time.sleep(1)
sign2.config(text = "BBB")
for x in range(5):
print(x)
time.sleep(1)
sign2.config(text = "CCC")
def close_window():
master.destroy()
sys.exit()
sign1 = Label(master, text = "VNA GUI").grid(pady=10, padx=10)
sign2 = Label(master, text = "Choose option to continue")
sign2.grid(pady=10, padx=10, ipadx=50)
Button(master, text='Exit', command=close_window).grid(pady=10, padx=20)
butTest = Button(master, text='test', command=tryout)
butTest.grid(pady=10, padx=20)
master.mainloop( )
So in this code I expect to see 'AAA' on the label first, followed by 'BBB' at the middle of the count from 0 to 4, and then 'CCC' at the end of the final count from 0 to 4. What happens here is the GUI freezes at the beginning, the count carries on and I just see 'CCC'. How do I get around this?
There are only a few changes necessary to do that with threading.
First create a function start_tryout:
def start_tryout():
Thread(target=tryout, daemon=True).start() # deamon=True is important so that you can close the program correctly
Then create the button with the new command:
butTest = Button(master, text='test', command=start_tryout)
Then it should no longer freeze the gui and you should be able to see the label change.
You can try threading. I've made changes below to the code and tested it here, and it worked.
from tkinter import *
from threading import Thread
import os
import sys
import time
import threading # NEW
master = Tk()
master.geometry("500x500")
master.resizable(False,False)
def tryout():
sign2.config(text = "AAA")
for x in range(5):
print(x)
time.sleep(1)
sign2.config(text = "BBB")
for x in range(5):
print(x)
time.sleep(1)
sign2.config(text = "CCC")
def close_window():
master.destroy()
sys.exit()
def thread(): # NEW
threading.Thread(target=tryout).start() # NEW
sign1 = Label(master, text = "VNA GUI").grid(pady=10, padx=10)
sign2 = Label(master, text = "Choose option to continue")
sign2.grid(pady=10, padx=10, ipadx=50)
Button(master, text='Exit', command=close_window).grid(pady=10, padx=20)
butTest = Button(master, text='test', command=thread) # Changed
butTest.grid(pady=10, padx=20)
master.mainloop( )
I am trying to end the process of multi-scripts running on tkinter by a click of the end process created button. However, this does not seem to work when any of the scripts are running in an infinite loop to stop the process.
Here is my code:
import os
import sys
from tkinter import *
import tkinter.messagebox as tkmsg
root = Tk(className=" x") # creates root window
# all components of thw window will come here
Label(root,text='Enter Load Value or Choose a test action',font=("Helvetica", 16), width=40, height=2).pack(side=TOP)
svalue = StringVar() # defines the widget state as string
w = Entry(root,textvariable=svalue).pack(side=LEFT,padx=10) # adds a textarea widget
def act():
print ("Value entered")
print ('%s' % svalue.get())
def script_1():
os.system('python script1_1.py')
def script_2():
os.system('python script1_2.py')
global root
while 1:
root.update()
pass
def script_3():
os.system('python script1_3.py')
def script_4():
os.system('python script1_4.py')
def script_5():
os.system('python script1_5.py')
def quit():
tkmsg.showinfo("Stopped", "Stopped")
sys.exit
if __name__ == '__main__':
global root
root = Frame ()
root.pack ()
button0 = Button(root,text="Load", command=act).pack(side=LEFT,padx=10)
button1 = Button(root,text="Test 1",command=script_1).pack(side=LEFT,padx=10)
button2 = Button(root,text="Test 2",command=script_2).pack(side=LEFT,padx=10)
button3 = Button(root,text="Test 3",command=script_3).pack(side=LEFT,padx=10)
button4 = Button(root,text="Test 4",command=script_4).pack(side=LEFT,padx=10)
button5 = Button(root,text="Test 5",command=script_5).pack(side=LEFT,padx=10)
button6 = Button(root,fg="red",text="End process",command=quit).pack(side=LEFT,padx=10)
button.mainloop() # an event loop to invoke custom function "act"
root.mainloop() # To keep GUI window running