Improving performance on a tkinter scrollable canvas with many widgets inside - python-3.x

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()

Related

Get height/width of tkinter Canvas

I am searching for this for ages now. Is it so hard to get the height/width of a Canvas in tkinter?
I want to do something like this:
c = Tk.Canvas(self, heigth=12, width=12)
c.create_oval(0, 0, self.height, self.width)
So that I draw a circle with the circumference of the width/height of my canvas.
How come I can't find any attribute like width/height of that canvas?
c.winfo_width and c.winfo_height wouldn't work, because that only gives me error.
Can you help me? This is really irritating, since even in the constructor there is the attribute heightand width...
Use winfo_reqwidth and winfo_reqheight:
root = tk.Tk()
c = tk.Canvas(root, height=120, width=120)
c.create_oval(0, 0, c.winfo_reqheight(), c.winfo_reqwidth())
c.pack()
root.mainloop()
You have to call <tkinter.Tk>.update and <tkinter.Canvas>.pack before the winfo_height like this:
import tkinter as tk
root = tk.Tk()
c = tk.Canvas(root, height=120, width=120)
c.pack()
root.update()
c.create_oval(0, 0, c.winfo_height(), c.winfo_width())
root.mainloop()
Also parts of this code are stolen from #hussic.
<tkinter.Tk>.update makes sure that all tkinter tasks are done. Those tasks can be things like making sure that the geometry manager was reserved the space for the widgets and the widgets are drawn on the screen. Calling <tkinter.Widget>.update where Widget can be any tkinter widget is the same as calling <tkinter.Tk>.update as it will call the same tcl function.

update tkinter without any button moved

Before Update:
After update:
My code for the update:
def plot(data,the_main):
fig = Figure(figsize=(5,5),dpi=100)
plot1 = fig.add_subplot()
plot1.plot(data)
canvas = FigureCanvasTkAgg(fig,master = the_main)
canvas.draw()
# placing the canvas on the Tkinter window
canvas.get_tk_widget().grid(row=5,column=6)
# creating the Matplotlib toolbar
# toolbar = NavigationToolbar2Tk(canvas,the_main)
# toolbar.update()
My code before accessing the update(it's related cause the window):
def buttons_for_month(country):
the_button = tkinter.Tk()
the_button.title(country)
the_button.geometry('967x590')
month_list = [1,2,3,4,5,6,7,8,9,10,11,12]
columns = 0
for i in range(0,len(month_list)):
name = "button",i
name = tkinter.Button(master = the_button,
command = lambda: plot([1,2,3,4,5],the_button),
height = 2,
width=10,
text=month_list[i])
name.grid(row=0,column=columns)
columns += 1
I'm trying to embedding matplotlib into tkinter
It's working , but not as i expected.
I've tried just make a new window , special for the graph ..However , My desktop screen were a messed.
so my question: How to manipulate my code to make matplotlib embedded into tkinter while
keep the button(s) at their places?
I can't run your code, but I'm guessing this will do it:
canvas.get_tk_widget().grid(row=5,column=6, columnspan=7)
The reason this works is because you are putting the plot inside a single column. Every column has a uniform width so it forces column 6 to be as wide as the plot. By having the plot span multiple columns, it prevents the single column from having to be resized to fit the entire plot.
Arguably, an even better solution is to put your buttons in a frame, pack that frame at the top, and then pack the plot below it. grid is very useful when creating a matrix of widgets, but in your case you're not doing that.

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()

aligning stuff with pack() tkinter

I know you can align a button to the left with pack(side="left"), and you can also do right top and bottom, but how would i align stuff on a new line just below a set of buttons above it?
I tried using grid but no matter how much i googled it I couldn't find out how to fix it.
What you'd want to do is create different frames, each with their own positioning logic. Take a look at this:
import tkinter
root = tkinter.Tk()
frame = tkinter.Frame(root)
frame.pack()
bottomframe = tkinter.Frame(root)
bottomframe.pack(side=tkinter.BOTTOM)
tkinter.Button(frame, text="left").pack(side=tkinter.LEFT)
tkinter.Button(frame, text="middle").pack(side=tkinter.LEFT)
tkinter.Button(frame, text="right").pack(side=tkinter.LEFT)
# what you want
tkinter.Button(bottomframe, text="top").pack(side=tkinter.TOP)
tkinter.Button(bottomframe, text="under").pack(side=tkinter.TOP)
root.mainloop()

Updating image in tkinter window with newly made images

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.

Resources