Generator for placing images on tkinter canvas - python

I have a list of paths to around 90 images, now I want to place all of them on the canva,but only let's say 30 in one "row", but if I use
from tkinter import *
def createCanvaImages(paths):
paths = ['list with the paths']
mainWin = Tk()
canva = Canvas(mainWin, width = 900, height = 300).pack()
for x in range(0, len(paths),):
if x <= 30: #not sure if this places only 30 in one row
y=x/3
elif x > 30
y=(x+24)/3
elif x >= 60
y=(x+48)/3
img = PhotoImage(file = paths[x])
canva.create_image(x+24, y, image = img)
mainWin.mainloop()
it only shows the image from the last path
EDIT
now shows all images on the canvas if the canvas isn't in a frame(thanks to Novel) but doesn't work if the canva is in a frame
from tkinter import *
def createImagePaths(dct):
paths=[]
for i in range(len(masteries)):
if dct.get(masteries[i]) == 0:
file = masteries[i]+'.png'
path = os.path.join(path_gray, file)
paths.append(path)
#create canvas image fnc
if dct.get(masteries[i]) != 0:
file = masteries[i]+'.png'
path = os.path.join(path_colored, file)
paths.append(path)
return createCanvaImages(paths)
def createCanvaImages(paths):
img_refs = []
canva = Canvas(masteryFrame, height = 400).pack()
for i, path in enumerate(paths):
col,row = divmod(i,30)
img = PhotoImage(file=path)
canva.create_image( row*24, col*24, image = img, anchor = 'nw')
img_refs.append(img)
root = Tk()
mainFrame = Frame(root)
mainFrame.grid(column=0,row=0, sticky=(N,W,E,S))
masteryFrame = Frame(root)
masteryFrame.grid(row=1,column=0, sticky=(N,W,E,S))
root.mainloop()

You need to save your image references. The easiest way to do that is to just add them to a list. As a guess:
from tkinter import *
def createCanvaImages(paths):
canva = Canvas(masteryFrame, width = 900, height = 300)
canva.pack()
canva.img_refs = []
for i, path in enumerate(paths):
row, col = divmod(i, 30)
img = PhotoImage(file = path)
canva.create_image(col*24, row*24, image = img, anchor='nw') # each image is 24x24
canva.img_refs.append(img)
Also, make sure you never put a widget initialization and layout on the same line. IOW, don't ever do this: Widget(master).pack(). Always put those on separate lines.
You should also learn about OOP and classes very soon. Using functions to build the UI like this will get very messy and buggy very quickly.

Related

tk.Misc.lift(canvas) seemingly not working

