Tkinter canvas (FigureCanvasTkAgg) bbox gives None - python

Met strange problem : canvas.bbox('ALL') gives None.
SO topics say that canvas only have coords to give when there some is .create_xxx() methods in code. The thing is that class FigureCanvasTkAgg in its __init__ method use create_image method and I think in my little snippet, when I create instance of thic Class it should give me some coordinate box when call instance.bbox command. But it's not...
self.Frame = Frame(root, bg = 'white')
self.Frame.place (relx = 0.37 , rely = 0.05 , relheight = 0.85 , relwidth = 0.51)
self.canvas = FigureCanvasTkAgg ( fig , master = self.Frame) # A tk.DrawingArea.
self.canvas.get_tk_widget ().place ( relx = 0 , rely = 0)
self.canvas.get_tk_widget ().config ( yscrollcommand = self.vbar.set ,
scrollregion = (0,0,w,639*h/10 ))
self.canvas.get_tk_widget ().update_idletasks ()
print(self.canvas.get_tk_widget ().winfo_width(),self.canvas.get_tk_widget ().winfo_height() )
self.Frame.update_idletasks ()
print(self.canvas.get_tk_widget ().bbox('ALL'))
self.toolbar = NavigationToolbar2Tk ( self.canvas , self.Frame )
self.vbar.pack ( side = RIGHT , fill = Y)

Met strange problem : canvas.bbox('ALL') gives None.
When you use 'ALL', it is looking for all canvas objects with the tag 'ALL' and not finding any.
If you want the bounding box of all objects, the correct argument to bbox is 'all', not 'ALL'. The literal string "all" is treated as a special case by the canvas to represent all objects on the canvas.

#BryanOakley 's answer is really an important clarification.
The excellent documentation by the late John Shipman, unfortunately, does have an error for present-day tkinter. It stated that:
.bbox(tagOrId=None)
Returns a tuple (x1, y1, x2, y2) describing a rectangle that encloses all the objects
specified by tagOrId. If the argument is omitted, returns a rectangle enclosing all objects
on the canvas. The top left corner of the rectangle is (x1, y1) and the bottom right corner
is (x2, y2).
Presently, if canvas.bbox() is used, tkinter returns _tkinter.TclError: wrong # args: should be ".!xxxxxxx bbox tagOrId ?tagOrId ...?"
There are online tkinter documents that advocate using ALL. An example is this one. However, most readers would have been oblivious that the from tkinter import * statement had been used to import all tkinter objects, which is also not in line with PEP8 guidelines on import statements. A good practice for importing tkinter is to use import tkinter as tk. Following this, your statement should either be:
print(self.canvas.get_tk_widget().bbox(tk.ALL))
or
print(self.canvas.get_tk_widget().bbox('all'))
Summarising: Either use .bbox(tk.ALL) or .bbox('all') on a tk.Canvas object/instance.

Related

