How to capture image (screenshot) of tkinter window on MacOS - python

I have created a GUI app in Python tkinter for analyzing data in my laboratory. There are a number of buttons, figures, and canvas widgets. It would be helpful to take a screenshot of the entire window ('root') using just a single button that saves the filename appropriately. Example using Mac's built-in "screenshot" app here.
Related questions here, here, and here, but none worked successfully. The final link was almost successful, however the image that is saved is my computer's desktop background. My computer is a Mac, MacOS Monterey 12.0.1.
'root' is the tkinter window because
root = tk.Tk()
appears at the beginning of the script, analogous to 'window' in the example here. I'm using PIL.ImageGrab in the code sample below.
This is the current code, which takes an unhelpful screenshot of my desktop background,
def screenshot():
# retrieve the time string to use as a filename
file_name = root.time_string[-6:]
full_file_name = file_name + '_summary' + '.png'
x = root.winfo_rootx() + root.winfo_x()
y = root.winfo_rooty() + root.winfo_y()
x1 = x + root.winfo_width()
y1 = y + root.winfo_height()
ImageGrab.grab().crop((x, y, x1, y1)).save(full_file_name)
I create the button like so:
screenshot_btn = tk.Button(root, text='Screenshot', command=lambda: screenshot(), font=('Verdana', 24), state=DISABLED)
And I place the button in 'root' like this:
screenshot_btn.grid(row=11, column=3)
[This is my first post at stackoverflow. I apologize in advance if I did not follow all the guidelines perfectly on the first try. Thanks for your patience.]

First, I didn't have an issue with the grab showing my desktop, but it was showing an improperly cropped image.
I have found a hacky solution. The issues appears to be with the resolution. So the dimensions need some scaling.
What I did was get the output from ImageGrab.grab().save(full_file_name) ( no cropping ) and measure the size of the desired image area in pixels. These dimensions will be called x_pixels and y_pixels.
Then I measured that same area on the actual window in screen units. I did this by bringing up the mac screenshot tool which shows the dimensions of an area. I then call these dimensions x_screen and y_screen. Then I modified your screenshot function as follows.
def screenshot():
# retrieve the time string to use as a filename
file_name = root.time_string[-6:]
full_file_name = file_name + '_summary' + '.png'
x = root.winfo_rootx()
y = root.winfo_rooty()
x1 = x + root.winfo_width()
y1 = y + root.winfo_height()
x_pixels = 337
y_pixels = 79
x_screen = 171
y_screen = 41
x = x*(x_pixels/x_screen)
y = y*(y_pixels/y_screen)
x1 = x1*(x_pixels/x_screen)
y1 = y1*(y_pixels/y_screen)
ImageGrab.grab().crop((x, y, x1, y1)).save(full_file_name)
Notice I also removed +root.winfo_x() and +root.winfo_y()
The result is shown below. It's not perfect, but I believe if I more carefully measured the pixels at the bounds of the screenshot and window the scaling would be improved.

Do you by chance have a "Retina" monitor? ImageGrab fails to take into account the 144 DPI image it collects from the Mac's screencapture command. You can compensate for this by multiplying all the x, y, x1, y1 values by 2 (but you may still miss a bit if you need to account for the Mac window titlebar).
Alternatively, use the pyscreenshot package (or steal its code...). It uses the "-R" option of screencapture to get the proper bounds for the image. Works on extended monitors like mine where the x co-ord is < 0 ('cause it's to the left of the main monitor).

Related

Mouse Coordinates in Graph Window

I'm trying to figure out how to find the mouse coordinates when clicking on the graph window a few times.
So far I've tried
mx ,my = win.mouseX(), win.mouseY() and it tells me that the Nonetype is not callable. I've seen other posts involving tkinter, but I am not using that library even though I see that it's easier. Some more example code is as follows:
from graphics import *
win = GraphWin("test", 300, 300)
for i in range(3):
win.getMouse()
mx, my = win.mouseX(), win.mouseY()
print(mx,my)
I want the above code to have the user click on the window and print the regarding mouse coordinates. Eventually I want to store these coordinates, but I think I can figure that out.
win.getMouse() returns a Point which you can get coordinates from like this:
from graphics import *
win = GraphWin("test", 300, 300)
for i in range(3):
point = win.getMouse()
mx, my = point.getX(), point.getY()
print(mx,my)