The full code for the chess game im making
import chess
#from stockfish import Stockfish
import tkinter as tk
import time
board = chess.Board()
def boardtoarr(board): #converts chess board string to 2d array
boardlist = []
linelist = []
for b in range(0,128,2):
linelist.append(str(board)[b])
if len(linelist) == 8:
boardlist.append(linelist)
linelist = []
return boardlist
#given the square of a piece on the board, returns the legal squares the piece can move to
def piecelegalmoves(square,cboard):
ans = []
for a in cboard.legal_moves:
a = str(a)
if a[0:2] == square:
ans.append(a[2::])
return ans
def alllegalmoves(cboard): #given a chess board it will return all legal moves
x = str(cboard.legal_moves)[38:-2]
x = x.split(', ')
return x
squarevar = [] #variable to contain the starting square and the moving-to square
def squareclicked(event):
global squarevar
x,y = event.x,event.y
#print(x,y)
#print(pixeltosquare(x,y))
#print(lsquaretocsquare(pixeltosquare(x,y)))
#squarevar.append(lsquaretocsquare(pixeltosquare(x,y)))
#print(squarevar)
return lsquaretocsquare(pixeltosquare(x,y))
def highlightlegalmoves(piece,board):
global BoardImage,redsquare
#for widget in root.winfo_children(): #code to clear page
#widget.destroy()
legalmoves = piecelegalmoves(piece,board)
canvas = tk.Canvas(root,width = BoardImage.width(), height = BoardImage.height())
#canvas = tk.Canvas(root,width = 500, height = 500)
canvas.pack()
for a in legalmoves:
x = csquaretolsquare(a)
print(x)
pixel = topleftpixels(x[0],x[1])
#canvas.create_image(0,0, image = redsquare, anchor = 'nw')
canvas.create_image(pixel[1],pixel[0], image = redsquare, anchor = 'nw')
tk.Misc.lift(canvas)
def makemoves(event): #Function to make chess moves when clicks have been inputted
global squarevar
squarevar.append(squareclicked(event))
if len(squarevar) != 2:
#print(squarevar[0])
highlightlegalmoves(squarevar[0],board)
if len(squarevar) == 2:
if squarevar[1] in piecelegalmoves(squarevar[0],board): #checking if move is legal to prevent errors (input validation)
board.push_san(squarevar[0]+squarevar[1]) #play move on backend board
squarevar = []
for widget in root.winfo_children(): #code to clear page
widget.destroy()
boardtoimage(root,str(board))
else:
squarevar = []
def topleftpixels(x,y): #returns top left pixel of given square
return [x*100,y*100]
def pixeltosquare(x,y): #given a pixel it will return what chess square it is on
x = ('000' + str(x))
x = x[len(x)-3::]
y = ('000' + str(y))
y = y[len(y)-3::]
return [int(y[0]),int(x[0])]
def csquaretolsquare(square): #converts chess square notation to list square notation
ans = []
letterorder = 'abcdefgh'
ans.append(8-int(square[1]))
ans.append(letterorder.index(square[0]))
return ans
def lsquaretocsquare(square): #converts list square notation to chess square notation
ans = ''
letterorder = 'abcdefgh'
ans += (letterorder[square[1]])
ans += str(8-square[0])
return ans
def boardtoimage(root,boardstr): #places all pieces onto the window graphically
global wQueen,wKing,wKnight,wBishop,wPawn,wRook,bQueen,bKing,bKnight,bBishop,bPawn,bRook,BoardImage
LetterToImage= {
'r': bRook, 'n': bKnight, 'b': bBishop, 'q': bQueen, 'k': bKing, 'p': bPawn,
'R': wRook, 'N': wKnight, 'B': wBishop, 'Q': wQueen, 'K': wKing, 'P': wPawn,
}
boardcanvas = tk.Canvas(root,width = BoardImage.width(), height = BoardImage.height()) #This will be the canvas for the whole board, pieces and all
boardcanvas.pack()
boardcanvas.create_image(0,0, image = BoardImage, anchor = 'nw') #placing chess board on canvas
#iterate through board and place piece images in correct place
for y in range(8):
for x in range(8):
piece = boardtoarr(boardstr)[y][x]
if piece in LetterToImage.keys():
boardcanvas.create_image(topleftpixels(x,y)[0], topleftpixels(x,y)[1], image=LetterToImage[piece], anchor="nw")
boolvar = False
def CMI_clicked(): #check if the chess menu image was clicked
global root
global BoardImage
global boolvar
for widget in root.winfo_children(): #code to clear page
widget.destroy()
boardtoimage(root,str(board))
root.bind("<Button-1>", makemoves)
def Menu():
global root
global ChessMenuImage
#Menu
ChessMenuOption = tk.Button(root ,image = ChessMenuImage, command = CMI_clicked) #create button using chess menu image and call function 'CMI_clicked' when pressed
ChessMenuOption.place(x = 380, y = 380) #place the chess menu image at given coordinates
root = tk.Tk()
ChessMenuImage = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\Chess_Selection_Image.png') #Load chess menu image file
BoardImage = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\ChessBoardImage.png') #Load board image file
redsquare = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\redsquare.png') #Load see-through red square file, used for showing legal moves in the GUI
#Piece image loading
#White
wQueen = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\wQueen.png')
wKing = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\wKing.png')
wPawn = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\wPawn.png')
wBishop = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\wBishop.png')
wKnight = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\wKnight.png')
wRook = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\wRook.png')
#Black
bQueen = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\bQueen.png')
bKing = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\bKing.png')
bPawn = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\bPawn.png')
bBishop = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\bBishop.png')
bKnight = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\bKnight.png')
bRook = tk.PhotoImage(file = r'C:\Users\benja\Documents\Python\Chess project\bRook.png')
root.geometry('800x800')
Menu()
root.mainloop()
specifically lines 47-61 'highlightlegalmoves' function
def highlightlegalmoves(piece,board):
global BoardImage,redsquare
#for widget in root.winfo_children(): #code to clear page
#widget.destroy()
legalmoves = piecelegalmoves(piece,board)
canvas = tk.Canvas(root,width = BoardImage.width(), height = BoardImage.height())
#canvas = tk.Canvas(root,width = 500, height = 500)
canvas.pack()
for a in legalmoves:
x = csquaretolsquare(a)
print(x)
pixel = topleftpixels(x[0],x[1])
#canvas.create_image(0,0, image = redsquare, anchor = 'nw')
canvas.create_image(pixel[1],pixel[0], image = redsquare, anchor = 'nw')
tk.Misc.lift(canvas)
so if I run the code
for widget in root.winfo_children(): #code to clear page
widget.destroy()
the image I get (before/after I click a piece)
before:
after (I clicked the e2 pawn):
so I know that under the original canvas its actually displaying what I want. I just what to show to lift it but its not doing that.
when I have the delete all widgets code commented, the image does not change once I click a piece, it does not display the legal moves of the piece.

How to resize images that are displayed from an optionmenu in Tkinter?

As mentioned in the title, how do I resize the pictures dynamically?
Functionality: When I click "Other" button, an open menu is displayed. The optionmenu displays images from a folder that I have specified in the code. All the images are in different sizes. How can I maintain the aspect ratio while resizing them?
...
def otherdrops():
other_opt = wother
other_opt.config(width=50, font=('Helvetica', 12))
other_opt.pack()
def other_images(wother):
print(wother) # selected option
other_label.config(image=other[wother])
other_label = tk.Label(otherframe)
other_label.pack(side = 'bottom', pady=padylength)
other = {}
for other_name in tradinglists.tradingotherimages:
other[other_name] = ImageTk.PhotoImage(Image.open("/Images/{}.png".format(other_name)))
othervariable = tk.StringVar(tab2)
othervariable.set(tradinglists.tradingotherimages[0])
wother = tk.OptionMenu(otherframe, othervariable, *tradinglists.tradingotherimages, command=other_images)
def refreshother():
otherframe.pack_forget() if otherframe.winfo_manager() else otherframe.pack(anchor='center')
other_k = tk.Button(wavebtnframe, bg = "red", text="Other", width = artbtn_width, height = btnsize_height, command=lambda:[otherdrops(), refreshother()])
other_k.pack(side = 'left', padx=wavebtnspadx, pady=padylength)
V2:
def importImageWithResize(filename):
img = Image.open(filename)
width, height = img.size
ratio = width / height
new_height = 20
new_width = new_height * ratio
return img.resize((width, height))
def otherdrops():
other_opt = wother
other_opt.config(width=50, font=('Helvetica', 12))
other_opt.pack()
def other_images(wother):
print(wother) # selected option
other_label.config(image=other[wother])
other_label = tk.Label(otherframe)
other_label.pack(side = 'bottom', pady=padylength)
other = {}
for other_name in tradinglists.tradingotherimages:
other[other_name] = ImageTk.PhotoImage(importImageWithResize("./Images/{}.png".format(other_name)))
othervariable = tk.StringVar(tab2)
othervariable.set(tradinglists.tradingotherimages[0])
wother = tk.OptionMenu(otherframe, othervariable, *tradinglists.tradingotherimages, command=other_images)
def refreshother():
otherframe.pack_forget() if otherframe.winfo_manager() else otherframe.pack(anchor='center')
other_k = tk.Button(wavebtnframe, bg = "red", text="Other", width = artbtn_width, height = btnsize_height, command=lambda:[otherdrops(), refreshother()])
other_k.pack(side = 'left', padx=wavebtnspadx, pady=padylength)
You can resize images with image_name.resize((width, height))
I'd make a method like this:
def importImageWithResize(filename):
img = Image.open(filename)
width, height = img.size
ratio = width / height
new_height = preset_height
new_width = int(new_height * ratio)
return img.resize((new_width, new_height ))
And then change your import line to match:
other[other_name] = ImageTk.PhotoImage(importImageWithResize("/Images/{}.png".format(other_name)))
I assumed you had a set height you want them all to match, but you could change that to have a preset width or whatever you want. If you have specific rules for the size you want and need help, feel free to ask for examples.
Let us know if you have more questions etc.

