Closed nishyp closed 1 year ago
Hi! Thank you for your interest and your feedback!
Regarding test_MacOSNSWindow.py
, this is my output in Catalina:
Hello, World! <NSRect origin=<NSPoint x=400.0 y=800.0> size=<NSSize width=250.0 height=122.0>> <NSPoint x=400.0 y=800.0> <NSSize width=250.0 height=122.0> <class 'NSRect'> <class 'NSPoint'> --> My objects are NS*, whilst yours are CG*
ACTIVE WINDOW: Hello, World!
RESIZE Size(width=600, height=400)
RESIZEREL Size(width=610, height=420)
MOVE Point(x=600, y=300) --> My output stands that the window will be moved to position (600, 300), whilst yours says (600, 743)
... --> My output continues, with no errors
EDIT: I first thought the problem was related to NS (in my case) vs. CG (in your case) objects... but after updating my version (3.9) to your very same version (3.10.9), I now get the same objects types, but doesn't fail in my case. The difference about the target point is still there. In my system the target is (600, 300), whilst in yours it's (600, 743). Let me investigate a little bit to try to understand. It's possible I have to use you as tester, if you agree, since I don't know how to reproduce it.
In the mean time, can you please tell me your screen resolution, check if your python is 32 or 64 bits, and run this on a terminal?:
python3 -m pip install pywinctl --force-reinstall
this last will assure we have the same version of all related packages.
Thanks again!
Thanks for getting back to me on this, I'm more than happy to be a tester!
I've gone ahead and done the force reinstall and can confirm that python is 64bit retesting also brings the same result
My screen info: I'm running 3x Dell U2412Mb monitors The resolution is 1920 x 1200 for each one
Attached is the layout in case that's needed also
wow! THREE monitors!!! I will definitely not be able to reproduce it!! HAHAHAHA!
First, when you have the time (of course no obligation, no hurries), please try this very simple script (it's just to check what might be happening with the screen positions in your case). Please pay visual attention where the window is placed in every one of the three steps (there is a print() statement to signal them) and check if the window is placed where it's expected.
import sys
import time
from AppKit import (
NSApp, NSObject, NSApplication, NSMakeRect, NSWindow, NSWindowStyleMaskTitled, NSWindowStyleMaskClosable,
NSWindowStyleMaskMiniaturizable, NSWindowStyleMaskResizable, NSBackingStoreBuffered, NSMakeRect)
import pywinctl
print(pywinctl.getAllScreens())
def _moveTo(handle, newLeft: int, newTop: int, width, height):
handle.setFrame_display_animate_(
NSMakeRect(newLeft, pywinctl.getScreenSize().height - newTop - height, width, height),
True, True)
def _moveToUnflipped(handle, newLeft: int, newTop: int, width, height):
handle.setFrame_display_animate_(NSMakeRect(newLeft, newTop, width, height), True, True)
class Delegate(NSObject):
npw = None
demoMode = False
def getDemoMode(self):
return self.demoMode
def setDemoMode(self):
self.demoMode = True
def unsetDemoMode(self):
self.demoMode = False
def applicationDidFinishLaunching_(self, aNotification: None):
NSApp().activateIgnoringOtherApps_(True)
for win in NSApp().orderedWindows():
print(win.title(), win.frame(), type(win.frame().origin))
if self.demoMode:
if not self.npw:
self.npw = pywinctl.getActiveWindow(NSApp())
if self.npw:
print("ACTIVE WINDOW:", self.npw.title)
else:
print("NO ACTIVE WINDOW FOUND")
return
print("BEFORE MOVE", self.npw.box, "Window should be at the left and near the bottom")
time.sleep(1)
_moveToUnflipped(self.npw.getHandle(), 1800, 50, self.npw.width, self.npw.height)
time.sleep(1)
print("AFTER MOVE", self.npw.box, "Window should be at the right and near the bottom")
time.sleep(1)
_moveTo(self.npw.getHandle(), 50, 50, self.npw.width, self.npw.height)
time.sleep(1)
print("AFTER FLIPPED MOVE", self.npw.box, "Window should be at left and near the top")
def windowWillClose_(self, aNotification: None):
print("Window has been closed")
NSApp().terminate_(self)
def windowDidBecomeKey_(self, aNotification: None):
print("Now I'm ACTIVE")
def demo():
a = NSApplication.sharedApplication()
delegate = Delegate.alloc().init()
delegate.setDemoMode()
a.setDelegate_(delegate)
frame = NSMakeRect(50, 50, 250, 100)
mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable
w = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(frame, mask, NSBackingStoreBuffered, False)
w.setDelegate_(delegate)
w.setTitle_(u'Hello, World!')
w.orderFrontRegardless()
w.display()
a.run()
if __name__ == '__main__':
demo()
Second, disconnect two of your monitors (please include the one which is vertical), leaving just one, and re-test everything, I mean test_pywinctl.py
, test_MacOSNSWindow.py
and the script above, and let me know what happens!
Thank you!!!
Hi! Sorry to bother you. I just wanted to check if you had the chance to test this so we can try to solve it and include the potential solution in the next release. Thank you!
This is with a single screen
So I just tried the script you provided by copy/pasting into a file called test.py
{'DELL U2412M': {'id': 724063885, 'is_primary': True, 'pos': Point(x=0, y=0), 'size': Size(width=1920, height=1200), 'workarea': Rect(left=0, top=0, right=1920, bottom=1177), 'scale': (100, 100), 'dpi': (72, 72), 'orientation': 0, 'frequency': 0.0, 'colordepth': 32}}
Hello, World! <Foundation.NSRect origin=<Foundation.NSPoint x=50.0 y=50.0> size=<Foundation.NSSize width=250.0 height=122.0>> <class 'Foundation.NSPoint'>
ACTIVE WINDOW: Hello, World!
BEFORE MOVE Box(left=50, top=1028, width=250, height=122) Window should be at the left and near the bottom
AFTER MOVE Box(left=1800, top=1028, width=250, height=122) Window should be at the right and near the bottom
AFTER FLIPPED MOVE Box(left=50, top=50, width=250, height=122) Window should be at left and near the top
Now I'm ACTIVE
and once I close the box
Window has been closed
test_pywinctl.py
- this didn't show any info when just typing python3 PATH
so I did
python3 -m pytest -vv PATH
================================================================================================= test session starts ==================================================================================================
platform darwin -- Python 3.10.9, pytest-7.2.1, pluggy-1.0.0 -- /Library/Frameworks/Python.framework/Versions/3.10/bin/python3
cachedir: .pytest_cache
rootdir: /Users/myusername/Downloads/PyWinCtl-0.0.42-test
collected 1 item
Downloads/PyWinCtl-0.0.42-test/tests/test_pywinctl.py::test_basic PASSED [100%]
============================================================================================ 1 passed in 107.68s (0:01:47) =============================================================================================
test_MacNSWindow.py
Hello, World! <Foundation.NSRect origin=<Foundation.NSPoint x=400.0 y=800.0> size=<Foundation.NSSize width=250.0 height=122.0>>
ACTIVE WINDOW: Hello, World!
RESIZE Size(width=600, height=400)
RESIZEREL Size(width=610, height=420)
MOVE Point(x=600, y=300)
MOVEREL Point(x=601, y=302)
RESIZE Size(width=601, height=401)
MOVE Point(x=100, y=600)
MOVE Point(x=200, y=600)
MOVE Point(x=-401, y=600)
MOVE Point(x=-401, y=200)
MOVE Point(x=-401, y=399)
MOVE Point(x=300, y=400)
MOVE Point(x=-301, y=400)
MOVE Point(x=300, y=299)
MOVE Point(x=-301, y=499)
MOVE Point(x=300, y=200)
MOVE Point(x=-301, y=200)
MOVE Point(x=0, y=400)
MOVE Point(x=0, y=299)
MOVE Point(x=0, y=200)
MOVE Point(x=700, y=200)
MOVE Point(x=700, y=100)
RESIZE Size(width=600, height=401)
LOWER
RAISE
ALWAYS ON TOP
DEACTIVATE AOT
ALWAYS AT BOTTOM
DEACTIVATE AOB
SEND BEHIND
BRING FROM BEHIND
GET PARENT
HIDE
SHOW
CLOSE
Window has been closed
Window has been closed
This is with all screens connected for the test.py
{'DELL U2412M': {'id': 724063885, 'is_primary': False, 'pos': Point(x=0, y=-1200), 'size': Size(width=1920, height=1200), 'workarea': Rect(left=0, top=-1200, right=1920, bottom=1200), 'scale': (100, 100), 'dpi': (72, 72), 'orientation': 0, 'frequency': 0.0, 'colordepth': 32}}
Hello, World! <Foundation.NSRect origin=<Foundation.NSPoint x=50.0 y=50.0> size=<Foundation.NSSize width=250.0 height=122.0>> <class 'Foundation.NSPoint'>
ACTIVE WINDOW: Hello, World!
BEFORE MOVE Box(left=50, top=1028, width=250, height=122) Window should be at the left and near the bottom
AFTER MOVE Box(left=1800, top=1028, width=250, height=122) Window should be at the right and near the bottom
AFTER FLIPPED MOVE Box(left=50, top=50, width=250, height=122) Window should be at left and near the top
Now I'm ACTIVE
once i close the box
Window has been closed
Window has been closed
Thank you!
First bug I can detect is that in getAllScreens() I have to add the index to the monitor name or, in case the physical names of the monitors are the same (which is your case), its info it's overwritten (this is why there is just one monitor in the dict output you pasted).
Second, if I am interpreting correctly the output with just one monitor plugged, it is working OK with one monitor. If so, I think the issue is here:
{'DELL U2412M': {'id': 724063885, 'is_primary': False, 'pos': Point(x=0, y=-1200), 'size': Size(width=1920, height=1200), 'workarea': Rect(left=0, top=-1200, right=1920, bottom=1200), 'scale': (100, 100), 'dpi': (72, 72), 'orientation': 0, 'frequency': 0.0, 'colordepth': 32}}
More specifically, here: Point(x=0, y=-1200)
. One of your monitors starts in Y coordinate -1200.
Then, the problem is how the bottom property is calculated in PyRect (external module used by PyWinCtl):
@property
def bottom(self):
"""The y coordinate for the bottom edge of the rectangle.
>>> r = Rect(0, 0, 10, 20)
>>> r.bottom
20
>>> r.bottom = 30
>>> r
Rect(left=0, top=10, width=10, height=20)
"""
if self.onRead is not None:
self.onRead(BOTTOM)
return self._top + self._height
If top is 300 and height is 122, the result is 422, what is right. But what happens if top is -300? The result is -188, what it's wrong... Since PyRect is not intended for multi-monitor setups, at this moment I see no other alternative apart from replacing PyRect by custom code.
Besides, if you intend to work with PyWinCtl in a multi-monitor setup, and once I figure out how to solve the issue above, you have to bear in mind these display positions. Let me explain myself in detail.
math.copysign(1, displayX) * (abs(displayX) + targetX), math.copysign(1, displayY) * (abs(displayY) + targetY) = 100, -1300
getAllScreens()
, to gather the monitors coordinates, window.box
(or window.left
, window.top
, ...) to gather the actual window position, and finally putting it all together using window.getDisplay()
to know in which monitor the window is placed.Please, let me know if you need further help on this.
Hi again!
I have a preliminary version of the module in which I replaced PyRect by a (hopefully) multi-monitor custom class. If you have the time, I would really appreciate if you could test this new version (of course, no obligation, no hurries...). I'm attaching this new wheel so you can uninstall previous pywinctl version (python3 -m pip uninstall pywinctl
), install this new one (python3 -m pip install wherever/you/placed/it/PyWinCtl-0.1-py3-none-any.whl
) and run test_pywinctl.py
and test_MacNSWindow.py
again.
Thanks a lot for your help!
I apologise for the delay in getting back to you! I've downloaded 0.1 and ran the tests from 0.0.43
Here are the results
test_pywinctl.py
==================================================================== test session starts ====================================================================
platform darwin -- Python 3.10.9, pytest-7.2.1, pluggy-1.0.0 -- /Library/Frameworks/Python.framework/Versions/3.10/bin/python3.10
cachedir: .pytest_cache
rootdir: /Users/myusername/Downloads/PyWinCtl-0.0.43
collected 1 item
Downloads/PyWinCtl-0.0.43/tests/test_pywinctl.py::test_basic FAILED [100%]
========================================================================= FAILURES ==========================================================================
________________________________________________________________________ test_basic _________________________________________________________________________
def test_basic():
if sys.platform == "win32":
subprocess.Popen('notepad')
time.sleep(0.5)
testWindows = [pywinctl.getActiveWindow()]
# testWindows = pywinctl.getWindowsWithTitle('Untitled - Notepad') # Not working in other languages
assert len(testWindows) == 1
npw = testWindows[0]
basic_win32(npw)
elif sys.platform == "linux":
subprocess.Popen('gedit')
time.sleep(5)
testWindows = [pywinctl.getActiveWindow()]
assert len(testWindows) == 1
npw = testWindows[0]
basic_linux(npw)
elif sys.platform == "darwin":
if not pywinctl.checkPermissions(activate=True):
exit()
subprocess.Popen(['touch', 'test.py'])
time.sleep(2)
subprocess.Popen(['open', '-a', 'TextEdit', 'test.py'])
time.sleep(5)
testWindows = pywinctl.getWindowsWithTitle('test.py')
assert len(testWindows) == 1
npw = testWindows[0]
assert isinstance(npw, pywinctl.Window)
npw = testWindows[0]
> basic_macOS(npw)
Downloads/PyWinCtl-0.0.43/tests/test_pywinctl.py:55:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
npw = MacOSWindow(hWnd=<NSRunningApplication: 0x7f85ee64a010 (com.apple.TextEdit - 53430) LSASN:{hi=0x0;lo=0x391391}>)
def basic_macOS(npw: pywinctl.Window):
assert npw is not None
wait = True
timelap = 0.50
# Test maximize/minimize/restore.
if npw.isMaximized: # Make sure it starts un-maximized
npw.restore(wait=wait)
assert not npw.isMaximized
npw.maximize(wait=wait)
time.sleep(timelap)
assert npw.isMaximized
npw.restore(wait=wait)
time.sleep(timelap)
assert not npw.isMaximized
npw.minimize(wait=wait)
time.sleep(timelap)
assert npw.isMinimized
npw.restore(wait=wait)
time.sleep(timelap)
assert not npw.isMinimized
# Test resizing
npw.resizeTo(600, 400, wait=wait)
time.sleep(timelap)
assert npw.size == (600, 400)
assert npw.width == 600
assert npw.height == 400
npw.resizeRel(10, 20, wait=wait)
assert npw.size == (610, 420)
assert npw.width == 610
assert npw.height == 420
# Test moving
npw.moveTo(50, 54, wait=wait)
assert npw.topleft == (50, 54)
assert npw.left == 50
assert npw.top == 54
assert npw.right == 660
assert npw.bottom == 474
assert npw.bottomright == (660, 474)
assert npw.bottomleft == (50, 474)
assert npw.topright == (660, 54)
npw.moveRel(1, 2, wait=wait)
assert npw.topleft == (51, 56)
assert npw.left == 51
assert npw.top == 56
assert npw.right == 661
assert npw.bottom == 476
assert npw.bottomright == (661, 476)
assert npw.bottomleft == (51, 476)
assert npw.topright == (661, 56)
# Move via the properties
npw.resizeTo(601, 401, wait=wait)
npw.moveTo(100, 250, wait=wait)
npw.left = 200
time.sleep(timelap)
assert npw.left == 200
npw.right = 200
time.sleep(timelap)
assert npw.right == 200
npw.top = 200
time.sleep(timelap)
assert npw.top == 200
npw.bottom = 800
time.sleep(timelap)
> assert npw.bottom == 800
E assert 904 == 800
E + where 904 = MacOSWindow(hWnd=<NSRunningApplication: 0x7f85ee64a010 (com.apple.TextEdit - 53430) LSASN:{hi=0x0;lo=0x391391}>).bottom
Downloads/PyWinCtl-0.0.43/tests/test_pywinctl.py:472: AssertionError
================================================================== short test summary info ==================================================================
FAILED Downloads/PyWinCtl-0.0.43/tests/test_pywinctl.py::test_basic - assert 904 == 800
=============================================================== 1 failed in 63.15s (0:01:03) ================================================================
test_MacNSWindow.py
Hello, World! <CoreFoundation.CGRect origin=<CoreFoundation.CGPoint x=400.0 y=800.0> size=<CoreFoundation.CGSize width=250.0 height=122.0>>
ACTIVE WINDOW: Hello, World!
RESIZE Size(width=600, height=400)
RESIZEREL Size(width=610, height=420)
MOVE Point(x=600, y=300)
MOVEREL Point(x=601, y=302)
RESIZE Size(width=601, height=401)
MOVE Point(x=100, y=600)
MOVE Point(x=200, y=600)
MOVE Point(x=-401, y=600)
MOVE Point(x=-401, y=200)
MOVE Point(x=-401, y=399)
MOVE Point(x=300, y=400)
MOVE Point(x=-301, y=400)
MOVE Point(x=300, y=299)
MOVE Point(x=-301, y=503)
2023-05-18 01:51:07.897 Python[53913:2000695] <class 'AssertionError'>:
Now I'm ACTIVE
Hi! Thank you SO MUCH! Please, no apologies at all!
Since then, I have been working in a totally new version (hopefully multi-monitor-ready... or near). I have also developed and uploaded a very preliminary, experimental new module (PyMonCtl, used by PyWinCtl) to handle monitors in multi-monitor (or single-monitor, of course) setups. If you can give a try to both, it would be very very very helpful!!!
New PyWinCtl-0.1 wheel: https://github.com/Kalmat/PyWinCtl/tree/dev/dist (not sure, but better download test folder from dev-0.1 branch, just to be sure there have been no changes since 0.0.43)
New PyMonCtl-0.0.6: Download/install directly from PyPi using pip (or from its github page: https://github.com/Kalmat/PyMonCtl/tree/master/dist). It has its own tests within tests
folder, just run python3 test_pymonctl.py
I couldn't test anything of this since I still have no access to an actual macOS setup, and VMs don't allow (or I don't know how to) set multiple monitors.
Thanks!
Hello, I'm fairly new to all this and I'm getting AssertionErrors when I run the two tests
System Info: MacOS Catalina 10.15.7 - 9700k, 64GB RAM, RX 5700 XT Python 3.10.9 PyWinCtl 0.0.42
When running test_MacNSWindow.py
When running test_pywinctl.py