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.
Related
The following program attempts to display a row of four buttons. The inner two, "ArrowLeft" and "ArrowRight", display correctly. The outer two do not show the image. I have tried two different strategies in this code for importing the image ("LeftLeft" and "RightRight") and by no means will it work. Since I can't attach the image files in their original form, here is a zip file that includes the code below and six very small .png files. If anyone could take a look and see what is going wrong, I would sure appreciate it.
The alternative tryCanvas code successfully displays the LeftLeft image!
Zip file: https://easyupload.io/ubnfn1
import tkinter as tk
from tkinter import ttk, PhotoImage
from PIL import Image, ImageTk
root = tk.Tk()
def layout(frame):
global leftIcon, upIcon, rightIcon, downIcon
leftLeft1 = Image.open("LeftLeft.png")
leftLeft1.show()
leftIcon = ImageTk.PhotoImage(file = "ArrowLeft.png")
leftLeftIcon = ImageTk.PhotoImage(leftLeft1)
rightIcon = ImageTk.PhotoImage(file="ArrowRight.png")
rightRightIcon = ImageTk.PhotoImage(file = "RightRight.png")
ttk.Button(frame, image=leftLeftIcon).pack(side=tk.LEFT)
ttk.Button(frame, image=leftIcon).pack(side=tk.LEFT)
ttk.Button(frame, image=rightIcon).pack(side=tk.LEFT)
ttk.Button(frame, image=rightRightIcon).pack(side=tk.LEFT)
def tryCanvas():
can1 = tk.Canvas(root)
image = PhotoImage(file='LeftLeft.png')
item = can1.create_image(100, 100, image = image)
can1.image = image
can1.pack()
# tryCanvas()
layout(root)
root.mainloop()
I'm making a hangman-like game; for that, I need all the different stages of the man to be visualized, hence images. I want the entire tkinter window to be an image, when I change the size it pushes the image right.
from tkinter import *
root=Tk()
root.geometry("600x350")
canvas = Canvas(root, width=1600, height=900)
canvas.pack()
img = PhotoImage(file="4.png")
canvas.create_image(470,190, image=img, )
root.mainloop()
If canvas is bigger than window then when you resize then it show more canvas but and it can looks like it moves image.
But if you use smaller canvas then pack() will try to keep centered horizontally. And if you add pack(expand=True) then it will try to keep it centered vertically.
In example code I added red background to window to show where is canvas
import tkinter as tk # PEP8: import *
root = tk.Tk()
root.geometry("600x350")
root['bg'] = 'red'
img = tk.PhotoImage(file="lenna.png")
canvas = tk.Canvas(root, width=600, height=350)
canvas.pack(expand=True)
canvas.create_image(300, 175, image=img)
root.mainloop()
Image Lenna from Wikipedia
PEP 8 -- Style Guide for Python Code
Before resizing:
After resizing:
If you want to draw only image then you could use Label(image=img)
import tkinter as tk # PEP8: import *
root = tk.Tk()
root.geometry("600x350")
root['bg'] = 'red'
img = tk.PhotoImage(file="lenna.png")
label = tk.Label(root, image=img)
label.pack(expand=True)
root.mainloop()
Before resizing:
After resizing:
BTW:
tkinter can bind() some function to event <Configure> and it will execute this function everytime when you resize window (and/or move window) - and this function may also move or resize image in window.
import tkinter as tk # PEP8: import *
from PIL import Image, ImageTk
def resize(event):
global img
lower = min(event.width, event.height)
#print(event.width, event.height, 'lower:', lower)
new_image = original_image.resize((lower, lower))
img = ImageTk.PhotoImage(new_image) # need to assign to global variable because there is bug in PhotoImage
label['image'] = img
# --- main ---
root = tk.Tk()
root.geometry("350x350")
root['bg'] = 'red'
original_image = Image.open("lenna.png")
img = ImageTk.PhotoImage(original_image)
label = tk.Label(root, image=img)
label.pack(expand=True)
root.bind('<Configure>', resize)
root.mainloop()
Before (it resized image at start to fit window):
After (it resized image to fit window):
I have a code that is working perfectly but it's not giving me transparent background, (here is the image ) after a research on web, I found the solution by using canvas widget, we can us images with transparent background.
Here is my code,
import tkinter as tk
from PIL import Image, ImageTk
def work(progress=1):
if progress > 300: # define the width by yourself
return
tmp_images = ImageTk.PhotoImage(progress_images.resize((progress, 10))) # the image size
lb.image = tmp_images # keep reference
lb["image"] = tmp_images # change the image
root.add = root.after(100, work, progress+10) # define the amplitude by yourself
root = tk.Tk()
progress_images = Image.open("path.png")
lb = tk.Label(root, bg="black")
lb.pack(side="left")
work()
root.mainloop()
but I am confused how to change Label widget into Canvas ? can anyone help me please ? I am noob in Tkinter still!!!
You can use canvas.create_text(...) to replace the labels and then use canvas.itemconfig(...) to update the labels. I got this from an other stack over flow question
reference link :-
add label ..
Below is a modified code using Canvas:
import tkinter as tk
from PIL import Image, ImageTk
def work(progress=10):
if progress > 300: # define the width by yourself
return
canvas.image = ImageTk.PhotoImage(progress_images.resize((progress, 30))) # the image size
canvas.itemconfig(pbar, image=canvas.image) # update image item
root.add = root.after(100, work, progress+10) # define the amplitude by yourself
root = tk.Tk()
progress_images = Image.open("path.png")
canvas = tk.Canvas(root, width=300, height=30, bg='black', highlightthickness=0)
canvas.pack()
pbar = canvas.create_image(0, 0, anchor='nw') # create an image item
work()
root.mainloop()
I'm having a problem that when i try to create an image on the canvas i am unable to produce the image. However before my after() loop is initiated i am able to create and configure my images. Also, i am able to remove objects from my canvas using canvas.delete() in my after() loop so i do still have some level of control.
I am on windows 8.1, using Python 3.5.4
import tkinter as tk
from PIL import Image, ImageTk
from math import floor
import numpy as np
from scipy import ndimage
root = tk.Tk()
HEIGHT = 600
WIDTH = 600
CANVAS_MID_X = WIDTH / 2
CANVAS_MID_Y = HEIGHT / 2
def my_mainloop():
#THIS WORKS!!! REMOVES THE DIAL CREATED BEFORE THE MAINLOOP
canvas.delete(dial_1)
#THIS WORKS BELOW BEFORE MAINLOOP, BUT NOT IT WONT WORK! (use opencv?)
img = dial_1_img_resized
img2 = img.rotate(45, expand=True)
dial_1_photo_new = ImageTk.PhotoImage(img2)
dial_2 = canvas.create_image((dial_1_center), image=dial_1_photo_new, anchor=tk.E)
'''CANT DRAW TO CANVAS IN AFTER LOOP'''
print("loop!")
root.after(4000,my_mainloop)
'''-------------------Create Canvas, and starting dials in their starting positions---------------------'''
canvas = tk.Canvas(root, width=HEIGHT, height=WIDTH, bg="black")
canvas.grid(row=0, column=0)
dial_1_path = "gauge1.png"
dial_1_width = 400
dial_1_img = Image.open(dial_1_path, 'r') #open image
dial_1_img_ratio = int(dial_1_img.size[1]) / int(dial_1_img.size[0])
dial_1_img_resized = dial_1_img.resize((dial_1_width, floor(dial_1_img_ratio * dial_1_width)), 1)
dial_1_photo = ImageTk.PhotoImage(dial_1_img_resized)
dial_1_center = (CANVAS_MID_X, CANVAS_MID_Y)
#CREATE DIAL ON CANVAS, THIS WORKS!!
dial_1 = canvas.create_image((dial_1_center), image=dial_1_photo)
'''Start Main Loop'''
root.after(0, my_mainloop)
root.mainloop()
Therefore my question is: is there a way to manipulate and create canvas images in the after() loop? (called my_mainloop) any help is appreciated!
You'll need to save a reference to your photo because it is being garbage collected after my_mainloop runs. You can add it to your canvas object for instance:
canvas.dial_1_photo_new = ImageTk.PhotoImage(img2)
dial_2 = canvas.create_image((dial_1_center), image=canvas.dial_1_photo_new, anchor=tk.E)
I try to show a image in a label when I push a button, but the image are too large and I have tried to resize the image. I have created this function:
def image_resize(imageFile):
width = 500
height = 300
image = Image.open(imageFile)
im2 = image.resize((width, height), Image.ANTIALIAS)
return im2
To show the image I have created this function:
def show_image():
label_originalimage ['image'] = image_tk
And the button with the command=show_image:
filename = 'bild_1.jpg'
image_resize = image_resize(filename)
image_tk = PhotoImage(image_resize)
button_open = Button(frame_open, text='Open Image', command=show_image)
I get only this:
TypeError : __str__ returned non-string (type instance)
The PhotoImage class from tkinter takes a filename as an argument, and as it cannot convert the image into a string, it complains. Instead, use the PhotoImage class from the PIL.ImageTk module. This works for me:
from tkinter import *
from PIL import ImageTk, Image
def image_resize(imageFile):
width = 500
height = 300
image = Image.open(imageFile)
im2 = image.resize((width,height), Image.ANTIALIAS)
return im2
def show_image():
label_originalimage ['image'] = image_tk
root = Tk()
filename = './Pictures/Space/AP923487321702.jpg'
image_resize = image_resize(filename)
image_tk = ImageTk.PhotoImage(image_resize)
label_originalimage = Label(root)
label_originalimage.pack()
button_open = Button(root, text='Open Image', command=show_image)
button_open.pack()
root.mainloop()
Notice the change from image_tk = PhotoImage(image_resize) to image_tk = ImageTk.PhotoImage(image_resize).
I had the same problem when I try to construct a canvas image item for
tkinter from a tkinter PhotoImage. The latter was constructed from some
image data in memory (in my case an opencv image). The same exception
occurs if I simply try to convert the PhotoImage to a string.
I guess there is a bug in the conversion method __str__ of the PhotoImage,
making it simply returns the image source. If constructed from a file name
(see below) this works fine. If constructed from some image data, this is not
of type string and yields an exception.
Unfortunately, using the compatible PhotoImage from PIL's
ImageTk module like matsjoyce suggested didn't help me either because I experienced an even worse problem, probably a platform or library version dependent bug (I used OS X 10.11.6, python 3.5, tkinter 8.6, PIL 1.1.7): Now the python script crashed at the construction of the canvas image item with a "Bus Error".
The only workaround I am aware of was to store the image data into a temporary file and use a tkinter PhotoImage constructed from that file name. (Trying the same with the PIL PhotoImage still crashes.)
#!/usr/bin/env python3
import tkinter
import tempfile
import cv2
def opencv2photoimg(opencv_img):
"""Convert OpenCV (numpy) image to tkinter photo image."""
# ugly workaround: store as file & load file, because direct
# construction leads to a crash on my platform
tmpfile = tempfile.NamedTemporaryFile(suffix='.png', delete=True)
# ^^^ I am using PNGs only, you might want to use another suffix
cv2.imwrite(tmpfile.name, opencv_img)
return tkinter.PhotoImage(file=tmpfile.name)
# load image
img = cv2.imread('test.png')
# do something w/ the image ...
# setup tk window w/ canvas containing an image
root = tkinter.Tk()
canvas = tkinter.Canvas(root, width=img.shape[1], height=img.shape[0])
canvas.pack()
# keep reference to PhotoImage to avoid it being garbage collected
# (well known tkinter bug for canvas image items)
photo_img = opencv2photoimg(img)
# create a canvas item
img_item = canvas.create_image(0, 0, anchor=tkinter.NW, image=photo_img)
# display the window
tkinter.mainloop()
I do not think it's elegant, but it works.
Yes it works, but yeeeucchh - what I way to have to do it.
Surely there is a better way.
Here is my test code I got to starting from here....
import tkinter
from PIL import Image
import numpy
import time
import io
#python2 version (original) -> 120fps
#full physical file io and new image each cycle -> 130fps
#reuse PIL Image instead of create new each time -> 160fps
class mainWindow():
times=1
timestart=time.clock()
data=numpy.array(numpy.random.random((400,500))*100,dtype=int)
theimage = Image.frombytes('L', (data.shape[1],data.shape[0]),data.astype('b').tostring())
def __init__(self):
self.root = tkinter.Tk()
self.frame = tkinter.Frame(self.root, width=500, height=400)
self.frame.pack()
self.canvas = tkinter.Canvas(self.frame, width=500,height=400)
self.canvas.place(x=-2,y=-2)
self.root.after(0,self.start) # INCREASE THE 0 TO SLOW IT DOWN
self.root.mainloop()
def start(self):
global data
global theimage
self.theimage.frombytes(self.data.astype('b').tobytes())
self.theimage.save('work.pgm')
self.photo = tkinter.PhotoImage(file='work.pgm')
self.canvas.create_image(0,0,image=self.photo,anchor=tkinter.NW)
self.root.update()
self.times+=1
if self.times%33==0:
print("%.02f FPS"%(self.times/(time.clock()-self.timestart)))
self.root.after(10,self.start)
self.data=numpy.roll(self.data,-1,1)
if __name__ == '__main__':
x=mainWindow()
Here it is: I found that the input data to photoimage can be a byte array that looks like a ppm file, although it only appears to work on a subset of legal ppm (e.g. 16 bit values don't work)
So for future reference.......
import tkinter
import numpy
import time
#python2 version (original) -> 120fps
#full physical file io and new image each cycle -> 130fps
#reuse PIL Image instead of create new each time -> 160fps
#and... direct image into tkinter using ppm byte array -> 240 fps
class mainWindow():
times=1
timestart=time.clock()
data=numpy.array(numpy.random.random((400,500))*900,dtype=numpy.uint16)
def __init__(self):
self.root = tkinter.Tk()
self.frame = tkinter.Frame(self.root, width=500, height=400)
self.frame.pack()
self.canvas = tkinter.Canvas(self.frame, width=500,height=400)
self.canvas.place(x=-2,y=-2)
xdata = b'P5 500 400 255 ' + self.data.tobytes()
self.photo = tkinter.PhotoImage(width=500, height=400, data=xdata, format='PPM')
self.imid = self.canvas.create_image(0,0,image=self.photo,anchor=tkinter.NW)
self.root.after(1,self.start) # INCREASE THE 0 TO SLOW IT DOWN
self.root.mainloop()
def start(self):
global data
xdata = b'P5 500 400 255 ' + numpy.clip(self.data,0,255).tobytes()
self.photo = tkinter.PhotoImage(width=500, height=400, data=xdata, format='PPM')
if True:
self.canvas.itemconfig(self.imid, image = self.photo)
else:
self.canvas.delete(self.imid)
self.imid = self.canvas.create_image(0,0,image=self.photo,anchor=tkinter.NW)
self.times+=1
if self.times%33==0:
print("%.02f FPS"%(self.times/(time.clock()-self.timestart)))
self.root.update()
self.root.after(0,self.start)
self.data=numpy.roll(self.data,-1,1)
if __name__ == '__main__':
x=mainWindow()