Out of memory issue with digital picture frame program on Raspberry Pi (Model B, rev. 2.0)

I am working on a Python program for displaying photos on the Raspberry Pi (Model B Revision 2.0 with 512MB RAM). It uses Tk for displaying the images.
The program is mostly finished, but I ran into an issue where the program is terminated by the kernel because of low memory. This seems to happen randomly.
I do not understand why this happens. I have noticed that during image switching, the CPU spikes up significantly (up to 90%). I therefore thought that it might be an issue with the CPU not keeping up between two images and then falling behind and running out of memory. To test this I increased the timeout between showing images to 1 minute, but that did not help.
My question is, whether I am doing something wrong/inefficiently in the code (see below)? If not: I am considering switching to PyQt, because it seems to accelerate graphics with OpenGL (from what I read). Is this true and/or do you think that this might help with the issue I am facing?
This is my current Python code:
# From: https://stackoverflow.com/questions/19838972/how-to-update-an-image-on-a-canvas
import os
from pathlib import Path
from tkinter import *
from PIL import Image, ExifTags, ImageTk
import ipdb
class MainWindow():
def __init__(self, main):
self.my_images = []
self._imageDirectory = str(Path.home().joinpath("./Pictures/rpictureframe"))
self.main = main
w, h = main.winfo_screenwidth(), root.winfo_screenheight()
self.w, self.h = w, h
main.attributes("-fullscreen", True) # REF: https://stackoverflow.com/questions/45136287/python-tkinter-toggle-quit-fullscreen-image-with-double-mouse-click
main.focus_set()
self.canvas = Canvas(main, width=w, height=h)
self.canvas.configure(background="black", highlightthickness=0)
self.canvas.pack()
self.firstCall = True
# set first image on canvas
self.image_on_canvas = self.canvas.create_image(w/2, h/2, image = self.getNextImage()) ### replacing getNextImage instead of getNextImageV1 here fails
#property
def imageDirectory(self):
return self._imageDirectory
#imageDirectory.setter
def setImageDirectory(self,imageDirectory):
self._imageDirectory = imageDirectory
def getNextImage(self):
if self.my_images == []:
self.my_images = os.listdir(self.imageDirectory)
currentImagePath = self.imageDirectory + "/" + self.my_images.pop()
self.currentImage = self.readImage(currentImagePath, self.w, self.h)
return self.currentImage
def readImage(self,imagePath,w,h):
pilImage = Image.open(imagePath)
pilImage = self.rotateImage(pilImage)
pilImage = self.resizeImage(pilImage,w,h)
return ImageTk.PhotoImage(pilImage)
def rotateImage(self,image):
# REF: https://stackoverflow.com/a/26928142/653770
try:
for orientation in ExifTags.TAGS.keys():
if ExifTags.TAGS[orientation]=='Orientation':
break
exif=dict(image._getexif().items())
if exif[orientation] == 3:
image=image.rotate(180, expand=True)
elif exif[orientation] == 6:
image=image.rotate(270, expand=True)
elif exif[orientation] == 8:
image=image.rotate(90, expand=True)
except (AttributeError, KeyError, IndexError):
# cases: image don't have getexif
pass
return image
def resizeImage(self,pilImage,w,h):
imgWidth, imgHeight = pilImage.size
if imgWidth > w or imgHeight > h:
ratio = min(w/imgWidth, h/imgHeight)
imgWidth = int(imgWidth*ratio)
imgHeight = int(imgHeight*ratio)
pilImage = pilImage.resize((imgWidth,imgHeight), Image.ANTIALIAS)
return pilImage
def update_image(self):
# REF: https://stackoverflow.com/questions/7573031/when-i-use-update-with-tkinter-my-label-writes-another-line-instead-of-rewriti/7582458#
self.canvas.itemconfig(self.image_on_canvas, image = self.getNextImage()) ### replacing getNextImage instead of getNextImageV1 here fails
self.main.after(5000, self.update_image)
root = Tk()
app = MainWindow(root)
app.update_image()
root.mainloop()
UPDATE:
Below you will find the current code that still produces the out-of-memory issue.
You can find the dmesg out-of-memory error here: https://pastebin.com/feTFLSxq
Furthermore this is the periodic (every second) output from top: https://pastebin.com/PX99VqX0
I have plotted the columns 6 and 7 (memory usage) of the top output:
As you can see, there does not appear to be a continues increase in memory usage as I would expect from a memory leak.
This is my current code:
# From: https://stackoverflow.com/questions/19838972/how-to-update-an-image-on-a-canvas
import glob
from pathlib import Path
from tkinter import *
from PIL import Image, ExifTags, ImageTk
class MainWindow():
def __init__(self, main):
self.my_images = []
self._imageDirectory = str(Path.home().joinpath("Pictures/rpictureframe"))
self.main = main
w, h = main.winfo_screenwidth(), root.winfo_screenheight()
self.w, self.h = w, h
# main.attributes("-fullscreen", True) # REF: https://stackoverflow.com/questions/45136287/python-tkinter-toggle-quit-fullscreen-image-with-double-mouse-click
main.focus_set()
self.canvas = Canvas(main, width=w, height=h)
self.canvas.configure(background="black", highlightthickness=0)
self.canvas.pack()
# set first image on canvas
self.image_on_canvas = self.canvas.create_image(w / 2, h / 2,
image=self.getNextImage()) ### replacing getNextImage instead of getNextImageV1 here fails
#property
def imageDirectory(self):
return self._imageDirectory
#imageDirectory.setter
def setImageDirectory(self, imageDirectory):
self._imageDirectory = imageDirectory
def getNextImage(self):
if self.my_images == []:
# self.my_images = os.listdir(self.imageDirectory)
self.my_images = glob.glob(f"{self.imageDirectory}/*.jpg")
currentImagePath = self.my_images.pop()
self.currentImage = self.readImage(currentImagePath, self.w, self.h)
return self.currentImage
def readImage(self, imagePath, w, h):
with Image.open(imagePath) as pilImage:
pilImage = self.rotateImage(pilImage)
pilImage = self.resizeImage(pilImage, w, h)
return ImageTk.PhotoImage(pilImage)
def rotateImage(self, image):
# REF: https://stackoverflow.com/a/26928142/653770
try:
for orientation in ExifTags.TAGS.keys():
if ExifTags.TAGS[orientation] == 'Orientation':
break
exif = dict(image._getexif().items())
if exif[orientation] == 3:
image = image.rotate(180, expand=True)
elif exif[orientation] == 6:
image = image.rotate(270, expand=True)
elif exif[orientation] == 8:
image = image.rotate(90, expand=True)
except (AttributeError, KeyError, IndexError):
# cases: image don't have getexif
pass
return image
def resizeImage(self, pilImage, w, h):
imgWidth, imgHeight = pilImage.size
if imgWidth > w or imgHeight > h:
ratio = min(w / imgWidth, h / imgHeight)
imgWidth = int(imgWidth * ratio)
imgHeight = int(imgHeight * ratio)
pilImage = pilImage.resize((imgWidth, imgHeight), Image.ANTIALIAS)
return pilImage
def update_image(self):
# REF: https://stackoverflow.com/questions/7573031/when-i-use-update-with-tkinter-my-label-writes-another-line-instead-of-rewriti/7582458#
self.canvas.itemconfig(self.image_on_canvas,
image=self.getNextImage()) ### replacing getNextImage instead of getNextImageV1 here fails
self.main.after(30000, self.update_image)
root = Tk()
app = MainWindow(root)
app.update_image()
root.mainloop()
I believe there is a memory leak when you open the image files with PIL and don't close them.
To avoid it, you must call Image.close(), or better yet consider using the with syntax.
def readImage(self,imagePath,w,h):
with Image.open(imagePath) as pilImage:
pilImage = self.rotateImage(pilImage)
pilImage = self.resizeImage(pilImage,w,h)
return ImageTk.PhotoImage(pilImage)
I run the code on my machine and I noticed similiar spikes.
After some memory adjustments on a virtual machine I had a system without swap (turned of to get the crash "faster") and approximately 250Mb free memory.
While base memory usage was somewhere around 120Mb, the image change was between 190Mb and 200Mb (using images with a file size of 6,6Mb and 5184x3456 pixel) similiar to your plot.
Then I copied a bigger (panorama) image (8,1Mb with 20707x2406 pixel) to the folder - and voila the machine got stuck.
I could see that the memory usage of the process reached 315Mb and the system became unusable (after 1 minute I "pulled the plug" on the VM).
So I think your problem has nothing todo with the actual code, but with the pictures you are trying to load (and the limited amount of RAM/Swap from your system). Maybe skipping the rotate and resize functions might mitigate your problem...

