Updating image in tkinter window with newly made images - python-3.x

I have written a program that simulate orbits of planets around a star, I would like a visualiser to display current planet positions around the star. Since all the planets have their x and y co-ords stored, plotting them on an image is straightforward, but I don't know a good way to update the image and re-display after each simulation step.
Currently I draw the image like so (use the planet position to turn a pixel red)
def createwindow():
img = Image.new( 'RGB', (750,730), "black")
pixels = img.load()
for thing in celestials:
pixels[thing.xpos+375+sun.xpos,thing.ypos+375+sun.xpos]=250,20,20
So I have an image which displays the planets quite well, which can be remade after each time the planets move. But how do I display this in a tkinter window?
Is there a way of doing this without using a Photoimage which must be saved and loaded? Or a better way of doing this all together? Maybe assigning each planet a label and drawing them directly on a tkinter black window, updating the position of the label with each simulation step?
All help appreciated

You should probably draw the animation on a canvas, instead of on a static image; You can then move the elements of the animation that are modified at each step of the simulation.
Something along those lines: (press start at the bottom to start)
import tkinter as tk
def animate():
canvas.move(1, 5, 0) # 1 refers to the object to be moved ID, dx=5, dy=0
root.update()
root.after(50, animate)
if __name__ == '__main__':
root = tk.Tk()
frame = tk.Frame(root)
canvas = tk.Canvas(root, width=800, height=400)
canvas.pack()
canvas.create_polygon(10, 10, 50, 10, 50, 50, 10, 50) # canvas object ID created here by tkinter
btn = tk.Button(root, text='start', command=animate)
btn.pack()
root.mainloop()

There're many ways to display dynamic content, like moving planets. It depends on whether you prefer working with widgets, pixels, images or graphical primitives.
Widgets
If you prefer to move widgets, as you said, you can assign each planet to a Label. Then you can place it on a window using Place Geometry Manager:
yourLabel.place(anchor=CENTER, relx=0.5, rely=0.5, x=-100, y=50)
That'd put a center of the label to coords (-100,50) relative to its parent center (relx=0.5, rely=0.5). To change the position call .place() again with different (x,y) coords.
Pixels
To change image pixels at runtime you can use .blank(), .get() and .put() methods of PhotoImage. They're not in TkInter book, but you can see them in help(PhotoImage) in python console. To set a single pixel (x,y) to color #fa1414 use:
yourPhotoImage.put("{#fa1414}", to=(x,y))
The "{#fa1414}" argument here is actually a pixels list. Tk can put a rectangular block of pixels in one call. Colors are specified row-by-row, separated by spaces, each row enclosed in curly braces. Block of NxM colors would look like "{C0,0 C1,0 ... CN-1,0} {C0,1 C1,1 ... CN-1,1} ... {C0,M-1 C1,M-1 ... CN-1,M-1}". For example, to put 2x2 pixels to coords (4,6) use:
yourPhotoImage.put("{red green} {#7f007f blue}", to=(4,6))
Tk recognizes many symbolic color names in addition to #rrggbb syntax.
Note that all the changes are displayed right away. You don't need to manually "re-display" images. E.g. if you have a Label(image=yourPhotoImage) defined somewhere, it'd display the changes after you modify yourPhotoImage.
Images
Also PhotoImage can load images from base64-encoded strings. So, technically, to skip saving/loading a file, you can load your image from a string, containing base64-encoded .gif or .png (formats supported natively in Tk):
newimg = PhotoImage(data="R0lGODlhHgAUAPAAAP//AAAAACH5BAAAAAAALAAAAAAeABQAAAIXhI+py+0Po5y02ouz3rz7D4biSJZmUgAAOw==")
.put() also supports base64-strings. For example, if you have planets as images, loaded into base64-strings, you can put them into your image with:
yourPhotoImage.put(base64_mars_image_string, to=(100,50))
Finally, if you're using PIL anyway, you can create PIL.ImageTk.PhotoImage from your PIL.Image img and use it everywhere Tkinter accepts an image object[ref]
from PIL import ImageTk
photoimg = ImageTk.PhotoImage(img)
Graphical primitives
If you want lines or arcs you can also use Canvas. It provides a way to put together different widgets, images, texts, graphical primitives, and move them around, or configure interaction with them. A canvas-based planetary system could look like this (python2 syntax):
from Tkinter import *
import math, time
color = ["#fffc00", "#a09991", "#b6b697", "#2c81ca", "#a36447", "#b16811", "#e5ca9d", "#bcdaf2", "#7e96bc", "#d3ac8b"]
size = [20, 4, 7, 7, 5, 15, 13, 9, 9, 4]
start_time = time.time()
def update_positions():
dt = (time.time() - start_time)*5
for i in range(len(planets)):
px, py = 350+35*i*math.cos(dt/(i+1)/size[i]+4*i), 350+35*i*math.sin(dt/(i+1)/size[i]+4*i)
canvas.coords(planets[i], (px-size[i],py-size[i],px+size[i],py+size[i]))
root.after(100, update_positions)
root = Tk()
canvas = Canvas(root, width=700, height=700, bg="black")
arcs = [canvas.create_oval(350-35*i, 350-35*i, 350+35*i, 350+35*i, outline=color[i]) for i in range(len(color))]
planets = [canvas.create_oval(350, 350, 350, 350, fill=color[i]) for i in range(len(color))]
update_positions()
canvas.pack()
root.mainloop()
Note that items created on the canvas are kept until you remove them. If you want to change the drawing, you can either use methods like .coords(), .itemconfig(), and .move() to modify the items, or use .delete() to remove them.

