Tkinter Scrolling Canvas Widget speed issues - python

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.

Related

Detect DPI/scaling factor in Python TkInter application

I'd like my application to be able to detect if it's running on a HiDPI screen, and if so, scale itself up so as to be usable. As said in this question, I know I need to set a scaling factor, and that this factor should be my DPI divided by 72; my trouble is in getting my DPI. Here's what I have:
def get_dpi(window):
MM_TO_IN = 1/25.4
pxw = window.master.winfo_screenwidth()
inw = window.master.winfo_screenmmwidth() * MM_TO_IN
return pxw/inw
root = Tk()
root.tk.call('tk', 'scaling', get_dpi(root)/72)
This doesn't work (testing on my 4k laptop screen). Upon further inspection, I realized get_dpi() was returning 96.0, and that winfo_screenmmwidth() was returning 1016! (Thankfully, my laptop is not over a meter wide).
I assume that TkInter is here calculating the width in mm from some internally-detected DPI, wrongly detected as 96, but I'm not sure where it's getting this; I'm currently on Linux, and xrdb -query returns a DPI of 196, so it's not getting the DPI from the X server.
Does anyone know a cross-platform way to get my screen DPI, or to make TkInter be able to get it properly? Or, more to the point: how can I make TkInter play nice with HiDPI screens and also work fine on normal ones? Thanks!
This answer is from this link and left as a comment above, but it took hours of searching to find. I have not had any issues with it yet, but please let me know if it does not work on your system!
import tkinter
root = tkinter.Tk()
dpi = root.winfo_fpixels('1i')
The documentation for this says:
winfo_fpixels(number)
# Return the number of pixels for the given distance NUMBER (e.g. "3c") as float
A distance number is a digit followed by a unit, so 3c means 3 centimeters, and the function gives the number of pixels on 3 centimeters of the screen (as found here).
So to get dpi, we ask the function for the number of pixels in 1 inch of screen ("1i").
I know I'm answering this question late, but I'd like to expand upon #Andrew Pye 's idea. You are right, GUI's with tkinter look different across different monitors with different DPI's anytime you use a 'width' or 'height' or 'pady' or anything that is measured in pixels. I noticed this when I made a GUI on my desktop, but then later ran the same GUI on my 4K laptop (The window and the widgets appeared much smaller on the laptop). This is what I did to fix it, and it worked for me.
from tkinter import *
ORIGINAL_DPI = 240.23645320197045 # This is the DPI of the computer you're making/testing the script on.
def get_dpi():
screen = Tk()
current_dpi = screen.winfo_fpixels('1i')
screen.destroy()
return current_dpi
SCALE = get_dpi()/ORIGINAL_DPI # Now this is the appropriate scale factor you were mentioning.
# Now every time you use a dimension in pixels, replace it with scaled(*pixel dimension*)
def scaled(original_width):
return round(original_width * SCALE)
if __name__ == '__main__':
root = Tk()
root.geometry(f'{scaled(500)}x{scaled(500)}') # This window now has the same size across all monitors. Notice that the scaled factor is one if the script is being run on a the same computer with ORIGINAL_DPI.
root.mainloop()
I'm using TclTk, not TkInter, and the only way I know how to do this is to work it out from the font metrics...
% font metrics Tk_DefaultFont
-ascent 30 -descent 8 -linespace 38 -fixed 0
The linespace is approximately 0.2x the DPI (currently set to 192 here)

trying to draw over sprite or change picture pyglet