Tkinter bind widgets below a rectangle widget to a mouse event

I hope I am explaining the problem correctly.
My example below is able to move two images defined on a canvas. The problem is that I want a rectangle, also defined on the canvas, on top of the images. When I do that using .tag_raise, the event triggered by mouse drag is triggered by the rectangle, not the images.
I tried using bing_class but that did not work. I tried to define a separate canvas for the rectangle but it has to overlay the main canvas and I got stuck.
How to keep the rectangle on top but bind the images to my mouse drag event?
import Tkinter as tk # for Python2
import PIL.Image, PIL.ImageTk
win = tk.Tk()
canvas = tk.Canvas(win, height = 500, width = 500)
#Create a rectangle with stipples on top of the images
rectangle = canvas.create_rectangle(0, 0, 400, 300, fill = "gray", stipple = "gray12")
#Create two images
SPRITE = PIL.Image.open("image.jpg")
imagePIL = SPRITE.resize((100, 100))
imagePI = PIL.ImageTk.PhotoImage(imagePIL)
image1 = canvas.create_image(100, 100, image = imagePI, tags = "image")
image2 = canvas.create_image(200, 200, image = imagePI, tags = "image")
#Callback
# Here I select image1 or image2 depending on where I click, and
# drag them on the canvas. The problem is when I put the rectangle
# on top using tag_raise (see below).
def callback(event):
id = canvas.find_withtag(tk.CURRENT)
canvas.coords(id, (event.x, event.y))
#Binding
canvas.bind("<B1-Motion>", callback)
#Place the rectangle on top of all
canvas.pack()
# This is the problem. I want to have the rectangle on top and be able to use the callback
#canvas.tag_raise(rectangle)
canvas.mainloop()
SOLUTION: I enhanced Nehal's answer with the following code. His answer had a glitch, by which images could be switched. In my enhancement I solve it by storing a lock for each image so that, while dragging an image around on the canvas, the same image is dragged. When I move e.g. image1 over image2 I notice that image1 does not completely move over image2, which is fine for me.
import Tkinter as tk # for Python2
import PIL.Image, PIL.ImageTk
win = tk.Tk()
canvas = tk.Canvas(win, height = 500, width = 500)
#Create a rectangle with stipples on top of the images
rectangle = canvas.create_rectangle(0, 0, 400, 300, fill = "gray", stipple = "gray12")
#Create two images
SPRITE = PIL.Image.open("image.jpg")
imagePIL = SPRITE.resize((100, 100))
imagePI = PIL.ImageTk.PhotoImage(imagePIL)
image1 = canvas.create_image(100, 100, image = imagePI, tags = "image")
image2 = canvas.create_image(200, 200, image = imagePI, tags = "image")
images = [image1, image2]
locks = [True, True]
def getImage(x, y):
for image in images:
curr_x, curr_y = canvas.coords(image)
x1 = curr_x - imagePI.width()/2
x2 = curr_x + imagePI.width()/2
y1 = curr_y - imagePI.height()/2
y2 = curr_y + imagePI.height()/2
if (x1 <= x <= x2) and (y1 <= y <= y2):
return image
#Callback
# Here I select image1 or image2 depending on where I click, and
# drag them on the canvas.
def callback(event):
id = getImage(event.x, event.y)
if id:
if locks[images.index(id)] is False: #Hold on to the image on which I originally clicked
canvas.coords(id, (event.x, event.y))
def mouseClick(event):
id = getImage(event.x, event.y)
if id:
locks[images.index(id)] = False
print(locks)
def mouseRelease(event):
id = getImage(event.x, event.y)
if id:
locks[images.index(id)] = True
print(locks)
#Binding
canvas.bind("<ButtonPress-1>", mouseClick) #unlock the image to move it
canvas.bind("<ButtonRelease-1>", mouseRelease) #lock the image
canvas.bind("<B1-Motion>", callback)
#Place the rectangle on top of all
canvas.pack()
# This was the original problem
canvas.tag_raise(rectangle)
canvas.mainloop()
I don't know a tkinter specific way to do this, however, you can try to get the coordinates of the closest image and play with them. Like this:
import Tkinter as tk # for Python2
import PIL.Image, PIL.ImageTk
win = tk.Tk()
canvas = tk.Canvas(win, height = 500, width = 500)
#Create a rectangle with stipples on top of the images
rectangle = canvas.create_rectangle(0, 0, 400, 300, fill = "gray", stipple = "gray12")
#Create two images
SPRITE = PIL.Image.open("image.jpg")
imagePIL = SPRITE.resize((100, 100))
imagePI = PIL.ImageTk.PhotoImage(imagePIL)
image1 = canvas.create_image(100, 100, image = imagePI, tags = "image")
image2 = canvas.create_image(200, 200, image = imagePI, tags = "image")
images = [image1, image2]
def getImage(x, y):
for image in images:
curr_x, curr_y = canvas.coords(image)
x1 = curr_x - imagePI.width()/2
x2 = curr_x + imagePI.width()/2
y1 = curr_y - imagePI.height()/2
y2 = curr_y + imagePI.height()/2
if (x1 <= x <= x2) and (y1 <= y <= y2):
return image
#Callback
# Here I select image1 or image2 depending on where I click, and
# drag them on the canvas. The problem is when I put the rectangle
# on top using tag_raise (see below).
def callback(event):
id = getImage(event.x, event.y)
if id:
canvas.coords(id, (event.x, event.y))
#Binding
canvas.bind("<B1-Motion>", callback)
#Place the rectangle on top of all
canvas.pack()
# This is the problem. I want to have the rectangle on top and be able to use the callback
canvas.tag_raise(rectangle)
canvas.mainloop()