Python SFML window goes dark after a few seconds

So I'm learning Python and wanted to try some graphics, so I'm giving SFML a try. I wrote the following program and everything seems good, but the window goes black and white after about 6 seconds (but it keeps drawing the sprite). Am I missing something that is causing the window to go "inactive"?
from sfml import sf
from math import *
texture = sf.Texture.from_file('gum.png')
sprite = sf.Sprite(texture)
i = 0
w = sf.RenderWindow(sf.VideoMode(1024, 768), "Sprite Test")
w.clear()
w.active = True
while w.is_open:
i += .1
if i == 180:
i = 0
#w.clear()
sprite.position = (cos(i) * i + 500, sin(i) * i + 350)
w.draw(sprite)
w.display()
You'll have to poll events. Otherwise the window won't respond to your window manager and be considered unresponsive (typically drawn in a different way, e.g. darkened or brighter).
I've never used the SFML bindings (so this might include bugs), but you'll most likely need something like this:
while w.is_open:
while w.poll_event(e):
# handle events here
i += .1
# Here follows your code as-is
w.display()

Crop Cairo-Generated PDF

I have the following script which generates a number of circles in a box on top of a bigger circle. The output is to a PDF, which I would like to be tightly bounded to the ink extents.
#!/usr/bin/env python
import math
import cairocffi as cairo
import random
def DrawFilledCircle(x,y,radius,rgba):
ctx.set_source_rgba(*rgba)
ctx.arc(x,y,radius,0,2*math.pi)
ctx.fill()
def DrawCircle(x,y,radius,rgba=(0,0,0,1)):
ctx.set_source_rgba(*rgba)
ctx.arc(x,y,radius,0,2*math.pi)
ctx.stroke()
surface = cairo.RecordingSurface(cairo.CONTENT_COLOR_ALPHA, None)
ctx = cairo.Context (surface)
DrawCircle(200,200,150,(0,0,0,1))
for i in range(1000):
DrawFilledCircle(200+(150-300*random.random()), 200+(150-300*random.random()), 4, (0,0,0,0.5))
#This part will change throughout the question
extents = surface.ink_extents()
pdfout = cairo.PDFSurface ("circle.pdf", extents[2], extents[3])
pdfctx = cairo.Context (pdfout)
pdfctx.set_source_surface(surface,0,0)
Running the script produces the following.
As you can see, the width and height are correct, but the origin needs to be shifted. Accordingly, I tried this:
pdfout = cairo.PDFSurface ("circle.pdf", extents[2], extents[3])
pdfctx = cairo.Context (pdfout)
pdfctx.set_source_surface(surface,extents[0],extents[1])
This just shifted things further to the bottom right.
That suggested using negative coordinates to shift things the other way:
pdfout = cairo.PDFSurface ("circle.pdf", extents[2], extents[3])
pdfctx = cairo.Context (pdfout)
pdfctx.set_source_surface(surface,-extents[0],-extents[1])
But that didn't work either:
As a back-up, I could make a shell call to pdfcrop, but that seems like a ridiculous workaround.
What can I do to achieve what I'm trying to do? Where am I going wrong? How can a lasting peace be achieved in Cairo?

Mouse Position Python Tkinter