(Instantiating an array of Buttons, but only one works

I'm trying to create a GUI for a virtual board for the game Go. There should be an nxn grid of tiles where a player can place a stone, either black or white. Clicking on a tile will make it change from tan(the default) to black, click again to white, and click a third time to go back to tan. Player one can click once on a spot to place a stone there, and player two can click twice (you need to remove stones later, so three clicks resets it). I created a tile object and then used a nested for loop to instantiate 9 by 9 of them. Unfortunately, running the code only seems to produce 1 functional tile, not 81. This code should work on any python machine (I'm using Python 3.4), so you can try to run it and see for yourself. Can anyone point out the reason the loop is only running once?
from tkinter import *
window = Tk()
n = 9
"""
A tile is a point on a game board where black or white pieces can be placed. If there are no pieces, it remains tan.
The basic feature is the "core" field which is a tkinter button. when the color is changed, the button is configured to represent this.
"""
class tile(object):
core = Button(window, height = 2, width = 3, bg = "#F4C364")
def __init__(self):
pass
"""the cycle function makes the tile object actually change color, going between three options: black, white, or tan."""
def cycle(self):
color = self.core.cget("bg")
if(color == "#F4C364"): #tan, the inital value.
self.core.config(bg = "#111111")#white.
elif (color == "#111111"):
self.core.config(bg = "#DDDDDD")#black.
else:
self.core.config(bg = "#F4C364")#back to tan.
board = [] #create overall array
for x in range(n):
board.append([])#add subarrays inside it
for y in range(n):
board[x].append(tile())#add a tile n times in each of the n subarrays
T = board[x][y] #for clarity, T means tile
T.core.config(command = lambda: T.cycle()) #I do this now because cycle hadn't been defined yet when I created the "core" field
T.core.grid(row = x, column = y) #put them into tkinter.
window.mainloop()
As mhawke points out in his answer you need to make the core an instance variable, so that each Tile gets its own core.
And as I mention in my comment above, you also need to fix the Button's command callback function. The code you use in your question will call the .cycle() method of the current value of T, which happens to be the last tile created. So no matter where you click only the last tile changes color. One way to fix that is to pass the current tile as a default argument of the lambda function when you create it. But because you are using OOP to create your Tile there's a better way, which you can see below.
I've made a few modifications to your code.
Although many Tkinter examples use from tkinter import * it's not a good practice. When you do from some_module import * it brings all of the names from some_module into the current module (your script), which means you could accidentally override those names with your own names. Even worse, if you do import * with multiple modules each new module's names can clash with the previous module's names, and you have no way of knowing that's happened until you start getting mysterious bugs. Using import tkinter as tk means you need to do a little more typing, but it makes the resulting program less bug-prone and easier to read.
I've modified the __init__ method so that it is called with the window and the (x, y) location of the tile (it's customary to use x for the horizontal coordinate and y for the vertical coordinate). Each Tile object now keeps track of its current state, where 0=empty, 1=black, 2=white. This makes it easier to update the colors. And because we've passed in the window and (x, y) we can use that info to add the tile to the grid. The tile also remembers the location (in self.location), which may come in handy.
I've modified the cycle method so that it updates both the background color and the activebackground of the tile. So when the mouse hovers over the tile it changes to a color that's (roughly) halfway between its current color and the color it will turn if you click it. IMO, this is nicer than the tile always turning pale grey when the mouse hovers over it.
I've also optimized the code that creates all the tiles and stores them in the board list of lists.
import tkinter as tk
colors = (
#background, #activebackground
("#F4C364", "#826232"), #tan
("#111111", "#777777"), #black
("#DDDDDD", "#E8C8A8"), #white
)
class Tile(object):
""" A tile is a point on a game board where black or white pieces can be placed.
If there are no pieces, it remains tan.
The basic feature is the "core" field which is a tkinter button.
when the color is changed, the button is configured to represent this.
"""
def __init__(self, win, x, y):
#States: 0=empty, 1=black, 2=white
self.state = 0
bg, abg = colors[self.state]
self.core = tk.Button(win, height=2, width=3,
bg=bg, activebackground=abg,
command=self.cycle)
self.core.grid(row=y, column=x)
#self.location = x, y
def cycle(self):
""" the cycle function makes the tile object actually change color,
going between three options: black, white, or tan.
"""
#cycle to the next state. 0 -> 1 -> 2 -> 0
self.state = (self.state + 1) % 3
bg, abg = colors[self.state]
self.core.config(bg=bg, activebackground=abg)
#print(self.location)
window = tk.Tk()
n = 9
board = []
for y in range(n):
row = [Tile(window, x, y) for x in range(n)]
board.append(row)
window.mainloop()
The problem is that core is a class variable which is created once and shared by all instances of class tile. It should be an instance variable for each tile instance.
Move core = Button(window, height = 2, width = 3, bg = "#F4C364") into tile.__init__() like this:
class Tile(object):
def __init__(self):
self.core = Button(window, height = 2, width = 3, bg = "#F4C364")
The root of the problem is that core is shared by all instances of the class by virtue of how you've defined it. You need to move creation of the button into the initializer.
I also suggest moving the configuration of the command into the button itself. The caller shouldn't need (nor care) how the button works internally. Personally I'd have the tile inherit from Button, but if you favor composition over inheritance I'll stick with that.
Example:
class tile(object):
def __init__(self):
self.core = Button(window, height = 2, width = 3, bg = "#F4C364"
command=self.cycle)