Tkinter: Can't add more than 3 PhotoImages to a list for animation?

I'm trying to program a game in Tkinter (trust that I'd much rather use another better-suited library), and I'm having trouble coding in animations. I tried 2 ways of working around the problem but both have failed.
What I've done is to write an Animation class, which, in my first implementation, takes a list of PhotoImages, and deletes/draws each frame:
class Animation():
def __init__(self, container, pos, images, framerate):
self.container = container
self.x = pos[0]
self.y = pos[1]
self.images = copy(images)
self.imagescopy = copy(images)
self.framerate = framerate # ms between frames
self.done = False
def animate(self):
if len(self.images)>0:
self.container.delete("currentframe")
self.container.create_image(self.x, self.y, image = self.images.pop(0), tag = "currentframe")
self.animation = self.container.after(self.framerate, self.animate)
else:
self.container.delete("currentframe")
self.container.after_cancel(self.animation)
self.done = True
In the init method of the class which draws the Tk window, I loaded the PhotoImages (I tried this with a simple placeholder 5 frame animation):
for i in range(1,6):
self.imgs_playbgready.append(PhotoImage(file ='Graphics\\PlayArea\\ani_bgready'+str(i)+'.gif'))
But when I try to execute the program, it takes very very long to load beyond the third image. Even writing out each line explicitly like so:
self.imgs_playbgready = [PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready1.gif'),
PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready2.gif'),
PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready3.gif'),
PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready4.gif'),
PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready5.gif') ]
didn't help. Removing the last 2 items seems to make everything fine, so it looks like there's a limit on 3 PhotoImages in a list though I can't imagine why that'd be the case?
In my second implementation, I tried to change the animate method so that the PhotoImages would be 'lazily loaded' (so now self.images is a list of file names rather than PhotoImages):
def animate(self):
if len(self.images)>0:
self.container.delete("currentframe")
image = PhotoImage(file = self.images.pop(0))
self.container.create_image(self.x, self.y, image = image, tag = "currentframe")
self.animation = self.container.after(self.framerate, self.animate)
else:
self.container.delete("currentframe")
self.container.after_cancel(self.animation)
self.done = True
Now when I try to run the program it freezes at frame 4 and I have to force quit. Again, it seems 3 is the magic number?
I'm not really sure at all what's going on with this. From googling about animation in Tkinter, redrawing PhotoImages frame by frame seems the way to go. Please be patient with me because I only started learning to use the Tkinter library last week. Any help is much appreciated.
EDIT
Thanks for your help.
To clarify the points raised in the comment (full working code at the end):
It takes a few minutes before the last two images load in and the program starts up. My framerate as defined in the Animation class is more like an inverse framerate (number of miliseconds between each redraw) as you can see in the animate function. My whole code is really quite long so I will try to extract the more relevant parts (for the first implementation I tried). Basically in my GUI class (named PlayArea), under the __init__ method, I try to load in the image list for the animation as well as create an Animation instance:
class PlayArea():
def __init__(self):
self.window = Tk()
.....
#---------------------
# Button and background images
#---------------------
......
self.imgs_playbgready = [PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready1.gif'),
PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready2.gif'),
PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready3.gif'),
PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready4.gif'),
PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready5.gif') ]
.......
self.ani_playbgready = Animation(self.playCanvas,(self.playWidth/2, self.playHeight/2),self.imgs_playbgready, 5000)
The animation will only run after a user clicks a button, and the command callback for the button is this(I've put in only the relevant portions):
startGame(self):
# Play Area
self.playCanvas.delete("bgready")
self.ani_playbgready.animate() # animating the images
I know for sure that the program is freezing up after loading the third PhotoImage, because when I try to do the loading using a for loop, and print the list after each loop, I can see it hang up in the fourth one. As mentioned, this code works as long as I only load 3 images into the list. In other words, it probably isn't a problem with my animate method
The monstrous block of working code as requested:
from Tkinter import *
import tkFont
from PIL import Image, ImageTk
from copy import deepcopy, copy
#------------------------------------------------
# Player, Controller, Levels, and Customer
#------------------------------------------------
class Player():
'''
Player class to keep track of progress of specific player.
Progress and highscore will be written to a save file
'''
def __init__(self, name = "Default", highscore = 0, currentlevel = 1):
self.name = name
self.highscore = highscore
self.currentlevel = currentlevel
class Controller():
'''
Controller which keeps track of score as well as the timer.
To be instantiated by the GUI class to determine the score and time left to display
'''
def __init__(self):
self.score = 0
self.level = 1
self.timer = 10
def runTimer(self):
if self.timer > 0:
self.timer -= 1
else:
self.timer = 10
class Levels():
'''
Contains the commands(tasks) for each level, as well as the order of customers and the
corresponding answers.
To be instantiated by the GUI class to determine the current command and customer to display
'''
def __init__(self):
#--------------------------------------------------------------
# Initialize commands, anwers and customers for each level (hardcode for now, may be a way to
# randomize or dynamically generate challenges depending on the level design)
# Customers can have more than one attribute, so we store it as a tuple
# Answers for each customer can be more than one pizza (eg. for a while loop), so store it as a tuple
# If the statement doesn't execute, correct answer is to move on to the next customer
#---------------------------------------------------------------
# Level 1
# Action images
self.img_pepperoni = (PhotoImage(file = 'Graphics\\Actions\\pepperoniup.gif'),
PhotoImage(file = 'Graphics\\Actions\\pepperonihover.gif'),
PhotoImage(file = 'Graphics\\Actions\\pepperonidown.gif'))
self.img_hawaiian = (PhotoImage(file = 'Graphics\\Actions\\hawaiianup.gif'),
PhotoImage(file = 'Graphics\\Actions\\hawaiianhover.gif'),
PhotoImage(file = 'Graphics\\Actions\\hawaiiandown.gif'))
self.img_vegetarian = (PhotoImage(file = 'Graphics\\Actions\\vegetarianup.gif'),
PhotoImage(file = 'Graphics\\Actions\\vegetarianhover.gif'),
PhotoImage(file = 'Graphics\\Actions\\vegetariandown.gif'))
self.img_nextcustomer = (PhotoImage(file = 'Graphics\\Actions\\nextcustomerup.gif'),
PhotoImage(file = 'Graphics\\Actions\\nextcustomerhover.gif'),
PhotoImage(file = 'Graphics\\Actions\\nextcustomerdown.gif'))
level1commands = ['if customer_is_red:\n\tserve pepperoni', 'if customer_is_blue:\n\tserve hawaiian']
level1customers = [('red'), ('green')] #make this a dict corresponding to image too?
level1answers = [('pepperoni'), ('next')]
level1actions = {'pepperoni': self.img_pepperoni , 'hawaiian':self.img_hawaiian, 'vegetarian': self.img_vegetarian,
'next': self.img_nextcustomer}
self.levelscommands = [level1commands]
self.levelscustomers = [level1customers]
self.levelsanswers = [level1answers]
self.levelsactions = [level1actions]
class Customer():
def __init__(self, **attributes):
for k,v in attributes:
self.k = v
#--------------------------------
# Animations
#--------------------------------
class Animation():
def __init__(self, container, pos, images, framerate):
self.container = container
self.x = pos[0]
self.y = pos[1]
self.images = copy(images)
self.imagescopy = copy(images)
self.framerate = framerate # ms between frames
self.done = False
def animate(self):
if len(self.images)>0:
print "animating", len(self.images), self.images
self.container.delete("currentframe") # won't throw an error if non-existant
print self.images[0], "image name"
image = PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready1.gif')
print image, "image"
self.container.create_image(self.x, self.y, image = image, tag = "currentframe")
self.animation = self.container.after(self.framerate, self.animate)
else:
print "finished"
self.container.delete("currentframe")
self.container.after_cancel(self.animation)
self.done = True
def resetAnimation(self):
self.done = False
self.images = copy(self.imagescopy)
#-------------------------
# GUI classes
#-------------------------
class PlayArea():
def __init__(self):
self.window = Tk()
self.window.title("Pizza Program!")
#------------------------------------------------------------
# Initialize player, controller, levels and customer classes
#------------------------------------------------------------
self.player = Player()
self.controller = Controller()
self.levels = Levels()
self.customer = Customer()
#---------------------
# Button and background images
#---------------------
self.img_buttonstart = (PhotoImage(file = 'Graphics\\Controller\\button_startup.gif'),
PhotoImage(file = 'Graphics\\Controller\\button_starthover.gif'),
PhotoImage(file = 'Graphics\\Controller\\button_startdown.gif'))
self.img_buttonpause = (PhotoImage(file = 'Graphics\\Controller\\button_pauseup.gif'),
PhotoImage(file = 'Graphics\\Controller\\button_pausehover.gif'),
PhotoImage(file = 'Graphics\\Controller\\button_pausedown.gif'))
self.img_buttonresume = (PhotoImage(file = 'Graphics\\Controller\\button_resumeup.gif'),
PhotoImage(file = 'Graphics\\Controller\\button_resumehover.gif'),
PhotoImage(file = 'Graphics\\Controller\\button_resumedown.gif'))
self.img_playbgplay = Image.open("Graphics\\PlayArea\\bgplay.gif")
self.img_playbgready = Image.open("Graphics\\PlayArea\\bgready.gif")
# self.imgs_playbgready = [PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready1.gif'),
# PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready2.gif'),
# PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready3.gif'),
# PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready4.gif'),
# PhotoImage(file = 'Graphics\\PlayArea\\ani_bgready5.gif') ]
#-----------------------
# Animations
#-----------------------
self.imgs_playbgready = []
self.imgs_startserving = []
for i in range(1,4):# change later
self.imgs_playbgready.append('Graphics\\PlayArea\\ani_bgready'+str(i)+'.gif')
self.imgs_startserving.append('Graphics\\PlayArea\\ani_startserving'+str(i)+'.gif')
#self.imgs_playbgready = []
#for filename in imgs_playbgready:
# self.imgs_playbgready.append(PhotoImage(file = filename))
#--------------
# Font styles
#--------------
self.controlFont = tkFont.Font(family = "Calibri", size = 10, weight = "bold" )
self.commandFont = tkFont.Font(family = "Courier New", size = 14, weight = "bold")
#-------------------------------------------------------------------------------------------
# Frames to contain the play area (graphics), the command area (what players must do),
# the controller area (start button, timer, score), and action area (possible actions players can take)
#-------------------------------------------------------------------------------------------
self.playFrame = Frame(self.window, bd = 0, highlightthickness = 0)
self.commandFrame = Frame(self.window, bd =0, highlightthickness =0)
self.controlFrame = Frame(self.window, bd =0, highlightthickness =0)
self.actionsFrame = Frame(self.window, bd =0, highlightthickness =0)
self.commandFrame.grid(column = 1, row = 1)
self.playFrame.grid(column = 1, row = 2)
self.controlFrame.grid(column = 2, row = 1)
self.actionsFrame.grid(column = 2, row = 2)
self.actionsFrame.pack_propagate(False)
#self.actionsFrame.columnconfigure(1,weight=1)
#self.window.columnconfigure(2, weight =1)
#-----------------------------------
# Play Area Elements
#-----------------------------------
self.playWidth = 500
self.playHeight = 500
self.playCanvas = Canvas(self.playFrame, width = self.playWidth, height = self.playHeight, bd = -2)
self.playCanvas.pack()
resized = self.img_playbgplay.resize((self.playWidth, self.playHeight), Image.ANTIALIAS)
self.img_playbgplay = ImageTk.PhotoImage(resized)
resized = self.img_playbgready.resize((self.playWidth, self.playHeight), Image.ANTIALIAS)
self.img_playbgready = ImageTk.PhotoImage(resized)
self.playCanvas.create_image(self.playWidth/2, self.playHeight/2, image = self.img_playbgplay)
self.playCanvas.create_image(self.playWidth/2, self.playHeight/2, image = self.img_playbgready,tag = "bgready")
# Bgready animation for when start is pressed
self.ani_playbgready = Animation(self.playCanvas,(self.playWidth/2, self.playHeight/2),self.imgs_playbgready, 5000)
self.ani_startserving = Animation(self.playCanvas,(self.playWidth/2, self.playHeight/2), self.imgs_playbgready,5000)
# Controller elements
# timer and score labels only appear after game starts
self.controlColor = "sky blue"
self.controlWidth = 200
self.controlHeight = 200
self.controllerCanvas = Canvas(self.controlFrame, width = self.controlWidth, height = self.controlHeight, bg = self.controlColor,
bd =0 , highlightthickness =0)
# Frame for containing button, timer and score
self.controlLabelFrame = LabelFrame(self.controllerCanvas, bd = 0, highlightthickness = 0, relief = "ridge", bg = self.controlColor)
#print self.controlLabelFrame.config(), "configs for controlLabelFrame"
self.btStart = Button(self.controlLabelFrame, command = self.startGame, highlightthickness =0, bd =0,
bg = self.controlColor, image = self.img_buttonstart[0])
self.btPause = Button(self.controlLabelFrame, command = self.pauseGame, highlightthickness =0, bd =0,
bg = self.controlColor, image = self.img_buttonpause[0])
self.btResume = Button(self.controlLabelFrame, command = self.resumeGame, highlightthickness =0, bd =0,
bg = self.controlColor, image = self.img_buttonstart[0])
self.controllerCanvas.pack()
self.btStart.grid(row=1, pady = 5)
self.controllerCanvas.create_window(self.controlWidth/2, self.controlHeight/2, window = self.controlLabelFrame)
#Toggle button image
self.changeButtonImage(self.btStart, self.img_buttonstart)
# Timer label
self.timerText = StringVar()
self.timerLabel = Label(self.controlLabelFrame, textvariable = self.timerText, bg = self.controlColor, font = self.controlFont)
self.timerLabel.grid( row = 2, pady=5)
# Level label
self.levelText = StringVar()
self.levelLabel = Label(self.controlLabelFrame, textvariable = self.levelText, bg = self.controlColor, font = self.controlFont)
self.levelLabel.grid( row = 3, pady=5)
# Command elements
self.commandColor = "light sky blue"
self.commandWidth = 500
self.commandHeight = 200
self.commandCanvas = Canvas(self.commandFrame, width = self.commandWidth, height = self.commandHeight, bg = self.commandColor)
self.commandCanvas.pack()
self.commText = StringVar()
self.commLabel = Label(self.commandCanvas, textvariable = self.commText, bg = self.commandColor, font = self.commandFont)
self.commandCanvas.create_window(self.commandWidth/2, self.commandHeight/2, window = self.commLabel)
# Action elements
self.actionColor = "cornflower blue"
self.actionWidth = 200
self.actionHeight = 500
self.actionsFrame.config(width = self.actionWidth, height = self.actionHeight, bg = self.actionColor,)
#self.actionCanvas = Canvas(self.actionsFrame, width = self.actionWidth, height = self.actionHeight, bg = self.actionColor, highlightthickness =0)
#self.actionCanvas.pack()
self.window.mainloop()
#-------------------------------------
# Step methods
# (for checking conditions each step)
#-------------------------------------
def checkAnimation(self, animation, triggerevent, *args):
if not animation.done:
self.window.after(100, self.checkAnimation)
else:
triggerevent(args)
#-------------------------------------
# Controller methods
#-------------------------------------
def changeButtonImage(self, button, images):
def toImage(event, button, image):
button["image"] = image
up, hover, down = images
button.bind("<Enter>", lambda event, button = button, image = hover : toImage(event, button, image))
button.bind("<Leave>", lambda event, button = button, image = up : toImage(event, button, image))
button.bind("<Button-1>", lambda event, button = button, image = down : toImage(event, button, image))
button.bind("<ButtonRelease-1>", lambda event, button = button, image = up : toImage(event, button, image))
def updateController(self):
self.timerText.set("Next Customer: " + str(self.controller.timer))
self.controller.runTimer()
self.update = self.controlFrame.after(1000, self.updateController)
def startGame(self):
#--------------------------
# Update controller elements
#-----------------------------
# Change button
self.btStart.grid_remove()
self.btPause.grid(row=1, pady = 5)
self.changeButtonImage(self.btPause, self.img_buttonpause)
# Timer label
self.timerText.set("Next Customer:" + str(self.controller.timer))
# Level label
self.levelText.set("Level: " + str(self.controller.level))
self.updateController()
#--------------------------------
# Access level commands, customers, answers, and set command display
#--------------------------------
# Commands
self.levelcommands = deepcopy(self.levels.levelscommands[self.controller.level-1]) #copy the list
self.currentcommand = self.levelcommands.pop(0)
self.displayCommand(self.currentcommand)
# Actions
self.levelactions = copy(self.levels.levelsactions[self.controller.level-1])
self.btActions = []
for action, img in self.levelactions.iteritems():
if action != "next":
self.btActions.append(Button(self.actionsFrame, command = self.servePizza,
highlightthickness =0, bd =0, bg = self.actionColor, image = img[0]))
else:
self.btActions.append(Button(self.actionsFrame, command = self.nextCustomer,
highlightthickness =0, bd =0, bg = self.actionColor, image= img[0]))
index = self.btActions.index(self.btActions[-1]) +1
self.btActions[-1].pack(pady = 35)
self.changeButtonImage(self.btActions[-1], img)
# Play Area
self.playCanvas.delete("bgready")
self.ani_playbgready.animate()
#self.checkAnimation(self.ani_playbgready, self.ani_startserving.animate)
self.levelcustomers = deepcopy(self.levels.levelscustomers[self.controller.level-1]) #copy the list
self.currentcustomer = self.levelcustomers.pop(0)
#self.displayCustomer(self.currentcustomer) #need to write this method
# Answers
self.levelanswers = self.levels.levelsanswers[self.controller.level-1][:] # no nested elements
self.currentanswer = self.levelanswers.pop(0)
def pauseGame(self):
#Pause controller updating
# Change button
self.btPause.grid_remove()
self.btResume.grid(row=1, pady = 5)
self.changeButtonImage(self.btResume, self.img_buttonresume)
self.controlFrame.after_cancel(self.update)
def resumeGame(self):
# Resume controller updating
# Change button
self.btResume.grid_remove()
self.btPause.grid(row=1, pady = 5)
self.changeButtonImage(self.btResume, self.img_buttonpause)
self.updateController()
def readyLevel(self):
pass
#---------------------------------
# Command methods
#---------------------------------
def updateCommand(self):
pass
def displayCommand(self, command):
self.commText.set(command)
#--------------------------------
# Action methods
#--------------------------------
def servePizza(self):
pass
def nextCustomer(self):
pass
PlayArea()

Categories