Is there a way to get the position of the mouse and set it as a var?
You could set up a callback to react to <Motion> events:
import Tkinter as tk
root = tk.Tk()
def motion(event):
x, y = event.x, event.y
print('{}, {}'.format(x, y))
root.bind('<Motion>', motion)
root.mainloop()
I'm not sure what kind of variable you want. Above, I set local variables x and y to the mouse coordinates.
If you make motion a class method, then you could set instance attributes self.x and self.y to the mouse coordinates, which could then be accessible from other class methods.
At any point in time you can use the method winfo_pointerx and winfo_pointery to get the x,y coordinates relative to the root window. To convert that to absolute screen coordinates you can get the winfo_pointerx or winfo_pointery, and from that subtract the respective winfo_rootx or winfo_rooty
For example:
root = tk.Tk()
...
x = root.winfo_pointerx()
y = root.winfo_pointery()
abs_coord_x = root.winfo_pointerx() - root.winfo_rootx()
abs_coord_y = root.winfo_pointery() - root.winfo_rooty()
Personally, I prefer to use pyautogui, even in combination with Tkinter. It is not limited to Tkinter app, but works on the whole screen, even on dual screen configuration.
import pyautogui
x, y = pyautogui.position()
In case you want to save various positions, add an on-click event.
I know original question is about Tkinter.
I would like to improve Bryan's answer, as that only works if you have 1 monitor, but if you have multiple monitors, it will always use your coordinates relative to your main monitor. in order to find it relative to both monitors, and get the accurate position, then use vroot, instead of root, like this
root = tk.Tk()
...
x = root.winfo_pointerx()
y = root.winfo_pointery()
abs_coord_x = root.winfo_pointerx() - root.winfo_vrootx()
abs_coord_y = root.winfo_pointery() - root.winfo_vrooty()

Get window position and size in python with Xlib

I need to find window position and size, but I cannot figure out how. For example if I try:
id.get_geometry() # "id" is Xlib.display.Window
I get something like this:
data = {'height': 2540,
'width': 1440,
'depth': 24,
'y': 0, 'x': 0,
'border_width': 0
'root': <Xlib.display.Window 0x0000026a>
'sequence_number': 63}
I need to find window position and size, so my problem is: "y", "x" and "border_width" are always 0; even worse, "height" and "width" are returned without window frame.
In this case on my X screen (its dimensions are 4400x2560) I expected x=1280, y=0, width=1440, height=2560.
In other words I'm looking for python equivalent for:
#!/bin/bash
id=$1
wmiface framePosition $id
wmiface frameSize $id
If you think Xlib is not what I want, feel free to offer non-Xlib solution in python if it can take window id as argument (like the bash script above). Obvious workaround to use output of the bash script in python code does not feel right.
You are probably using reparenting window manager, and because of this id window has zero x and y. Check coordinates of parent window (which is window manager frame)
Liss posted the following solution as a comment:
from ewmh import EWMH
ewmh = EWMH()
def frame(client):
frame = client
while frame.query_tree().parent != ewmh.root:
frame = frame.query_tree().parent
return frame
for client in ewmh.getClientList():
print frame(client).get_geometry()
I'm copying it here because answers should contain the actual answer, and to prevent link rot.
Here's what I came up with that seems to work well:
from collections import namedtuple
import Xlib.display
disp = Xlib.display.Display()
root = disp.screen().root
MyGeom = namedtuple('MyGeom', 'x y height width')
def get_absolute_geometry(win):
"""
Returns the (x, y, height, width) of a window relative to the top-left
of the screen.
"""
geom = win.get_geometry()
(x, y) = (geom.x, geom.y)
while True:
parent = win.query_tree().parent
pgeom = parent.get_geometry()
x += pgeom.x
y += pgeom.y
if parent.id == root.id:
break
win = parent
return MyGeom(x, y, geom.height, geom.width)
Full example here.
In the same idea as #mgalgs, but more direct, I ask the root window to translate the (0,0) coordinate of the target window :
# assuming targetWindow is the window you want to know the position of
geometry = targetWindow.get_geometry()
position = geometry.root.translate_coords(targetWindow.id, 0, 0)
# coordinates are in position.x and position.y
# if you are not interested in the geometry, you can do directly
import Xlib.display
position = Xlib.display.Display().screen().root.translate_coords(targetWindow.id, 0, 0)
This gives the position of the client region of the targeted window (ie. without borders, title bar and shadow decoration created by the window manage). If you want to include them, replace targetWindow with targetWindow.query_tree().parent (or second parent).
Tested with KUbuntu 20.04 (ie KDE, Plasma and KWin decoration).

Categories

Resources