"_tkinter.TclError: bad screen distance" in python's tkinter when trying to modify object coordinates with Canvas.coords()

I am trying to make a canvas with some items that can move and rotate, to do this, i have functions to modify the coordinates, however i am having trouble with moving the objects. I am trying to use the coords function to change the coordinates of each object.
the current bit of code that is raising the error is:
count = 1
for part in self._createdpartlist:
self.coords(part, self._partlist[count].coordinates)
count += 1
self is a Canvas object i created. with createdpartlist containing id's of created parts in canvas (all 4 sided polygons) and partlist being a list of objects that have coordinates that are returned in the form of [(x1, y1), (x2, y2), (x3, y3), (x4, y4)]
however when i try to run it, i get the error;
_tkinter.TclError: bad screen distance "340)]"
(in this case 340 is the y4 coordinate)
I dont exactly know what it means by bad screen distance, and cant really figure out whats going wrong or if i am using coords function incorrectly.
Any help is greatly appreciated
Edit: i get this error when i make a new file only containing this.
from tkinter import *
coordinates = [(330,230), (350,230), (350,340), (330,340)]
new_coords = [(340,245), (340,260), (400,260), (400,245)]
c = Canvas()
shape = c.create_polygon(coordinates)
c.coords(shape, new_coords)
the error comes up with "245)]" instead of "340)]" in this instance
Can you try this? I will try it later when I am not on mobile.
import itertools
try:
import Tkinter as tk
except ImportError:
import tkinter as tk
# from itertools recipes: https://docs.python.org/2/library/itertools.html
def flatten(list_of_lists):
"""Flatten one level of nesting"""
return itertools.chain.from_iterable(list_of_lists)
coordinates = [(330,230), (350,230), (350,340), (330,340)]
new_coords = [(340,245), (340,260), (400,260), (400,245)]
c = tk.Canvas()
shape = c.create_polygon(coordinates)
c.coords(shape, *flatten(new_coords))
If that works then try:
for i, part in enumerate(self._createdpartlist):
self.coords(part, *flatten(self._partlist[i+1].coordinates))
I ran into _tkinter.TclError: bad screen distance when trying Pypy3 5.5.0-alpha.
Changing coordinate lists into tuples helped.

Moving Around A Python Canvas Without Using Scrollbars

