Pyautogui scroll fine tuning? - python

The pyautogui scroll amount value 1 is too small, 2 is to big for a specific task I want to do. Is there a way to scroll inbetween? I tried 1.5, but it didn't work.
I'm on OSX 10.13 and I can certainly scroll with more precision than what pyautogui is doing, when using the trackpad.

This is an issue that has been annoying me, so I took a look at the pyautogui source code and was able to solve the problem. This will probably be quite a long answer; I'll try to explain every step in detail. Note that this only works for Mac. (scroll to the bottom if you want the answer, not the explanation)
First, here is the source code for the scroll function:
def _scroll(clicks, x=None, y=None):
_vscroll(clicks, x, y)
def _vscroll(clicks, x=None, y=None):
_moveTo(x, y)
clicks = int(clicks)
for _ in range(abs(clicks) // 10):
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
None, # no source
Quartz.kCGScrollEventUnitLine, # units
1, # wheelCount (number of dimensions)
10 if clicks >= 0 else -10) # vertical movement
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
None, # no source
Quartz.kCGScrollEventUnitLine, # units
1, # wheelCount (number of dimensions)
clicks % 10 if clicks >= 0 else -1 * (-clicks % 10)) # vertical movement
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
Let's break this down:
1.
def _scroll(clicks, x=None, y=None):
_vscroll(clicks, x, y)
This is just a wrapper for the _vscroll function, simple enough.
2.
The main thing to realise is that pyautogui, for Mac, uses Quartz Core Graphics, all it does is provide a simpler, more readable wrapper for the Quartz code.
With the scroll function, what it is doing is creating a scroll event:
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent
And then posting it:
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
Ignore the details of the posting, we won't be changing any of that.
To me, it seems as if this code repeats itself, and I have no clue why any of the code after the for loop is included. I deleted this from my source code and everything works; If anyone knows why this code is included, please comment below and correct me.
3.
So we are left with the following code (ignoring the mouse moveTo, which has nothing to do with the scrolling itself):
clicks = int(clicks)
for _ in range(abs(clicks) // 10):
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
None, # no source
Quartz.kCGScrollEventUnitLine, # units
1, # wheelCount (number of dimensions)
10 if clicks >= 0 else -10) # vertical movement
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
The format of a CGEventCreateScrollWheelEvent is the following:
Quartz.CGEventCreateScrollWheelEvent(source, units, wheelCount, scroll distance)
The source in this case is None, don't worry about that, and we are only dealing with 1 wheel, so wheelCount is 1.
What the source code is doing, therefore, is scrolling a distance of ±10 Quartz.kCGScrollEventUnitLine, which are your computers units for one 'scroll'. It repeats this in a for loop for however many times you specify because the system can bug if too many scroll units are sent at once.
Therefore, the minimum one can scroll on pyautogui is one iteration of this loop, which sends one computer unit. The problem is that these units are too big for fine scrolling.
SOLUTION
We need to change the minimum value we can send. Currently it is 1 Quartz.kCGScrollEventUnitLine, but we can change these to base units by replacing them with a zero. I also see no need to floor divide clicks (in range(abs(clicks) // 10)) and then send 10 scroll units.
We can change these two parts, and remove the unnecessary repetition:
def _scroll(clicks, x=None, y=None):
_vscroll(clicks, x, y)
def _vscroll(clicks, x=None, y=None):
_moveTo(x, y)
clicks = int(clicks)
for _ in range(abs(clicks)): # <------------------------------------
scrollWheelEvent = Quartz.CGEventCreateScrollWheelEvent(
None, # no source
0, # units <------------------------------------------------
1, # wheelCount (number of dimensions)
1 if clicks >= 0 else -1) # vertical movement <--------------
Quartz.CGEventPost(Quartz.kCGHIDEventTap, scrollWheelEvent)
If you don't feel comfortable editing the source code itself, you can use these functions in your code directly, skipping out the need for pyautogui. Just have pyobjc installed (which you'll have anyway if you use pyautogui), remove _moveTo(x, y) and the keyword arguments, and use the following imports:
from Quartz.CoreGraphics import CGEventCreateScrollWheelEvent, CGEventPost, kCGHIDEventTap
I realise this answer is a bit late, but I came looking for answers to this problem and saw your question; When I solved the problem I thought I would share the knowledge.

I really struggled with this one, so I thought I'd post my solution for Windows.
After a quick pip install pywin32, I got access to the necessary win32api & win32con, among others.
NOTE: The last time I checked, pywin32 was only supported for:
Python :: 2.7
Python :: 3.5
Python :: 3.6
Python :: 3.7
import time
import win32api
import win32con
def scroll(clicks=0, delta_x=0, delta_y=0, delay_between_ticks=0):
"""
Source: https://learn.microsoft.com/en-gb/windows/win32/api/winuser/nf-winuser-mouse_event?redirectedfrom=MSDN
void mouse_event(
DWORD dwFlags,
DWORD dx,
DWORD dy,
DWORD dwData,
ULONG_PTR dwExtraInfo
);
If dwFlags contains MOUSEEVENTF_WHEEL,
then dwData specifies the amount of wheel movement.
A positive value indicates that the wheel was rotated forward, away from the user;
A negative value indicates that the wheel was rotated backward, toward the user.
One wheel click is defined as WHEEL_DELTA, which is 120.
:param delay_between_ticks:
:param delta_y:
:param delta_x:
:param clicks:
:return:
"""
if clicks > 0:
increment = win32con.WHEEL_DELTA
else:
increment = win32con.WHEEL_DELTA * -1
for _ in range(abs(clicks)):
win32api.mouse_event(win32con.MOUSEEVENTF_WHEEL, delta_x, delta_y, increment, 0)
time.sleep(delay_between_ticks)
Then, after defining
click_point = x_position, y_position
and then using
pyautogui.moveTo(x=click_point[0], y=click_point[1], duration=0.25)
to make sure that my mouse is in the correct location. I just call the above scroll function:
scroll(-4, 0.1)
to scroll down 4 ticks with a 100ms delay between ticks.

Related

Randomness of Python's random

I'm using Python to generate images using dashed lines for stippling. The period of the dashing is constant, what changes is dash/space ratio. This produces something like this:
However in that image the dashing has a uniform origin and this creates unsightly vertical gutters. So I tried to randomize the origin to remove the gutters. This sort of works but there is an obvious pattern:
Wondering where this comes from I made a very simple test case with stacked dashed straight lines:
dash ratio: 50%
dash period 20px
origin shift from -10px to +10px using random.uniform(-10.,+10.)(*) (after an initial random.seed()
And with added randomness:
So there is still pattern. What I don't understand is that to get a visible gutter you need to have 6 or 7 consecutive values falling in the same range (says, half the total range), which should be a 1/64 probability but seems to happen a lot more often in the 200 lines generated.
Am I misunderstanding something? Is it just our human brain which is seeing patterns where there is none? Could there be a better way to generate something more "visually random" (python 2.7, and preferably without installing anything)?
(*) partial pixels are valid in that context
Annex: the code I use (this is a Gimp script):
#!/usr/bin/env python
# -*- coding: iso-8859-15 -*-
# Python script for Gimp (requires Gimp 2.10)
# Run on a 400x400 image to see something without having to wait too much
# Menu entry is in "Test" submenu of image menubar
import random,traceback
from gimpfu import *
def constant(minShift,maxShift):
return 0
def triangle(minShift,maxShift):
return random.triangular(minShift,maxShift)
def uniform(minShift,maxShift):
return random.uniform(minShift,maxShift)
def gauss(minShift,maxShift):
return random.gauss((minShift+maxShift)/2,(maxShift-minShift)/2)
variants=[('Constant',constant),('Triangle',triangle),('Uniform',uniform),('Gauss',gauss)]
def generate(image,name,generator):
random.seed()
layer=gimp.Layer(image, name, image.width, image.height, RGB_IMAGE,100, LAYER_MODE_NORMAL)
image.add_layer(layer,0)
layer.fill(FILL_WHITE)
path=pdb.gimp_vectors_new(image,name)
# Generate path, horizontal lines are 2px apart,
# Start on left has a random offset, end is on the right edge right edge
for i in range(1,image.height, 2):
shift=generator(-10.,10.)
points=[shift,i]*3+[image.width,i]*3
pdb.gimp_vectors_stroke_new_from_points(path,0, len(points),points,False)
pdb.gimp_image_add_vectors(image, path, 0)
# Stroke the path
pdb.gimp_context_set_foreground(gimpcolor.RGB(0, 0, 0, 255))
pdb.gimp_context_set_stroke_method(STROKE_LINE)
pdb.gimp_context_set_line_cap_style(0)
pdb.gimp_context_set_line_join_style(0)
pdb.gimp_context_set_line_miter_limit(0.)
pdb.gimp_context_set_line_width(2)
pdb.gimp_context_set_line_dash_pattern(2,[5,5])
pdb.gimp_drawable_edit_stroke_item(layer,path)
def randomTest(image):
image.undo_group_start()
gimp.context_push()
try:
for name,generator in variants:
generate(image,name,generator)
except Exception as e:
print e.args[0]
pdb.gimp_message(e.args[0])
traceback.print_exc()
gimp.context_pop()
image.undo_group_end()
return;
### Registration
desc="Python random test"
register(
"randomize-test",desc,'','','','',desc,"*",
[(PF_IMAGE, "image", "Input image", None),],[],
randomTest,menu="<Image>/Test",
)
main()
Think of it like this: a gutter is perceptible until it is obstructed (or almost so). This only happens when two successive lines are almost completely out of phase (with the black segments in the first line lying nearly above the white segments in the next). Such extreme situations only happens about one out of every 10 rows, hence the visible gutters which seem to extend around 10 rows before being obstructed.
Looked at another way -- if you print out the image, there really are longish white channels through which you can easily draw a line with a pen. Why should your mind not perceive them?
To get better visual randomness, find a way to make successive lines dependent rather than independent in such a way that the almost-out-of-phase behavior appears more often.
There's at least one obvious reason why we see a pattern in the "random" picture : the 400x400 pixels are just the same 20x400 pixels repeated 20 times.
So every apparent movement is repeated 20 times in parallel, which really helps the brain analyzing the picture.
Actually, the same 10px wide pattern is repeated 40 times, alternating between black and white:
You could randomize the dash period separately for each line (e.g. between 12 and 28):
Here's the corresponding code :
import numpy as np
import random
from matplotlib import pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = [13, 13]
N = 400
def random_pixels(width, height):
return np.random.rand(height, width) < 0.5
def display(table):
plt.imshow(table, cmap='Greys', interpolation='none')
plt.show()
display(random_pixels(N, N))
def stripes(width, height, stripe_width):
table = np.zeros((height, width))
cycles = width // (stripe_width * 2) + 1
pattern = np.concatenate([np.zeros(stripe_width), np.ones(stripe_width)])
for i in range(height):
table[i] = np.tile(pattern, cycles)[:width]
return table
display(stripes(N, N, 10))
def shifted_stripes(width, height, stripe_width):
table = np.zeros((height, width))
period = stripe_width * 2
cycles = width // period + 1
pattern = np.concatenate([np.zeros(stripe_width), np.ones(stripe_width)])
for i in range(height):
table[i] = np.roll(np.tile(pattern, cycles), random.randrange(0, period))[:width]
return table
display(shifted_stripes(N, N, 10))
def flexible_stripes(width, height, average_width, delta):
table = np.zeros((height, width))
for i in range(height):
stripe_width = random.randint(average_width - delta, average_width + delta)
period = stripe_width * 2
cycles = width // period + 1
pattern = np.concatenate([np.zeros(stripe_width), np.ones(stripe_width)])
table[i] = np.roll(np.tile(pattern, cycles), random.randrange(0, period))[:width]
return table
display(flexible_stripes(N, N, 10, 4))
Posting my final solution as an answer, but please upvote others.
John Coleman has a point when he says:
To get better visual randomness, find a way to make successive lines dependent rather than independent in such a way that the almost-out-of-phase behavior appears more often.
So, finally, the best way to avoid gutters is to forego randomness and have a very fixed scheme of shifts, and one that works well is a 4-phase 0,25%,75%,50% cycle:
OK, there is still slight diamond pattern, but it is much less visible than the patterns introduced by the random schemes I tried.
This is slightly counter-intuitive, but as you add random elements together the randomness gets less. If I follow correctly the range of each element is 10px - 30px. So the total size of 10 elements is 100px to 300px, but the distribution is not even across that range. The extremes are very unlikely and on average it will be pretty close to 200px, so that fundamental 20px pattern will emerge. Your random distribution needs to avoid this.
EDIT: I see I slightly misunderstood, and all dashes are are 20px with a random offset. So, I think looking at any 1 vertical set of dashes would appear random, but that same random set is repeated across the page, giving the pattern.

visual analogue scale psychopy

I'm developing a metacognition experiment in PsychoPy (v. 1.90.1) and I need a visual analogue scale to measure confidence. However, I can't find a way to remove the numeric values (0 and 1) from the extremities of the Psychopy VAS.
Is there any way to hide them?
I need the word labels ("Not at all confident", "Extremely confident") but I would also like to have the answers recorded on a 0-100 scale (or an equivalent 0-1) as the analogue scale does (so switching to categorical wouldn't do).
Any suggestion?
Thanks in advance.
Sonia
You may also be interested in the new Slider, which is included in the current PsychoPy beta versions and will be part of the next release. Here is a Python 3 code example how to use it:
from psychopy.visual.window import Window
from psychopy.visual.slider import Slider
win = Window()
vas = Slider(win,
ticks=(1, 100),
labels=('Not at all confident', 'Extremely confident'),
granularity=1,
color='white')
while not vas.rating:
vas.draw()
win.flip()
print(f'Rating: {vas.rating}, RT: {vas.rt}')
Before re-use, you will have to call vas.reset().
Take a look at the documentation, particularly labels and scale. This is one solution:
# Set up window and scale
from psychopy import visual
win = visual.Window()
scale = visual.RatingScale(win,
labels=['Not at all confident', 'Extremely confident'], # End points
scale=None, # Suppress default
low=1, high=100, tickHeight=0)
# Show scale
while scale.noResponse:
scale.draw()
win.flip()
# Show response
print scale.getRating(), scale.getRT()
Might I extend #hoechenberger's answer to add support for
the randomised start on each trial (set markerPos to a random val in units of the tick marks)
keypress support (just use keys to adjust markerPos and when you're done assign that to rating)
custom step sizes (when you've understood (2) then this is probably obvious)
Python2.7 (no need to force Py3.6 here :wink: )
Here's the code:
from psychopy import visual
from psychopy import event
from numpy.random import random
stepSize = 2
win = visual.Window()
vas = visual.Slider(win,
ticks=(0, 1),
labels=('Not at all confident', 'Extremely confident'),
granularity=1,
color='white')
for thisTrialN in range(5):
vas.reset()
vas.markerPos = random() # randomise start
while not vas.rating:
# check keys
keys = event.getKeys()
if 'right' in keys:
vas.markerPos += stepSize
if 'left' in keys:
vas.markerPos -= stepSize
if 'return' in keys:
# confirm as a rating
vas.rating = vas.markerPos
# update the scale on screen
vas.draw()
win.flip()
print('Rating: {}, RT: {}'.format(vas.rating, vas.rt))

Python with kivy moving multiple 3D objects

I have a rather simple goal of drawing a few spheres in a 3D space and adjusting their location according to some function. I want to use python with kivy to accomplish this because it make touch screen interfacing super simple, and I found a repository which takes care of most of the heavy lifting with respects to programming.
From this code in the main.py function, I want to draw n spheres, and then update their locations later on (this is done under the draw_elements(self) function, and LOP[] is a list of the class 'points')
def drawPoints():
print self.scene.objects
for i in range(len(self.LOP)):
PushMatrix()
point = self.LOP[i]
point.shape = self.scene.objects['Sphere']
point.color = _set_color(i/10., (i+1)/10., 0., id_color=(int(255/(1+i)), int(255/(1+i)), 255))
point.shape.scale = Scale((i+1)/10.0,(i+1)/10.0,(i+1)/10.0)
self.LOP[i] = point
point.shape.scale.origin = (point.loc[0],point.loc[1],point.loc[2])
_draw_element(point.shape)
PopMatrix()
drawPoints()
When the points are drawn, they are at their stated origin.
Later on the program calls the update_scene function thanks the the clock scheduler.
def update_scene(self, *largs):
def randLoc(point):
newLoc = (0.1*random.random(),0.1*random.random(),0.1*random.random())
oldLoc = point.shape.scale.origin
newLoc = ( newLoc[0]-0.05+oldLoc[0], newLoc[1]-0.05+oldLoc[1], newLoc[2]-0.05+oldLoc[2] )
return newLoc
def updateLocs(self):
for i in range(len(self.LOP)):
point = self.LOP[i]
point.shape.scale.origin = randLoc(point)
if not self.pause:
updateLocs(self)
pass
When this update function is run, only the sphere that was drawn last moves, though it does move correctly.
How can I move the other spheres I drew earlier?
(my source code can be found here though it's really just build off of the first repository)

Python get mouse x, y position on click

Coming from IDL, I find it quite hard in python to get the x-y position of the mouse on a single left click using a method that is not an overkill as in tkinter. Does anyone know about a python package that contains a method simply returning x-y when the mouse is clicked (similar to the cursor method in IDL)?
There are a number of libraries you could use. Here are two third party ones:
Using PyAutoGui
A powerful GUI automation library allows you to get screen size, control the mouse, keyboard and more.
To get the position you just need to use the position() function. Here is an example:
>>>import pyautogui
>>>pyautogui.position()
(1358, 146)
>>>
Where 1358 is the X position and 146 is the Y position.
Relavent link to the documentation
Using Pynput
Another (more minimalistic) library is Pynput:
>>> from pynput.mouse import Controller
>>> mouse = Controller()
>>> mouse.position
(1182, 153)
>>>
Where 1182 is the X position and 153 is the second.
Documentation
This library is quite easy to learn, does not require dependencies, making this library ideal for small tasks like this (where PyAutoGui would be an overkill). Once again though, it does not provide so many features though.
Windows Specific:
For platform dependant, but default library options (though you may still consider them overkills) can be found here: Getting cursor position in Python.
Using PyMouse:
>>> import pymouse
>>> mouse = pymouse.PyMouse()
>>> mouse.position()
(231L, 479L)
As an example, for plot or images, it is possible to use the matplotlib tool called ginput.
At every click of the mouse the [x,y] coordinates of the selected point are stored in a variable.
# show image
fig, ax=plt.subplots()
ax.imshow(img)
# select point
yroi = plt.ginput(0,0)
using ginput(0,0) you can select any points on the plot or image.
here the link for the ginput documentation
https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.ginput.html
I made this the other day.
It a function to get color or pos on right click / left click:
#Add Any helpfull stuff in functions here for later use
def GetMouseInfos(WhatToGet="leaving emety will get you x and y", GetXOnly=False, GetYOnly=False, GetColor=False, Key='Right', OverrideKey=False):#gets color of whats under Key cursor on right click
try:
import win32api
except ModuleNotFoundError:
print("win32api not found, to install do pip install pywin32")
try:
import time
except ModuleNotFoundError:
print("time not found, to install do pip install time?")
try:
import pyautogui
except ModuleNotFoundError:
print("py auto gui not found, to install do pip install pyautogui")
#--------------------------------------------------------------
#above checks if needed modules are installed if not tells user
#code below is to get all varibles needed
#---------------------------------------------------------------
print(WhatToGet)
if OverrideKey:
Key_To_click = Key
if Key == 'Left':
Key_To_click = 0x01
if Key == 'Right':
Key_To_click = 0x02
if Key == 'Wheel':
Key_To_click = 0x04
state_left = win32api.GetKeyState(Key_To_click) # Left button up = 0 or 1. Button down = -127 or -128
IsTrue = True
while IsTrue:
a = win32api.GetKeyState(Key_To_click)
if a != state_left: # Button state changed
state_left = a
if a < 0:
global Xpos, Ypos
Xpos, Ypos = win32api.GetCursorPos()
x, y = pyautogui.position()
pixelColor = pyautogui.screenshot().getpixel((x, y))
else:
posnowX, posnowY = win32api.GetCursorPos()
win32api.SetCursorPos((posnowX, posnowY))
IsTrue = False#remove this for it to keep giving coords on click without it just quitting after 1 click
time.sleep(0.001)
#--------------------------------------------------------------------
#The Code above is the code to get all varibles and code below is for the user to get what he wants
#--------------------------------------------------------------------
if GetXOnly: #Checks if we should get Only X (def options) the command to do this would be GetKeyInfos("Click To get X ONLY", True)
if GetYOnly:
return(Xpos , Ypos)
if GetColor:
return(Xpos, pixelColor)
return(Xpos)
if GetYOnly: #Checks if we should get Only Y (def options) the command to do this would be GetKeyInfos("Click To get X ONLY",False, True)
if GetXOnly:
return(Xpos , Ypos)
if GetColor:
return(Ypos, pixelColor)
return(Ypos)
if GetColor:
return(pixelColor) #Checks
return(Xpos, Ypos)
# getKeyinfos("Anything here without any other guidelines will give u x and y only on right click")
Use pygame
import pygame
mouse_pos = pygame.mouse.get_pos()
This returns the x and y position of the mouse.
See this website: https://www.pygame.org/docs/ref/mouse.html#pygame.mouse.set_pos
Here is an example for canvas with tkinter:
def callback(event):
print("clicked at: ", event.x, event.y)
canvas.bind("<Button-1>", callback)
For turtle :
def get_mouse_click_coor(x, y):
print(x, y)
turtle.onscreenclick(get_mouse_click_coor)
Capture the coordinates (x,y) of the mouse, when clicking with the left button, without using Tkinter?
It's simple:
Install pynput (use pip install pynput (without the 'i').
Copy and paste this code into your editor:
from pynput.mouse import Listener
def on_click(x, y, button, pressed):
x = x
y = y
print('X =', x, '\nY =', y)
with Listener(on_click=on_click) as listener:
listener.join()
I hope this help you =D
You all are making it too hard, its just as easy as:
import pyautogui as pg
pos = pg.position()
# for x pos
print(pos[0])
# for y pos
print(pos[1])

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