Related

Improving performance on a tkinter scrollable canvas with many widgets inside

I am trying to create a scrollable canvas in tkinter, filled with a grid of smaller square canvases. I have no problem setting this up, but the result is very slow when scrolling. Any advice on how to speed things up? I have considered, but have yet to try to implement:
Using pages of many widgets rather than one scrollable canvas. This would solve the problem, but I'd like to use a scrolling solution if possible.
When scrolling, have the canvas "snap" to the nearest row of squares. The scrolling does not need to be continuous, e.g. we don't need to see half a square at a time.
Change the canvas to only contain the squares that are currently being viewed. All windows that aren't being viewed can be destroyed and created again later. I am unclear if this approach will even help, but a similar approach worked well for a large image on a different project I've worked on.
It could be possible that this lag is just a hardware issue on my end. Unfortunately, I'm not sure how to test this.
Here is the reprex, any help is appreciated! Thank you.
import tkinter as tk
import numpy as np
root = tk.Tk()
outside = tk.Frame(root, bg='green')
outside.pack(expand=True, fill='both')
def get_box(): # box is 16x16 pixels
colors = ['red', 'blue', 'green', 'yellow']
color = np.random.choice(colors, 1)[0]
ret = tk.Canvas(canvas, bg=color, width=16, height=16, highlightthickness=0)
return ret
canvas = tk.Canvas(outside, width=16*30, bg='gray20', highlightthickness=0). # 30 columns
canvas.pack(fill='y', expand=True, side='left')
scroll_y = tk.Scrollbar(outside, orient="vertical", command=canvas.yview) # Create scrollbar
canvas.configure(scrollregion=canvas.bbox('all'), yscrollcommand=scroll_y.set)
scroll_y.pack(fill='y', side='right', expand=True)
for i in range(600): # create many boxes. ideally, ~4000 once things are running smoothly
box = get_box()
canvas.create_window(16 * (i % 30), 16 * (i //30), anchor='nw', window=box)
canvas.update_idletasks()
canvas.configure(scrollregion=canvas.bbox('all'), yscrollcommand=scroll_y.set)
root.mainloop()

my attempt to find the dimensions of a widget results in bbox returning (0,0,0,0)

I am trying to construct a screen consisting of a variable number of image only radio buttons so the user can choose an image. I want to use a vertical scrollbar and understand a key part of coding the scrollbar is setting the scrollregion found by using bbox. I can't get bbox to produce anything other than (0,0,0,0)
I have reduced the code as much as I am able
from tkinter import *
from PIL import ImageTk,Image
root = Tk()
def display():
canvas.grid()
image=Image.open("c:/Python/Art Images/Thumbnails/0001#Ground Swell.jpg")
thumbnail = ImageTk.PhotoImage(image)
thumbnail_button = Radiobutton(canvas,indicatoron=0,image=thumbnail)
thumbnail_button.image=thumbnail #keep a reference to avoid garbage collection
thumbnail_button.grid()
print(root.bbox("all"))
canvas=Canvas(root)
display()
root.mainloop()

Is there a function to display an image in tkinter (that is any file type) and display it as a specific x-y position on the canvas

I have an array of image paths and a function that finds the paths connected to buttons that are where I want to display the image
I have already tried adding
file_path = photo.name
img = Image.open(file_path)
photo_image = ImageTk.PhotoImage(img)
tk.Label(window, image=photo_image).pack(side=tk.TOP)
to my function but the photos end up being too big and I want the pictures to be placed right above the buttons so I know the exact x-y coordinates I want the pictures to be I just need a way to get them there and to make them smaller
To resize your image, pass a tuple of size in pixels:
img = Image.open("1.bmp").resize((640, 480))
To place your image right above the buttons, you can use place.
tk.Label(root, image=photo_image).place(x=0,y=0) #change x and y to your desired value
You can also get the current loc of your button widget to calculate the exact location on where you need to place your label:
x,y = widget.winfo_x(), widget.winfo_y()
To sum it up:
import tkinter as tk
from PIL import Image, ImageTk
root = tk.Tk()
root.geometry("500x500")
def place_image():
x, y = button1.winfo_x(), button1.winfo_y()
tk.Label(root, image=photo_image).place(x=x,y=y+20)
button1 = tk.Button(root,text="Click me",command=place_image)
button1.pack()
img = Image.open("1.bmp").resize((400, 300))
photo_image = ImageTk.PhotoImage(img)
root.mainloop()
You can place the label at specific pixel coordinates within the canvas using .place(). You can also define the label's width and height in pixels, inside the brackets. This will therefore define the width and height in pixels, of the image.
E.g:
tk.Label(window, image=photo_image).place(x=300, y=400, width=200, height=50)
place() Pixel coordinates work like this in tkinter (for a 600 pixel by 600 pixel canvas):
Increasing a tkinter widget's X coordinate moves it rightwards.
Increasing a tkinter widget's Y coordinate moves it downwards.

tkinter textbox resizes instead of wrapping text to fill box

def red():
frame3.output_display.config(fg = 'red', font=root.customFont1)
def blue():
frame3.output_display.config(fg = 'darkblue', font=root.customFont2)
def green():
frame3.output_display.config(fg = 'darkgreen',font=root.customFont3)
def black():
frame3.output_display.config(fg = 'black',font=root.customFont4)
from tkinter import *
from tkinter import ttk
import tkinter.font
from tkinter.scrolledtext import ScrolledText
root = Tk()
root.title("Change Text")
root.geometry('700x500')
# change font size and family: not used currently because of resizing issue
root.customFont1 = tkinter.font.Font(family="Handwriting-Dakota", size=12)
root.customFont2 = tkinter.font.Font(family="Comic sans MS", size=14)
root.customFont3 = tkinter.font.Font(family="Script MT", size=16)
root.customFont4 = tkinter.font.Font(family="Courier", size=10)
# FRAME 3
frame3 = LabelFrame(root, background = '#EBFFFF', borderwidth = 2, text = 'text entry and display frame', fg = 'purple',bd = 2, relief = FLAT, width = 75, height = 40)
frame3.grid(column = 2, row = 0, columnspan = 3, rowspan = 6, sticky = N+S+E+W)
#frame3.grid_rowconfigure(0, weight=0)
#frame3.grid_columnconfigure(0, weight=0)
frame3.grid_propagate(True)
frame3.output_display = ScrolledText(frame3, wrap = WORD)
frame3.output_display.pack( side = TOP, fill = BOTH, expand = True )
frame3.output_display.insert('1.0', 'the text should appear here and should wrap at character forty five', END)
#frame3.output_display.config(state=DISABLED) # could be used to prevent modification to text (but also prevents load new file)
# draws all of the buttons,
ttk.Style().configure("TButton", padding=6, relief="flat",background="#A52A2A", foreground='#660066')
names_colour=(('Red',red),('Blue',blue),('Green',green),('Black',black))
root.button=[]
for i,(name, colour) in enumerate(names_colour):
root.button.append(ttk.Button(root, text=name, command = colour))
row,col=divmod(i,4)
root.button[i].grid(sticky=N+S+E+W, row=6, column=col, padx=1, pady=1)
root.mainloop()
In the GUI when the text font face and font size is changed, the textbox resizes and obscures the buttons. In my naivety I thought that the textbox would remain the same size and the text would simply wrap within the constraints of the textbox. At least taht is what I would like to achieve. Obviously there is some concept in font size or in textbox , tkinter that I do not understand
Thanks
The width of the text widget is defined in units of character widths rather than pixels, and it tries to use its configured width as its minimum width whenever possible. The widget will be wider for wider fonts, and narrower for narrow fonts. Thus, if you give it a wide font it will try to make itself wider to remain X characters wide.
So, how do you solve this?
One solution is to set the width and height to something small. For example, if you set the width and height to 1 (one), the widget will only ever try to force itself to be one character wide and tall. Unless you're using absolutely huge fonts, you'll barely see the widget grow as you enlarge the font.
Then you will need to rely on the pack, grid, or place algorithm to stretch the widget to the desired dimensions. If you're using grid, this usually means you need to make sure that column and row weights are set appropriately, along with setting the sticky attribute.
The downside to this is that you have to make sure your GUI has the right size, rather than depending on it just magically happening based on the preferred size of each widget.
As a quick hack, you can see this in your program by adding these lines after the widgets have been created:
frame3.output_display.configure(width=1, height=1)
root.grid_rowconfigure(0, weight=1)
root.grid_columnconfigure(2, weight=1)
When I run your code with the above additional lines, the text widget remains a fixed size and the text wraps at different places with each font.

How to resize an image using tkinter?

Is it possible to resize an image using tkinter only? If so, how can that be done?
You can resize a PhotoImage using the zoom and subsample methods. Both methods return a new PhotoImage object.
from tkinter import *
root = Tk() #you must create an instance of Tk() first
image = PhotoImage(file='path/to/image.gif')
larger_image = image.zoom(2, 2) #create a new image twice as large as the original
smaller_image = image.subsample(2, 2) #create a new image half as large as the original
However, both of these methods can only take integer values as arguments, so the functionality is limited.
It is possible to scale by decimal values but it is slow and loses quality. The below code demonstrates scaling by 1.5x:
new_image = image.zoom(3, 3) #this new image is 3x the original
new_image = new_image.subsample(2, 2) #halve the size, it is now 1.5x the original
from PIL import Image
img = Image.open("flower.png")
img = img.resize((34, 26), Image.ANTIALIAS)
For further information, go to http://effbot.org/imagingbook/image.htm
Here is a way to resize images (PhotoImages) using just tkinter. Here is a simple function that may suit your needs. This is a rudimentary function that reads the image pixel by pixel simply scaling from one image to another. It may be slow, but depending on your needs, it may suit you well. (In this context/discussion when I refer to an image, I am referring to a PhotoImage or instance of PhotoImage.)
Basically, it takes three arguments:
your image (an instance of a PhotoImage)
your desired width in pixels
your desired height in pixels
It returns a new instance of a PhotoImage.
If you want, you can reference your original image to the returned image effectively resizing your image. Or you can just retrieve a new image.
Here is the function and some examples:
from tkinter import *
def resizeImage(img, newWidth, newHeight):
oldWidth = img.width()
oldHeight = img.height()
newPhotoImage = PhotoImage(width=newWidth, height=newHeight)
for x in range(newWidth):
for y in range(newHeight):
xOld = int(x*oldWidth/newWidth)
yOld = int(y*oldHeight/newHeight)
rgb = '#%02x%02x%02x' % img.get(xOld, yOld)
newPhotoImage.put(rgb, (x, y))
return newPhotoImage
That function should resize an image, pixel by pixel and return a new image.
Basically in a nutshell, you create an image of the desired size and must fill it in pixel by pixel with your desired colors using data from the original image. You can think of this process maybe using a line (y=mx+b where b=0) or ratio or scale factor or however you want to think about it. Bottom line is you have to fill in the new pixel data by retrieving data from the original image.
To change the size of your image you could do something like this. Here would be example code:
from tkinter import *
#insert the resize function here
root = Tk()
myCanvas = Canvas(root, width=300, height=300)
myCanvas.pack()
puppyImage = PhotoImage(file="bassethound.png") # a 200px x 200px image
puppyImage = resizeImage(puppyImage, 150, 150) # resized to 150px x 150px
myCanvas.create_image(50, 50, anchor=NW, image=puppyImage)
myCanvas.create_text(0, 0, anchor=NW, text="original 200x200\nnow150x150")
root.mainloop()
And here is the result:
And here is the 200x200 image, shrunk to 100x100 and expanded to 300x300 with this code:
from tkinter import *
#insert the resize function here
root = Tk()
myCanvas = Canvas(root, width=600, height=300)
myCanvas.pack()
puppyImage = PhotoImage(file="bassethound.png") # a 200px x 200px image
puppySmall = resizeImage(puppyImage, 100, 100)
puppyLarge = resizeImage(puppyImage, 300, 300)
myCanvas.create_image(0, 0, anchor=NW, image=puppyImage)
myCanvas.create_text(0, 0, anchor=NW, text="original 200x200")
myCanvas.create_image(200, 0, anchor=NW, image=puppySmall)
myCanvas.create_text(200, 0, anchor=NW, text="small 100x100")
myCanvas.create_image(300, 0, anchor=NW, image=puppyLarge)
myCanvas.create_text(300, 0, anchor=NW, text="larger 300x300")
root.mainloop()
And here is the result of that code:
Here are just some arbitrary numbers, say 273px and 88px:
from tkinter import *
# put resize function here
root = Tk()
myCanvas = Canvas(root, width=400, height=100)
myCanvas.pack()
puppyImage = PhotoImage(file="bassethound.png") # a 200px x 200px image
puppyImage = resizeImage(puppyImage, 273, 88) # resized to 273px x 88px
myCanvas.create_image(0, 0, anchor=NW, image=puppyImage)
root.mainloop()
and the result:
This answer inspired by acw1668 and roninpawn at the following link:
How to rotate an image on a canvas without using PIL?
photo attribution:
n nlhyeyyeusysAnderson Nascimento, CC BY 2.0 https://creativecommons.org/licenses/by/2.0, via Wikimedia Commons
from: https://en.wikipedia.org/wiki/Puppy
Just in case anyone comes across this for future reference, as I was looking for this myself earlier. You can use tkinter's PhotoImage => subsample method
I wouldn't say it really resizes in a certain sense but if you look up the documentation it returns the same image but skips X amount of pixels specified in the method.
ie:
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, ....)
canvas_image = tk.PhotoImage(file = path to some image)
#Resizing
canvas_image = canvas_image.subsample(2, 2) #See below for more:
#Shrinks the image by a factor of 2 effectively
canvas.create_image(0, 0, image = canvas_image, anchor = "nw")
self.canvas_image = canvas_image #or however you want to store a refernece so it's not collected as garbage in memory
So say our original image was 400x400 it is now effectively at 200x200. This is what I've been using when I need to compile a game or something I made and don't want to deal with PIL and it's compiling issues.
However, other than the above reason I'd just use PIL.
As far as I know (and it's been a while since I've touched Tkinter), it's a GUI toolkit. The closest it comes to "images" is the PhotoImage class which allows you to load them up and use them in your GUIs. If you want to edit/alter an image, I think you'd be better of using the Python imaging library (PIL).

Resources