I'm trying to make a windows style image viewer that can display multiple images at once in an icon or thumbnail format (like when you do View > Layout > Large Icons on windows) in tkinter. However, the folder I want to open contains a couple of hundred images and it's way to slow to load them all and will probably use too much memory anyway. Therefore I'm trying to lazy load images based on if they are visible (i.e: if the user has scrolled down enough to see it). Here's what I have so far:
from tkinter import *
from tkinter import filedialog
from PIL import Image
from PIL import ImageTk
import glob
import datetime
import os
class ImageListViewer(Frame):
def __init__(self, root, img_paths, *args, **kwargs):
self.img_paths = img_paths
self.root = root
self.canvas = Canvas(root)
self.scroll_y = Scrollbar(root, orient="vertical", command=self.canvas.yview)
self.frame = Frame(self.canvas, bg='red')
self.max_w, self.max_h = 200, 200
cols = 3
loading_img = Image.open('loading.png')
loading_img = self.fit_img(loading_img, self.max_w, self.max_h)
loading_photo = ImageTk.PhotoImage(loading_img)
# group of widgets
for i, path in enumerate(img_paths):
label = Label(self.frame, image=loading_photo, width=self.max_w, height=self.max_h, borderwidth=2, relief="sunken")
label.photo = loading_photo
# label = Label(self.frame, text=str(i), width=self.max_w, height=self.max_h, borderwidth=2, relief="sunken")
label.index = i
label.has_img = False
label.grid(row=i-i%cols, column=i%cols)
self.canvas.create_window(0, 0, anchor='nw', window=self.frame)
self.canvas.update_idletasks()
self.canvas.configure(scrollregion=self.canvas.bbox('all'),
yscrollcommand=self.scroll_intercepter)
self.canvas.pack(fill='both', expand=True, side='left')
self.scroll_y.pack(fill='y', side='right')
self.root.bind('h', self.debug)
super().__init__(root)
def scroll_intercepter(self, a, b):
for label in self.labels_in_view():
if not label.has_img:
self.lazy_load_img(label)
self.scroll_y.set(a, b)
def lazy_load_img(self, label):
img = Image.open(self.img_paths[label.index])
img = self.fit_img(img, self.max_w, self.max_h)
photo = ImageTk.PhotoImage(img)
label.config(image = photo)
label.photo = photo
# label.config(bg = 'red')
label.has_img = True
def labels_in_view(self, show=False):
if show:
print('Root:', self.root.winfo_width(), self.root.winfo_height())
for label in self.frame.winfo_children():
label_x = label.winfo_rootx() #- label.winfo_width()
label_y = label.winfo_rooty() #- label.winfo_height()
if show:
print(label.index, label_x, label_y)
x_in = 0 <= label_x < self.root.winfo_width()
y_in = 0 <= label_y < self.root.winfo_height()
if x_in and y_in:
yield label
def debug(self, event):
print([l.index for l in self.labels_in_view(show=True)], '\n')
#staticmethod
def fit_img(img, width, height, keep_aspect=True):
if keep_aspect:
w, h = img.size
ratio = min(width/w, height/h)
image = img.resize((int(w*ratio), int(h*ratio)))
else:
image = img.resize((width, height))
return image
if __name__ == "__main__":
root = Tk()
root.title("Image List Viewer")
root.geometry('400x400')
paths = glob.glob('*.JPG')
ImageListViewer(root, paths).pack()
root.mainloop()
The labels_in_view method tries to only return labels (which contain the images) that are displayed. Each label is originally loaded with a dummy 'loading' image. You can also see I've binded 'h' to display some extra debugging info.
Here's the problem, past tens of images it completely hangs. If one were to scroll to the end of all the images, it tries to load all the intermediate images from top to bottom and not only the ones that are viewable.
This is certainly because of how I intercept the scrollbar call back to then lazy load but I cannot figure out any other way of doing it. Is there a better way? Maybe a better library too, tkinter is rather slow although I'd rather not restart from scratch?
Any help is greatly appreciated! Thanks.
EDIT: Images should only be loaded once because of the label.has_img flag that is set to true once the image has been loaded.
Related
I'm trying to put a background image in my tkinter project, but despite of several attempts it's simply not showing up.
Here is the code:
import tkinter
from PIL import ImageTk,Image
show_screen = tkinter.Tk()
show_screen.geometry('900x900')
show_screen.title("LEARNTECH OPE")
img_show = Image.open("C:\PYTHON IDE\RemoteProctoring_Featured.png")
show_image = ImageTk.PhotoImage(img_show)
show_label = tkinter.Label(show_screen,font=("Arial Bold",10),fg="blue",
text="FILL THE NECESSARY DETAILS GIVEN BELOW")
show_label.place(x=600,y=0)
enter_field = tkinter.Entry(show_screen,width=50)
enter_field.place(x=600,y=200)
def clicked():
ref = "Welcome" + enter_field.get()
show_label.configure(text=ref)
show_button = tkinter.Button(show_screen,text="CLICK TO EXIT",
fg="green",command=clicked).place(x=600,y=400)
show_screen.mainloop()
As I said in a comment, images can only be displayed as part of some tkinter widget. Below is an example of doing that by putting the image in a Label. I also added code to resize the image to fill the window (aka "screen" in your code).
import tkinter
from PIL import ImageTk, Image
WIDTH, HEIGHT = 900, 900
IMG_PATH = r"C:\PYTHON IDE\RemoteProctoring_Featured.png" # Note 'r' prefix.
show_screen = tkinter.Tk()
show_screen.geometry('{}x{}'.format(WIDTH, HEIGHT))
show_screen.title("LEARNTECH OPE")
# Place background image on a Label widget.
tmp_img = Image.open(IMG_PATH).resize((WIDTH, HEIGHT), Image.ANTIALIAS)
bkg_img = ImageTk.PhotoImage(tmp_img)
bkg_label = tkinter.Label(show_screen, image=bkg_img)
bkg_label.img = bkg_img # Keep a reference in case this code put is in a function.
bkg_label.place(relx=0.5, rely=0.5, anchor='center') # Place in center of window.
enter_field = tkinter.Entry(show_screen,width=50)
enter_field.place(x=600,y=200)
show_label = tkinter.Label(show_screen,font=("Arial Bold",10),fg="blue",
text="FILL THE NECESSARY DETAILS GIVEN BELOW")
show_label.place(x=600,y=0)
def clicked():
ref = "Welcome " + enter_field.get()
show_label.configure(text=ref)
show_button = tkinter.Button(show_screen,text="CLICK TO EXIT",
fg="green",command=clicked)
show_button.place(x=600,y=400)
show_screen.mainloop()
This question already has answers here:
Why does Tkinter image not show up if created in a function?
(5 answers)
Closed 2 years ago.
I am trying to make this UI from a stimulus as part of an assessment for school. I tried to import the provided school logo and banner on the top frame of the page and put images on the canvas but have yet to achieve any results. When I run the code, the pictures won't load at all. The code that I was working with is as followed:
from tkinter import *
import random
import time
import sqlite3
from tkinter import simpledialog
from tkinter import messagebox
from tkcalendar import *
from tkinter import ttk
import math
from PIL import Image, ImageTk
import winsound
#-------------Frames setup--------------------------
class VendingApp(Tk):
def __init__(self):
Tk.__init__(self)
self._frame = None
self.switch_frame(Home)
def switch_frame(self, frame_class):
#Destroys current frame and replaces it with a new one.
new_frame = frame_class(self)
if self._frame is not None:
self._frame.destroy()
self._frame = new_frame
self._frame.pack()
####-----------------------Home page---------------------------
class Home(Frame):
def __init__(self, master):
Frame.__init__(self, master)
topFrame = Frame(self,width = 1024, height = 100, bd = 2, bg = "black")
topFrame.pack()
canvas_for_logo = Canvas(topFrame, height=100, width=100, bg = 'green') ##logo image
canvas_for_logo.grid(row=0, column=0, sticky='ne')
img_logo = Image.open("pic/sitelogo.png")
img_logo = img_logo.resize((40,40), Image.ANTIALIAS)
logo = ImageTk.PhotoImage(img_logo)
canvas_for_logo.create_image(0, 0, anchor=NW, image=logo)
canvas_for_banner = Canvas(topFrame, bg='red', height=100, width=924) #banner image
canvas_for_banner.grid(row=0, column=1, sticky='nw')
img_banner = Image.open("pic/banner.jpg")
img_banner = img_banner.resize((40,40), Image.ANTIALIAS)
banner = ImageTk.PhotoImage(img_banner)
canvas_for_banner.create_image(0, 0, anchor=NW, image=banner)
MidFrame = Frame(self,width = 1024, height = 628, bd = 2)
MidFrame.pack()
MidFrame.grid_propagate(False)
BottomFrame = Frame(self,width = 1024, height = 50, bd = 2, bg = "black")
BottomFrame.pack()
BottomFrame.grid_propagate(False)
if __name__ == "__main__":
root = VendingApp()
#Sets the size of the window
root.geometry("1024x768")
#Renames the TITLE of the window
root.title("Vending machine")
root.geometry("1024x768")
root.resizable(False, False)
root.mainloop()
I decided to make a separate file to test if the image would load without class, and it did. Codes are as followed:
from tkinter import ttk
from tkinter import*
import time
from PIL import Image, ImageTk
root = Tk()
canvas_for_logo = Canvas(root, height=100, width=100)
canvas_for_logo.pack()
img = Image.open("pic/sitelogo.png")
img = img.resize((105,105), Image.ANTIALIAS)
logo = ImageTk.PhotoImage(img)
canvas_for_logo.create_image(0, 0, anchor=NW, image=logo)
canvas_for_banner = Canvas(root, bg='red', height=100, width=924) #banner image
canvas_for_banner.pack()
img_banner = Image.open("pic/banner.jpg")
img_banner = img_banner.resize((924,100), Image.ANTIALIAS)
banner = ImageTk.PhotoImage(img_banner)
canvas_for_banner.create_image(0, 0, anchor=NW, image=banner)
root.mainloop()
Can someone please tell me what I did wrong? All replies are much appreciated. Thank you.
This is a typical Tkinter bug. I don't want to go into details cause I don't fully understand why it happens either, but it has something to do with the garbage collector and the fact that it doesn't consider the objects you have created for storing those images like being in use, so it deletes them; or something like that.
Luckily, it has an easy solution: you can either create an internal list variable, let say, self._images that stores each image you are using, something like:
self._images = list()
(...)
self._images.append(logo)
(...)
self._images.append(banner)
Or, you could assign to each canvas instance an attribute image (or img, it doesn't really matters) that stores the image instance it is going to carry. In your code, it will look similar to:
canvas_for_logo.image = logo
(...)
canvas_for_banner.image = banner
This way, you can avoid the garbage collector deleting what it shouldn't, cause now it acknowledges that this instances are being in use.
I'm developing a GUI in Tkinter and want to apply animation in the below GIF on the image when it appears.
Here is my code,
from tkinter import *
from PIL import Image, ImageTk
root = Tk()
frame = Frame(root)
frame.pack()
canvas = Canvas(frame, width=300, height=300, bd=0, highlightthickness=0, relief='ridge')
canvas.pack()
background = PhotoImage(file="background.png")
canvas.create_image(300,300,image=background)
my_pic = PhotoImage(file="start000-befored.png")
frame.after(1000, lambda: (canvas.create_image(50,50,image=my_pic, anchor=NW))) #and on this image, I want to give the effect.
root.mainloop()
Instead of clicking on the play button as shown in GIF, the image should automatically appears after 1 second like this animation and stays on screen. (No closing option).
I'm not 100% sure I understood the problem, but I'll describe how to animate an image.
Tkinter does not contain functions for animating images so you'll have to write them yourself. You will have to extract all subimages, subimage duration and then build a sequencer to swap subimages on your display.
Pillow can extract image sequences. WEBP images seems to only support one frame duration whereas GIF images may have different frame duration for each subimage. I will use only the first duration for GIF images even if there is many. Pillow does not support getting frame duration from WEBP images as far as I have seen but you gan read it from the file, see WebP Container Specification.
Example implementation:
import tkinter as tk
from PIL import Image, ImageTk, ImageSequence
import itertools
root = tk.Tk()
display = tk.Label(root)
display.pack(padx=10, pady=10)
filename = 'images/animated-nyan-cat.webp'
pil_image = Image.open(filename)
no_of_frames = pil_image.n_frames
# Get frame duration, assuming all frame durations are the same
duration = pil_image.info.get('duration', None) # None for WEBP
if duration is None:
with open(filename, 'rb') as binfile:
data = binfile.read()
pos = data.find(b'ANMF') # Extract duration for WEBP sequences
duration = int.from_bytes(data[pos+12:pos+15], byteorder='big')
# Create an infinite cycle of PIL ImageTk images for display on label
frame_list = []
for frame in ImageSequence.Iterator(pil_image):
cp = frame.copy()
frame_list.append(cp)
tkframe_list = [ImageTk.PhotoImage(image=fr) for fr in frame_list]
tkframe_sequence = itertools.cycle(tkframe_list)
tkframe_iterator = iter(tkframe_list)
def show_animation():
global after_id
after_id = root.after(duration, show_animation)
img = next(tkframe_sequence)
display.config(image=img)
def stop_animation(*event):
root.after_cancel(after_id)
def run_animation_once():
global after_id
after_id = root.after(duration, run_animation_once)
try:
img = next(tkframe_iterator)
except StopIteration:
stop_animation()
else:
display.config(image=img)
root.bind('<space>', stop_animation)
# Now you can run show_animation() or run_animation_once() at your pleasure
root.after(1000, run_animation_once)
root.mainloop()
There are libraries, like imgpy, which supports GIF animation but I have no experience in usig any such library.
Addition
The duration variable sets the animation rate. To slow the rate down just increase the duration.
The simplest way to put the animation on a canvas it simply to put the label on a canvas, see example below:
# Replace this code
root = tk.Tk()
display = tk.Label(root)
display.pack(padx=10, pady=10)
# with this code
root = tk.Tk()
canvas = tk.Canvas(root, width=500, height=500)
canvas.pack(padx=10, pady=10)
display = tk.Label(canvas)
window = canvas.create_window(250, 250, anchor='center', window=display)
Then you don't have to change anything else in the program.
I have been trying to make a top-level window view which collects and shows all the images present in a folder in columns of 10. If the images were more than the allocated size of the window I wanted it to be possible to scroll through the images. I followed the answer given to Scrollable Toplevel Window (tkinter)
to correctly add an image to a canvas and make it possible to scroll through them. But, in my case the entire popup window just comes out to be blank. Here is the code
import tkinter as tk
from tkinter import *
import glob
import os
from PIL import Image, ImageTk
def pop_up_window():
win = Toplevel()
vbar = tk.Scrollbar(win, orient = VERTICAL)
vbar.grid(row = 0, column = 1, sticky = "ns")
container = tk.Canvas(win, height=300, width=720, scrollregion=(0, 0, 300, 720))
container.grid(row = 0, column = 0, sticky = "nsew")
vbar.config(command=container.yview)
container.config(yscrollcommand=vbar.set)
path = "D:\\image_collection"
COLUMNS = 10
image_count = 0
for infile in glob.glob(os.path.join(path, '*.jpg')):
image_count += 1
r, c = divmod(image_count-1, COLUMNS)
im = Image.open(infile)
resized = im.resize((100, 100), Image.ANTIALIAS)
img_part = ImageTk.PhotoImage(Image.open(infile).resize((100, 100), Image.ANTIALIAS))
image_in_canvas = container.create_image(r, c, image = img_part)
win.rowconfigure(0, weight=1)
win.columnconfigure(0, weight=1)
root = Tk()
button = Button(root, text='Call Pop-up window', command = pop_up_window)
button.place(x = 0, y = 0)
root.mainloop()
What changes should I make?
You need to keep a reference to your images or it will be garbage collected by Python. A simple change can do it:
placeholder = []
def pop_up_window():
...
for infile in glob.glob(os.path.join(path, '*.jpg')):
image_count += 1
r, c = divmod(image_count-1, COLUMNS)
im = Image.open(infile)
img_part = ImageTk.PhotoImage(Image.open(infile).resize((100, 100), Image.ANTIALIAS))
placeholder.append(img_part)
image_in_canvas = container.create_image(r, c, image = img_part)
Also I want to point that the create_image method takes two coordinates as args. You are currently creating them as if they are grids, and it won't show up in the alignment you expected.
Your code works but it have few issues with it. I fixed all of them or maybe most of them, in case I forgot to notice it.
When creating an image in a function always create a reference to them as here you have many images so you can create a list to your container.
To keep updating the scrollregion depending upon the amount of images bind "" to the container with the callback function lambda e: scrollregion=container.bbox('all').
Here are the changes that I did to your pop_up_window function.
...
path = "D:\\image_collection"
COLUMNS = 7
container.img_list = []
column = 0
row = 0
for infile in glob.glob(os.path.join(path, '*.jpg')):
if column >= COLUMNS:
column = 0
row += 1
im = Image.open(infile).resize((100, 100), Image.ANTIALIAS)
img = ImageTk.PhotoImage(im)
container.img_list.append(img)
container.create_image(column*100+10, row*100+10, image = img, anchor='nw')
column += 1
container.bind('<Configure>',lambda e:container.configure(scrollregion=container.bbox('all')))
...
I am just getting started with tkinter widgets and am trying to create Checkbuttons with both images and text labels. Unfortunately, this cannot be done with basic Checkbutton(..., image= , text= ) as setting the image suppresses the text.
Here's a silly yet reproducible example of what I'm trying to do.
from tkinter import Tk, Frame, Checkbutton, Label
from PIL import ImageTk, Image
import requests
def getImgFromUrl(url): # using solution from : https://stackoverflow.com/a/18369957/2573061
try:
r = requests.get(url, stream=True)
pilImage = Image.open(r.raw)
phoImage = ImageTk.PhotoImage(pilImage)
return phoImage
except Exception as e:
print('Error ' + repr(e) )
return None
class AnimalPicker(Frame):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.master.title("Animal Picker")
def main():
root = Tk()
root.geometry("250x450+300+300")
app = AnimalPicker()
imageUrls = ['http://icons.iconarchive.com/icons/martin-berube/flat-animal/64/dachshund-icon.png',
'http://icons.iconarchive.com/icons/iconka/meow/64/cat-walk-icon.png',
'http://icons.iconarchive.com/icons/sonya/swarm/64/Unicorn-icon.png']
labels = ['Dog','Cat','Unicorn']
images = [getImgFromUrl(x) for x in imageUrls]
for i in range(len(images)):
cb = Checkbutton(root, text=labels[i], image = images[i])
cb.pack(anchor = 'w', padx=5,pady=5)
root.mainloop()
if __name__ == '__main__':
main()
Gives us:
But I would like to have the labels ("Cat", "Dog", "Unicorn") either underneath or to the side of the images.
It's also important that the solution work for an arbitrary number of Checkbuttons, as in the for loop above.
Should I be using Grid and stacking these Checkbuttons next to Labels? I've managed to avoid learning it so far. Or is it easy to do with Pack?
I would like to have the labels ("Cat", "Dog", "Unicorn") either underneath or to the side of the images.
As suggested in Bryan's comment, this can simply be done by configuring compound option of Checkbutton.
Simply replace:
cb = Checkbutton(root, text=labels[i], image = images[i])
with:
cb = Checkbutton(root, text=labels[i], image = images[i], compound='left')
or simply set compound option to an element of ['bottom', 'center', 'left', 'right', 'top'] which is by default None:
cb['compound'] = 'top'
Below example produces a simple example for most windows users:
import tkinter as tk
from PIL import Image, ImageTk
root = tk.Tk()
mypath = r"C:\Users\Public\Pictures\Sample Pictures\Koala.jpg"
img = Image.open(mypath)
glb_img = ImageTk.PhotoImage(img)
tk.Checkbutton(root, text="Koala", image=glb_img, compound='top').pack()
root.mainloop()
Also it's worth noting that importing PIL is redundant for .png format, one could simply use either tk.PhotoImage(data=image_data) or tk.PhotoImage(file=image_file).