I have created a GUI that shows data about videos from YouTube and allows me to select which video I want to download.
The code works well, but for some reason some of the images don't load and I can't select the video.
This is the result I get:
it is always the few first images that don't load. And, as I mentioned, I can't click on them at all.
This is my code:
from __future__ import annotations
import contextlib
import dataclasses
import datetime
import tkinter as tk
from io import BytesIO
from typing import Iterable, Any
import requests
from PIL import Image, ImageTk
@dataclasses.dataclass
class VideoData:
id: str
title: str
uploader: str
duration: int
thumbnail: str
def _initialize_root(root: tk.Tk):
root.title('Downloader')
# root.protocol('WM_DELETE_WINDOW', lambda: ...)
root.attributes('-topmost', True)
root.minsize(350, 200)
class Gui:
def __init__(self, videos: Iterable[VideoData]):
self._root = tk.Tk()
_initialize_root(self._root)
self._frame = tk.Frame(self._root)
self._frame.grid(row=0, column=0, sticky='news')
for row, video in enumerate(videos):
self._create_video_line(video, row)
self._frame.grid_columnconfigure('all', weight=1)
self._root.grid_columnconfigure('all', weight=1)
self.selected_row = -1
def start(self):
self._root.mainloop()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def close(self):
self._root.quit()
with contextlib.suppress(tk.TclError):
self._root.destroy()
def _on_click(self, row: int):
self.selected_row = row
self.close()
def _create_video_line(self, video_data: VideoData, row: int) -> None:
response = requests.get(video_data.thumbnail)
image_data = response.content
# Create a PhotoImage from the image data and resize it
print(f'Getting thumbnail image from {video_data.thumbnail!r}')
image = Image.open(BytesIO(image_data)).resize((200, 200))
# Convert to a PNG image
with BytesIO() as fp:
image.save(fp, 'PNG')
png_image = Image.open(fp)
thumbnail = ImageTk.PhotoImage(png_image)
// Create the button
button = tk.Button(self._frame, image=thumbnail, width=200, height=200, command=lambda: self._on_click(row))
button.grid(row=row, column=0, columnspan=1, sticky="news")
duration = datetime.timedelta(seconds=video_data.duration)
text_label = tk.Label(self._frame, text=f'{video_data.title} by {video_data.uploader} (duration: {duration})'
, anchor='w')
text_label.grid(row=row, column=1, columnspan=1, sticky="news")
How can I fix this issue? I don't mind to add a default image, but I can't find out if the image is loaded or not.
You need to save the reference of the image thumbnail
, otherwise the image will be garbage collected after exiting the function _create_video_line()
:
def _create_video_line(self, video_data: VideoData, row: int) -> None:
print(f'Getting thumbnail image from {video_data.thumbnail!r}')
response = requests.get(video_data.thumbnail)
image_data = response.content
# Create a PhotoImage from the image data and resize it
image = Image.open(BytesIO(image_data)).resize((200,200))
thumbnail = ImageTk.PhotoImage(image)
# Create the button
button = tk.Button(self._frame, image=thumbnail, command=lambda: self._on_click(row))
button.grid(row=row, column=0, columnspan=1, sticky="news")
# use an attribute of button to store the reference of image
# the name of the attribute can be any user-defined name that
# does not override the built-in attributes of button
button.thumbnail = thumbnail
duration = datetime.timedelta(seconds=video_data.duration)
text_label = tk.Label(self._frame, text=f'{video_data.title} by {video_data.uploader} (duration: {duration})'
, anchor='w')
text_label.grid(row=row, column=1, columnspan=1, sticky="news")