I am trying to learn pyglet and practice some python coding with a questionnaire thing, but I am unable to find a way to make the background picture be removed or drawn on top of or something for 10 seconds. I am new and am lacking in a lot of the knowledge I would need, thank you for helping!
import pyglet
from pyglet.window import Window
from pyglet.window import key
from pyglet import image
import time
card1 = False
cat_image = pyglet.image.load("cat.png")
dog_image = pyglet.image.load("dog.png")
image = pyglet.image.load("backg.png")
background_sprite = pyglet.sprite.Sprite(image)
cat = pyglet.sprite.Sprite(cat_image)
dog = pyglet.sprite.Sprite(dog_image)
window = pyglet.window.Window(638, 404, "Life")
mouse_pos_x = 0
mouse_pos_y = 0
catmeme = pyglet.image.load("catmeme.png")
sprite_catmeme = pyglet.sprite.Sprite(catmeme)
#window.event
def on_draw():
window.clear()
background_sprite.draw()
card_draw1(63, 192, 385, 192)
def card1():
while time.time() < (time.time() + 10):
window.clear()
sprite_catmeme.draw()
#window.event
def card_draw1(x1, y1, x2, y2):
cat.set_position(x1, y1)
dog.set_position(x2, y2)
cat.draw()
dog.draw()
def card_draw2():
pass
#window.event
def on_mouse_press(x, y, button, modifiers):
if x > cat.x and x < (cat.x + cat.width):
if y > cat.y and y < (cat.y + cat.height):
card1()
game = True
while game:
on_draw()
pyglet.app.run()
There's a few flaws in the order and in which you do things.
I will try my best to describe them and give you a piece of code that might work better for what your need is.
I also think your description of the problem is a bit of an XY Problem which is quite common when asking for help on complex matters where you think you're close to a solution, so you're asking for help on the solution you've come up with and not the problem.
I'm assuming you want to show a "Splash screen" for 10 seconds, which happens to be your background? And then present the cat.png and dog.png ontop of it, correct?
If that's the case, here's where you probably need to change things in order for it to work:
The draw() function
It doesn't really update the screen much, it simply adds things to the graphical memory. What updates the screen is you or something telling the graphics library that you're done adding things to the screen and it's time to update everything you've .draw()'n. So the last thing you need in the loop would be window.flip() in order for the things you've drawn to actually show.
Your things might show if you try to wiggle the window around, it should trigger a re-draw of the scene because of how the internal mechanics of pyglet work..
If you don't call .flip() - odds are probable that the redraw() call will never occur - which again, is a internal mechanism of Pyglet/GL that tells the graphics card that something has been updated, we're done updating and it's time to redraw the scene.
a scene
This is the word most commonly used for what the user is seeing.
I'll probably throw this around a lot in my text, so it's good to know that this is what the user is seeing, not what you've .draw()'n or what's been deleted, it's the last current rendering of the graphics card to the monitor.
But because of how graphical buffers work we've might have removed or added content to the memory without actually drawing it yet. Keep this in mind.
The pyglet.app.run() call
This is a never ending loop in itself, so having that in a while game: loop doesn't really make sense because .run() will "hang" your entire application, any code you want to execute needs to be in def on_draw or an event that is generated from within the graphical code itself.
To better understand this, have a look at my code, i've pasted it around a couple of times here on SO over the years and it's a basic model of two custom classes that inherits the behavior of Pyglet but lets you design your own classes to behave slightly differently.
And most of the functionality is under on_??? functions, which is almost always a function used to catch Events. Pyglet has a lot of these built in, and we're going to override them with our own (but the names must be the same)
import pyglet
from pyglet.gl import *
key = pyglet.window.key
class CustomSprite(pyglet.sprite.Sprite):
def __init__(self, texture_file, x=0, y=0):
## Must load the texture as a image resource before initializing class:Sprite()
self.texture = pyglet.image.load(texture_file)
super(CustomSprite, self).__init__(self.texture)
self.x = x
self.y = y
def _draw(self):
self.draw()
class MainScreen(pyglet.window.Window):
def __init__ (self):
super(MainScreen, self).__init__(800, 600, fullscreen = False)
self.x, self.y = 0, 0
self.bg = CustomSprite('bg.jpg')
self.sprites = {}
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
elif symbol == key.C:
print('Rendering cat')
self.sprites['cat'] = CustomSprite('cat.png', x=10, y=10)
elif symbol == key.D:
self.sprites['dog'] = CustomSprite('dog.png', x=100, y=100)
def render(self):
self.clear()
self.bg.draw()
for sprite_name, sprite_obj in self.sprites.items():
sprite_obj._draw()
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
x = MainScreen()
x.run()
Now, this code is kept simple on purpose, the full code I usually paste on SO can be found at Torxed/PygletGui, the gui.py is where most of this comes from and it's the main loop.
What I do here is simply replace the Decorators by using "actual" functions inside a class. The class itself inherits the functions from a traditional pyglet.window.Window, and as soon as you name the functions the same as the inherited onces, you replace the core functionality of Window() with whatever you decide.. In this case, i mimic the same functions but add a few of my own.
on_key_press
One such example is on_key_press(), which normally just contain a pass call and does nothing, here, we check if key.C is pressed, and if so - we add a item to self.sprites.. self.sprites just so happen to be in our render() loop, anything in there will be rendered ontop of a background.
Here's the pictures I used:
(named bg.jpg, cat.png, dog.png - note the different file endings)
class:CustomSprite
CustomSprite is a very simple class designed to make your life easier at this point, nothing else. It's very limited in functionality but the little it do is awesome.
It's soul purpose is to take a file name, load it as an image and you can treat the object like a traditional pyglet.sprite.Sprite, meaning you can move it around and manipulate it in many ways.
It saves a few lines of code having to load all the images you need and as you can see in gui_classes_generic.py you can add a heap of functions that's "invisible" and normally not readily availbale to a normal sprite class.
I use this a bunch! But the code gets complicated real fast so I kept this post simple on purpose.
the flip function
Even in my class, I still need to use flip() in order to update the contents of the screen. This is because .clear() clears the window as you would expect, that also triggers a redraw of the scene.
bg.draw() might in some cases trigger a redraw if the data is big enough or if something else happens, for instance you move the window.
but calling .flip() will tell the GL backend to force a redraw.
Further optimizations
There's a thing called batched rendering, basically the graphic card is designed to take enormous ammounts of data and render it in one go, so calling .draw() on several items will only clog the CPU before the GPU even gets a chance to shine. Read more about Batched rendering and graphics! It will save you a lot of frame rates.
Another thing is to keep as little functionality as possible in the render() loop and use the event triggers as your main source of coding style.
Pyglet does a good job of being fast, especially if you only do things on event driven tasks.
Try to avoid timers, but if you really do need to use time for things, such as removing cat.png after a certain ammount of time, use the clock/time event to call a function that removes the cat. Do not try to use your own t = time() style of code unless you know where you're putting it and why. There's a good timer, I rarely use it.. But you should if you're starting off.
This has been one hell of a wall of text, I hope it educated you some what in the life of graphics and stuff. Keep going, it's a hurdle to get into this kind of stuff but it's quite rewarding once you've mastered it (I still haven't) :)