Background:I have a program using Tkinter as the basis of the GUI. The program has a canvas which is populated with a large number of objects. Currently, in order to move all objects on the screen, I am simply binding a movement function to the tag 'all' which of course moves all objects on the screen. However, it is vital for me to keep track of all canvas object positions- i.e. after every move I log the new position, which seems unnecessarily complicated.
Question:
What is the best way to effectively scroll/drag around the whole canvas (several times the size of the screen) using only the mouse (not using scrollbars)?
My Attempts:I have implemented scrollbars and found several guides to setting up scrollbars, but none that deal with this particular requirement.
Example of disused scrollbar method:
from Tkinter import *
class Canvas_On:
def __init__(self, master):
self.master=master
self.master.title( "Example")
self.c=Canvas(self.master, width=find_res_width-20, height=find_res_height, bg='black', scrollregion=(0,0,5000,5000))
self.c.grid(row=0, rowspan=25, column=0)
self.c.tag_bind('bg', '<Control-Button-1>', self.click)
self.c.tag_bind('bg', '<Control-B1-Motion>', self.drag)
self.c.tag_bind('dot', '<Button-1>', self.click_item)
self.c.tag_bind('dot', '<B1-Motion>', self.drag_item)
draw=Drawing_Utility(self.c)
draw.drawer(self.c)
def click(self, event):
self.c.scan_mark(event.x, event.y)
def drag(self, event):
self.c.scan_dragto(event.x, event.y)
def click_item(self, event):
self.c.itemconfigure('dot 1 text', text=(event.x, event.y))
self.drag_item = self.c.find_closest(event.x, event.y)
self.drag_x, self.drag_y = event.x, event.y
self.c.tag_raise('dot')
self.c.tag_raise('dot 1 text')
def drag_item(self, event):
self.c.move(self.drag_item, event.x-self.drag_x, event.y-self.drag_y)
self.drag_x, self.drag_y = event.x, event.y
class Drawing_Utility:
def __init__(self, canvas):
self.canvas=canvas
self.canvas.focus_set()
def drawer(self, canvas):
self.canvas.create_rectangle(0, 0, 5000, 5000,
fill='black', tags='bg')
self.canvas.create_text(450,450, text='', fill='black', activefill='red', tags=('draggable', 'dot', 'dot 1 text'))
self.canvas.create_oval(400,400,500,500, fill='orange', activefill='red', tags=('draggable', 'dot', 'dot 2'))
self.canvas.tag_raise(("dot"))
root=Tk()
find_res_width=root.winfo_screenwidth()
find_res_height=root.winfo_screenheight()
run_it=Canvas_On(root)
root.mainloop()
My Particular Issue
My program generates all canvas object coordinates and then draws them. The objects are arranged in various patterns, but critically they must 'know' where each other is. When moving around the canvas using the method #abarnert kindly supplied, and a similar method I wrote that moved all canvas objects, the issue arises that each object 'thinks' it is at the canvas coordinates generated before the objects were drawn. For example if I drag the canvas 50 pixels to the left and clicked on an object in my program, it jumps 50 pixels back to the right to it's original position. My solution to this was to write some code that, upon release of the mouse button, logged the last position and updated the coordinate data of each object. However, I'm looking for a way to remove this last step- I was hoping there was a way to move the canvas such that the object positions were absolute, and assumed a function similar to a 'scroll' function would do this. I realise I've rambled here, but I've added a couple of lines to the example above which highlights my issue- by moving the canvas you can see that the coordinates change. Thank you again.
I'll give you the code for the simplest version first, then explain it so you can expand it as needed.
class Canvas_On:
def __init__(self, master):
# ... your original code here ...
self.c.bind('<Button-1>', self.click)
self.c.bind('<B1-Motion>', self.drag)
def click(self, event):
self.c.scan_mark(event.x, event.y)
def drag(self, event):
self.c.scan_dragto(event.x, event.y)
First, the easy part: scrolling the canvas manually. As the documentation explains, you use the xview and yview methods, exactly as your scrollbar commands do. Or you can just directly call xview_moveto and yview_moveto (or the foo_scroll methods, but they don't seem to be what you want here). You can see that I didn't actually use these; I'll explain below.
Next, to capture click-and-drag events on the canvas, you just bind <B1-Motion>, as you would for a normal drag-and-drop.
The tricky bit here is that the drag event gives you screen pixel coordinates, while the xview_moveto and yview_moveto methods take a fraction from 0.0 for the top/left to 1.0 for the bottom/right. So, you'll need to capture the coordinates of the original click (by binding <Button-1>; with that, the coordinates of the drag event, and the canvas's bbox, you can calculate the moveto fractions. If you're using the scale method and want to drag appropriately while zoomed in/out, you'll need to account for that as well.
But unless you want to do something unusual, the scan helper methods do exactly that calculation for you, so it's simpler to just call them.
Note that this will also capture click-and-drag events on the items on the canvas, not just the background. That's probably what you want, unless you were planning to make the items draggable within the canvas. In the latter case, add a background rectangle item (either transparent, or with whatever background you intended for the canvas itself) below all of your other items, and tag_bind that instead of binding the canvas itself. (IIRC, with older versions of Tk, you'll have to create a tag for the background item and tag_bind that… but if so, you presumably already had to do that to bind all your other items, so it's the same here. Anyway, I'll do that even though it shouldn't be necessary, because tags are a handy way to create groups of items that can all be bound together.)
So:
class Canvas_On:
def __init__(self, master):
# ... your original code here ...
self.c.tag_bind('bg', '<Button-1>', self.click)
self.c.tag_bind('bg', '<B1-Motion>', self.drag)
self.c.tag_bind('draggable', '<Button-1>', self.click_item)
self.c.tag_bind('draggable', '<B1-Motion>', self.drag_item)
# ... etc. ...
def click_item(self, event):
x, y = self.c.canvasx(event.x), self.c.canvasy(event.y)
self.drag_item = self.c.find_closest(x, y)
self.drag_x, self.drag_y = x, y
self.tag_raise(item)
def drag_item(self, event):
x, y = self.c.canvasx(event.x), self.c.canvasy(event.y)
self.c.move(self.drag_item, x-self.drag_x, y-self.drag_y)
self.drag_x, self.drag_y = x, y
class Drawing_Utility:
# ...
def drawer(self, canvas):
self.c.create_rectangle(0, 0, 5000, 5000,
fill='black', tags='bg')
self.c.create_oval(50,50,150,150, fill='orange', tags='draggable')
self.c.create_oval(1000,1000,1100,1100, fill='orange', tags='draggable')
Now you can drag the whole canvas around by its background, but dragging other items (the ones marked as 'draggable') will do whatever else you want instead.
If I understand your comments correctly, your remaining problem is that you're trying to use window coordinates when you want canvas coordinates. The section Coordinate Systems in the docs explains the distinction.
So, let's say you've got an item that you placed at 500, 500, and the origin is at 0, 0. Now, you scroll the canvas to 500, 0. The window coordinates of the item are now 0, 500, but its canvas coordinates are still 500, 500. As the docs say:
To convert from window coordinates to canvas coordinates, use the canvasx and canvasy methods

Finding and Connecting Multiple Canvas Items in Python

Background: I have a code which generates the cartesian coordinates of a network of regular shapes (in this case triangles), and then plots the vertices of the shapes on a Tkinter Canvas as small circles. The process is automated and requires only height and width of the network to obtain a canvas output. Each vertex has the tags 'Vertex' and the vertex's number. Problem: I want to automatically connect the vertices of the shapes together (i.e dot to dot), I have looked into using find_closest and find_overlapping methods to do this, but as the network is composed of vertices at angles to one another, I often find find_overlapping to be unreliable (due to relying on a rectangular envelope), and find_closest appears limited to finding only one connection. As the vertices aren't necessarily connected in order, it is not possible to create a loop to simply connect vertex 1 --> vertex 2 etc. Question: Is there a way to efficiently get all of a vertex's neighbouring vertices and then 'connect the dots' without relying on individually creating lines between points using a manual method such as self.c.create_line(vertex_coord[1], vertex_coord[0], fill='black') for each connection? And would it be possible to share a small example of such a code? Thank you in advance for any help!Below is an abbreviated version of the canvas components of my code.Prototype Method:
from data_generator import *
run_coordinate_gen=data_generator.network_coordinates()
run_coordinate_gen.generator_go()
class Network_Canvas:
def __init__(self, canvas):
self.canvas=canvas
canvas.focus_set()
self.canvas.create_oval(Vertex_Position[0], dimensions[0], fill='black', tags=('Vertex1', Network_Tag, Vertex_Tag))
self.canvas.create_oval(Vertex_Position[5], dimensions[5], fill='black', tags=('Vertex2', Network_Tag, Vertex_Tag))
try:
self.canvas.create_line(Line_Position[5] ,Line_Position[0] , fill='black' tags=(Network_Tag,'Line1', Line_Tag )) #Connection Between 1 and 6 (6_1), Line 1
except:
pass
#Note: Line_Position, Dimensions and Vertex_Position are all lists composed of (x,y) cartesian coordinates in this case.
This is of course then replicated for each line and vertex throughout the network, but was only used for 90 vertices. The new version requires orders of magnitude more vertices and I am doing this with:
New Method:
#Import updated coordinate generator and run it as before
class Network_Canvas:
def __init__(self, canvas):
self.canvas=canvas
canvas.focus_set()
for V in range(len(vertex_coord_xy)):
self.canvas.create_text(vertex_coord_xy[V]+Text_Distance, text=V+1, fill='black', tags=(V, 'Text'), font=('Helvetica', '9'))
self.canvas.create_oval(vertex_coord_xy[V],vertex_coord_xy[V]+Diameter, fill='black', outline='black', tags=(V, 'Vertex'))
#loop to fit connections here (?)
I think any kind of nearest-neighbor search is going to be waay more time-intensive than just keeping track of the vertices, and there's no "automatic" connect-the-dots method that I can think of (plus, I don't see why such a method should be any faster than drawing them with create_line). Also, how will a nearest-neighbor search algorithm distinguish between the vertices of two separate, nearby (or overlapping) shapes if you aren't keeping track? Anyhow, in my opinion you've already got the right method; there are probably ways to optimize it.
I think that since your shapes are numerous, and there are complicated things you need to do with them, I would make a class for them, like the one I implemented below. It includes the "click to see neighboring vertices" functionality. All of the following code ran without errors. Image of the output shown below.
import Tkinter as TK
import tkMessageBox
# [Credit goes to #NadiaAlramli](http://stackoverflow.com/a/1625023/1460057) for the grouping code
def group(seq, groupSize):
return zip(*(iter(seq),) * groupSize)
Network_Tag, Vertex_Tag, Line_Tag = "network", "vertex", "line"
class Shape:
def __init__(self, canvas, vertexCoords, vertexDiam):
self.vertexIDs = []
self.perimeterID = None
self.vertexCoords = vertexCoords
self.vertexRadius = vertexDiam/2
self.canvas = canvas
def deleteVertices(self):
for ID in self.vertexIDs:
self.canvas.delete(ID)
self.vertexIDs = []
def bindClickToVertices(self):
coordsGrouped = group(self.vertexCoords, 2)
num = len(coordsGrouped)
for k in range(len(self.vertexIDs)):
others = [coordsGrouped[(k-1)%num], coordsGrouped[(k+1)%num]]
self.canvas.tag_bind(self.vertexIDs[k], '<Button-1>',
lambda *args:tkMessageBox.showinfo("Vertex Click", "Neighboring vertices: "+str(others)))
def drawVertices(self):
for x, y in group(self.vertexCoords, 2):
self.vertexIDs.append(self.canvas.create_oval(x-self.vertexRadius, y-self.vertexRadius, x+self.vertexRadius, y+self.vertexRadius, fill='black', tags=(Network_Tag, Vertex_Tag)))
self.bindClickToVertices()
def updateVertices(self):
self.deleteVertices()
self.drawVertices()
def deletePerimeter(self):
if self.perimeterID is not None:
self.canvas.delete(self.perimeterID)
self.perimeterID = None
def drawPerimeter(self):
print "creating line:", (self.vertexCoords + self.vertexCoords[0:2])
self.perimeterID = self.canvas.create_line(*(self.vertexCoords + self.vertexCoords[0:2]), fill='black', tags=(Network_Tag, Line_Tag))
def updatePerimeter(self):
self.deletePerimeter()
self.drawPerimeter()
def deleteShape(self):
self.deleteVertices()
self.deletePerimeter()
def updateShape(self):
self.updateVertices()
self.updatePerimeter()
It can be used very simply, like this:
root = TK.Tk()
frame = TK.Frame(root)
canvas = TK.Canvas(frame, width=1000, height=1000)
frame.grid()
canvas.grid()
# create a bunch of isoceles triangles in different places:
shapes = []
for dx, dy in zip(range(0,1000, 30), range(0,1000, 30)):
shapes.append(Shape(canvas, [0+dx, 0+dy, 10+dx, 10+dy, 20+dx, 0+dy], 5))
# draw (or redraw) the shapes:
for shape in shapes:
shape.updateShape()
# move one of the shapes and change it to a square
shapes[10].vertexCoords = [50, 10, 60, 10, 60, 20, 50, 20]
shapes[10].updateShape()
# delete all the odd-numbered shapes, just for fun:
for k in range(len(shapes)):
if k%2 == 1:
shape.deleteShape()
root.mainloop()
Output:

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