Background
I am trying - and succeeding - in creating a simple plot using using the Canvas object within tkinter. I am trying to use as many tools that are installed with Python3 as possible. Matplotlib and others are great, but they are pretty large installs for something that I'm trying to keep a bit smaller.
The plots are updated every 0.5s based on input from a hardware device. The previous 128 points are deleted and the current 128 points are drawn. See my most recent blog post for a couple of screenshots. I have successfully created the plots using canvas.create_oval(), but as I was running it, I heard my PC fans ramp up a bit (I have them on an aggressive thermal profile) and realized that I was using 15% of the CPU, which seemed odd.
The Problem
After running cProfile, I found that the canvas.create_oval() was taking more cumulative time than I would have expected.
After reading a bit about optimization in the tkinter canvas (there isn't much out there except 'use something else'), I came across a post that suggested that one might use an image of a dot and use canvas.create_images() instead of a canvas.create_oval(). I tried that and the time in create_image() was a bit less, but still quite significant.
For completeness, I will include the code fragment. Note that this method is part of a class called Plot4Q which is a subclass of tk.Canvas:
def plot_point(self, point, point_format=None, fill='green', tag='data_point'):
x, y = point
x /= self.x_per_pixel
y /= self.y_per_pixel
x_screen, y_screen = self.to_screen_coords(x, y)
if fill == 'blue':
self.plot.create_image((x_screen, y_screen), image=self.blue_dot, tag=tag)
else:
self.plot.create_image((x_screen, y_screen), image=self.green_dot, tag=tag)
The Profile
I am a profiling newb, so it would be prudent to include some portion of the output of that profiler. I have sorted by 'cumtime' and highlighted the relevant methods.
update_plots calls scatter
scatter calls plot_point (above)
Note that scatter consumes 11.6% of the total run time.
The Question
Is there a more efficient method of creating points (and deleting them, though that doesn't take very long in tkinter) on a canvas?
If not, is there a more efficient way of creating the plot and embedding it into the tkinter interface?
I am somewhat open to using a different library, but I would like to keep it small and fast. I had thought that the tk canvas would be small and fast since it was functioning competently on machines with 1/10th of the power that a modern PC has.
More Info
After running a helpful answer below (Brian Oakley), I have updated results.
To explain the updated code a bit, I am using ovals again (I like the color control). I check to see if the tag exists. If it does not exist, then the new oval is created at the point specified. If the tag does exist, then the new coordinate is calculated and the move function is called.
def plot_point(self, point, fill='green', tag='data_point'):
if not fill:
fill = self.DEFAULT_LINE_COLOR
point_width = 2
# find the location of the point on the canvas
x, y = point
x /= self.x_per_pixel
y /= self.y_per_pixel
x_screen, y_screen = self.to_screen_coords(x, y)
x0 = x_screen - point_width
y0 = y_screen - point_width
x1 = x_screen + point_width
y1 = y_screen + point_width
# if the tag exists, then move the point, else create the point
point_ids = self.plot.find_withtag(tag)
if point_ids != ():
point_id = point_ids[0]
location = self.plot.coords(point_id)
current_x = location[0]
current_y = location[1]
move_x = x_screen - current_x
move_y = y_screen - current_y
self.plot.move(point_id, move_x, move_y)
else:
point = self.plot.create_oval(x0,
y0,
x1,
y1,
outline=fill,
fill=fill,
tag=tag)
The improvement is only slight, 10.4% vs. 11.6%.
The canvas has performance problems when many items are created (more specifically, when new object ids are created). Deleting objects doesn't help, the problem is in the ever increasing object ids which are never reused. This problem usually doesn't appear until you have 10's of thousands of items. If you're creating 256/second, you'll start to bump into that problem in just a minute or two.
You can completely eliminate this overhead if you create 128 objects off screen once, and then simply move them around rather than destroying and recreating them.
Related
I have an application (actually a plugin for another application) that presents a GTK notebook. Each tab contains a technical drawing of an operation, with a set of SpinButtons that allow you to alter the dimensions of the operation.
If you need more context, it's here: https://forum.linuxcnc.org/41-guis/26550-lathe-macros?start=150#82743
As can be seen above, this all worked fine in GTK2. The widgets (first iteration in a GTK_Fixed, then moved to a GTK_Table) were pre-positioned and the image (a particular layer of a single SVG) was plonked in behind.
Then we updated to GTK3 (and Python 3) and it stopped working. The SVG image now appears on top of the input widgets, and they can no-longer be seen or operated.
I am perfectly happy to change the top level container[1], if that will help. But the code that used to work (and now doesn't) is:
def on_expose(self,nb,data=None):
tab_num = nb.get_current_page()
tab = nb.get_nth_page(tab_num)
cr = tab.get_property('window').cairo_create()
cr.set_operator(cairo.OPERATOR_OVER)
alloc = tab.get_allocation()
x, y, w, h = (alloc.x, alloc.y, alloc.width, alloc.height)
sw = self.svg.get_dimensions().width
sh = self.svg.get_dimensions().height
cr.translate(0, y)
cr.scale(1.0 *w / sw, 1.0*h/sh)
#TODO: gtk3 drawing works, but svg is drawn over the UI elements
self.svg.render_cairo_sub(cr = cr, id = '#layer%i' % tab_num)
[1] In fact I will probably go back to GTK_Fixed and move the elements about in the handler when the window resizes, scaled according to the original position. The GTK_Table (deprecated) version takes over 2 minutes to open in the Glade editor.
Unless there is a more elegant way to do this too?
I have a small python program that uses PyOpenGL libraries to draw to spheres. The code has the below process
draw spheres
if there is a hit, then uses the below
SELECT_BUFFER_SIZE = 512
x, y = event.x(), event.y()
# required to call this to force PyQt to read from the correct, updated buffer
viewport = glGetIntegerv(GL_VIEWPORT)
# print viewport
w = viewport[2] - viewport[0]
h = viewport[3] - viewport[1]
aspect_ratio = w / h
glSelectBuffer(SELECT_BUFFER_SIZE)
glRenderMode(GL_SELECT)
glInitNames()
glPushName(0)
glMatrixMode(GL_PROJECTION)
glPushMatrix()
glLoadIdentity()
gluPickMatrix(x, viewport[3] - y, 5, 5, viewport)
glOrtho( *** setting the schene***)
**-draw spheres using glLoadName(for each)**
glMatrixMode(GL_PROJECTION)
glPopMatrix()
glFlush()
buffer = glRenderMode(GL_RENDER)
# print buffer
for hit_record in buffer:
_, _, names = hit_record
print(names)
But when I try to print the names, although I hit only one of them on the window, both names are printed. What should be the solution for that
In short, I am trying to draw three spheres(in white color) in a Pyqt5 window using PyOpenGL with the same colors, but I am trying to make them eligible for picking. I have also the capabilities of rotating, moving, and zoom in-out. So these capabilities should not break picking when they are done.(As it is asked in the comment, it is not homework. It is a side project that I work on for the last 2 years. So this problem is only a really small portion of my code.)
I'm not too sure at all that this will help and I confess that I didn't read the actual code, and I didn't quite understood if the spheres were colliding between them or with the border itself
I once had a tiny bit similar problem that I couldn't understand for too much time. but basically a hit was printed twice because both of the objects were registering a hit, so whenever they've hit each other two hit messages were printed.
I used their ID(or place in a list or something similar)so only one of them (the one with the bigger ID) will print the message
I have no idea if that helps but it might just be it.
I created a tkinter program with a memory leak issue that originally caused it to lock up after about 40 minutes. I tried an initial fix which greatly improved performance, but it still slows down after sometime so I think there is a possible second memory leak and/or other issue that I wanted to check with the community on.
About the program: A back end script updates a data table every sixty seconds which is then pushed to a tkinter script that crafts the data table in a nice layout. (I made some example code below, its not the actual script which is much longer) Every time it is refreshed, the data table can have a different number of rows/columns. Thus, my tkinter script needs to dynamically create the table and bind a button function to each cell. I'm just getting into GUI program and choose Tkinter as the first library to test out. I wanted to use a GUI library instead of a charting library because: 1) I want to learn how to build basic GUIs and figure this would be a fun application; 2) I want to be able to click on any portion of my data table and have a window pop-up that will adjust inputs for the back-end script on its next refresh.
Evolution of my code to solve memory leak problem and potential additional memory leak: So originally, I assumed that the cells would just get erased when you write a new box over the grid position. Version 1.0 below:
import pandas as pd
import numpy as np
from tkinter import *
root = Tk()
root.configure(background='black')
#Placeholder for example code
def popupwindow():
pass
def build():
mydf = pd.DataFrame([np.arange(1, np.random.randint(3, 7)) * np.random.randint(1,10) for x in np.arange(1, np.random.randint(3, 7))])
rowindex = 1
for row in mydf.iterrows():
colindex = 1
for i in row[1]:
label = Label(root, text=str(i), width=7)
label.bind('<Button-1>', popupwindow)
label.grid(row=rowindex,column=colindex)
colindex += 1
rowindex += 1
#If grid is smaller than previous grid, remove old widgets
for label in root.grid_slaves():
if int(label.grid_info()['row']) > rowindex-1 or int(label.grid_info()['column']) > colindex-1:
label.grid_forget()
def refresh():
build()
#For purpose of example code, I made refresh rate faster than my actual program's 60 seconds
root.after(5000, refresh)
refresh()
root.mainloop()
As I found out that is not the case, and that was causing the first memory leak problem. So I created Version 2.0 which ‘forgot’ all the grid slaves before recreating the grid (See the two new lines of code in build()). This substantially improved the performance but I still see a partial slowdown in Tkinter responsiveness overtime. Although, it has brought a second issue where after 30-40 minutes, the screen will go partially or entirely black (background for my frame is black) and will stop refreshing after:
def build():
mydf = pd.DataFrame([np.arange(1, np.random.randint(3, 7)) * np.random.randint(1,10) for x in np.arange(1, np.random.randint(3, 7))])
# I ADDED THESE TWO LINES OF CODE
for label in root.grid_slaves():
label.grid_forget()
rowindex = 1
for row in mydf.iterrows():
colindex = 1
for i in row[1]:
label = Label(root, text=str(i), width=7)
label.bind('<Button-1>', popupwindow)
label.grid(row=rowindex,column=colindex)
colindex += 1
rowindex += 1
# REMOVED THESE 3 LINES OF CODE AS NOW REDUNDANT WITH ADDED CODE ABOVE
# for label in root.grid_slaves():
# if int(label.grid_info()['row']) > rowindex-1 or int(label.grid_info()['column']) > colindex-1:
# label.grid_forget()
After perusing the forums more I saw this post on Overstack (Tkinter - memory leak with canvas) , which possibly signaled that widget labels may never be garbage collected even if forgotten. Not sure if this is an accurate interpretation, but if it is then this could be another possible reason why my V2.0 slows down overtime as i'm always forgetting and never rewriting my labels. Therefore, my proposed solution for V3 would be to use an if else function to see if a label already exists at a given position, if it doesn’t create a new label, if it does exist then adjust it. My question is, is this how you would approach it? Is there another memory leak/performance issue you can see from my basic example? If you have additional proposed adjustments to my code, such as how I dynamically created the data table, feel free to provide any input/improvements! Since i'm new to programming, I am very open to different ideas and more efficient methods.
Thank you in advance for your help!
The bug isn't in grid, it's in your code. Every five seconds you are creating a new dataframe, and new labels for every row in that dataframe, and you never delete them. Instead, you just keep stacking them on top of each other.
Calling grid_forget or grid_remove only removes them from the view, the objects aren't deleted.
You need to either delete all of the old widgets on each call to refresh, or reuse the existing labels rather than creating new labels.
Therefore, my proposed solution for V3 would be to use an if else function to see if a label already exists at a given position, if it doesn’t create a new label, if it does exist then adjust it. My question is, is this how you would approach it?
Yes. That, and destroy the old widgets that are no longer being used, rather than just removing them from the grid.
With tkinter canvas, to calculate the size of the graphics I display, I normally use the function winfo_screenwidth(), and size my objects accordingly.
But when used on a system with two monitors, winfo_screenwidth() returns the combined width of both monitors -- which messes up my graphics.
How can I find out the screen width in pixels of each monitor, separately?
I have had this problem with several versions of Python 3.x and several versions of tkinter (all 8.5 or above) on a variety of Linux machines (Ubuntu and Mint).
For example, the first monitor is 1440 pixels wide. The second is 1980 pixels wide. winfo_screenwidth() returns 3360.
I need to find a way to determine the screenwidth for each monitor independently.
Thanks!
It is an old question, but still: for a cross-platform solution, you could try the screeninfo module, and get information about every monitor with:
import screeninfo
screeninfo.get_monitors()
If you need to know on which monitor one of your windows is located, you could use:
def get_monitor_from_coord(x, y):
monitors = screeninfo.get_monitors()
for m in reversed(monitors):
if m.x <= x <= m.width + m.x and m.y <= y <= m.height + m.y:
return m
return monitors[0]
# Get the screen which contains top
current_screen = get_monitor_from_coord(top.winfo_x(), top.winfo_y())
# Get the monitor's size
print current_screen.width, current_screen.height
(where top is your Tk root)
Based on this slightly different question, I would suggest the following:
t.state('zoomed')
m_1_height= t.winfo_height()
m_1_width= t.winfo_width() #this is the width you need for monitor 1
That way the window will zoom to fill one screen. The other monitor's width is just wininfo_screenwidth()-m_1_width
I also would point you to the excellent ctypes method of finding monitor sizes for windows found here. NOTE: unlike the post says, ctypes is in stdlib! No need to install anything.
I am currently remaking flappy bird in Tkinter. (I understand this is bad, I explain why at the bottom.) My issue is with the pipes, and the speeds they scroll at and the distance they are from each other. Unless something is wrong with my logic, if a start the two pipes separated from each other then move them when they get to a certain point, and place them at the same point, they should retain the gap between them. This may be better explained in code.
from tkinter import *
import random
root = Tk()
root.geometry('430x640')
root.configure(background='turquoise')
canvas = Canvas(root,width=int(435),height=int(645))
canvas.configure(background='turquoise')
canvas.pack()
x, x2 = 400, 700
y = random.randint(0,300)
y2 = random.randint(0,300)
def drawPipe():
global x,x2,y,y2
canvas.coords(pipeTop,(x,0,(x+50),y))
canvas.coords(pipeBottom,(x,640,(x+50),(y+150)))
canvas.coords(pipeTop2,(x2,0,(x2+50),y2))
canvas.coords(pipeBottom2,(x2,640,(x2+50),(y2+150)))
x -= 3
x2 -= 3
if x < -46:
x = 435
y = random.randint(5,540)
if x2 <-46:
x2 = 435
y2 = random.randint(5,540)
root.after(1,drawPipe)
pipeTop = canvas.create_rectangle(x,0,(x+50),y,fill='green')
pipeBottom = canvas.create_rectangle(x,640,x+50,y+150,fill='green')
pipeTop2 = canvas.create_rectangle(x2,0,(x2+50),y,fill='green')
pipeBottom2 = canvas.create_rectangle(x2,640,(x2+50),(y2+150),fill='green')
drawPipe()
root.mainloop()
This is not my full code, but it is the bit concerned with drawing and updating the pipes. When run, this code will show you how the pipes scrolling speed up and down. I do not understand how this is possible. All the values for the pipes are the same apart from the starting positions. Is this due to the inefficient way Tkinter uses the after method? I attempted to use threading but this produced problems when using root.bind (see my previous question). Or is it due to a logic error? Thank you in advance to anyone who can help me.
Side note: I realise I should not be making a game in tkinter, especially one that requires multiple things to be happening at once. However, I am doing this at school and the modules I would like to use (Pygame or Pyglet) cannot be downloaded just for me to make a game that has no real purpose. If I could use something other than tkinter I probably would. Thank you for your help.
Using after(1,..) you get 1000FPS (Frames Per Second) but you don't need it - use after(20, ...) to get 50 FPS.
Beside using after(1,..) your program have no time to do other things - it have no time to execute all after() so you can get different speed.
With after(1,..) I couldn't even move window.
And my CPU became hotter so fan started working faster and louder.