Tkinter canvas create_image and create_oval optimization

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.

tkinter winfo_screenwidth() when used with dual monitors

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.

Measuring the delay of background changing and sound playing using Timeit (python)

I need to figure out the delay between sending a command to change the background color or playing a sound and these events actually occurring using timeit.(I'm on windows, Python 2.73)
I'm doing a reaction time test where I'll record the time(using time.clock()) before either changing the background color or playing a sound. Then, when the subject presses a key I record the time again and take difference to find the reaction time.
For the sound playing, here's what I did:
import timeit
t = timeit.Timer(stmt = "winsound.PlaySound('C:\WINDOWS\media\Windows XP Error.wav', winsound.SND_FILENAME)",setup = "import winsound")
n = t.timeit(number = 100)
print n/100 -0.999
The 0.999 is the duration of the Windows XP Error.wav in seconds.
This gave me something like 56ms. I'm not sure if its reasonable and if its the right way to do it as well as should I be enabling the garbage collection or not?
For the background changing I'm having more problems. Since I'm doing the test in fullscreen mode I tried to put all of these into the setup parameter:
from Tkinter import Tk
root=Tk()
root.overrideredirect(True)
root.geometry("{0}x{1}+0+0".format(root.winfo_screenwidth(),root.winfo_screenheight()))
root.mainloop()
Even though I separate them all with ; I still get syntax errors. When I try it not in full screen
setup = 'from Tkinter import Tk; root=Tk(); root.mainloop()' the window actually opens, yet nothing happens and if I close it I see other errors.Invalid command name "."
The statement that I'm actually measuring is root.configure(background='red').
Here's an example of a way to create a multi-line setup string for use with timeit:
setup = """
import random
l1 = [random.randrange(100) for _ in xrange(100)]
l2 = [random.randrange(100) for _ in xrange(10)]
"""
Here's another tip. To get an accurate measurement, it's important to time things following this basic pattern:
time = min(timeit.repeat(statements, setup=setup, repeat=R, number=N))
With an R of at least 3 (R = 3). This takes the fastest value obtained by doing everything 3 times, which will eliminate differences due to the many other things running on your system in the background.
This doesn't answer your whole question, but may be helpful in your quest.

Categories