From 00c8f96b03ac1ed9aa0f90612c1896d90398e1aa Mon Sep 17 00:00:00 2001 From: Darth-Veitcher Date: Tue, 2 Jul 2019 12:43:40 +0100 Subject: [PATCH 1/9] remove exception and import macos --- src/pygetwindow/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pygetwindow/__init__.py b/src/pygetwindow/__init__.py index 235c596..4834993 100644 --- a/src/pygetwindow/__init__.py +++ b/src/pygetwindow/__init__.py @@ -34,7 +34,9 @@ def pointInRect(x, y, left, top, width, height): if sys.platform == 'darwin': - raise NotImplementedError('PyGetWindow currently does not support macOS. If you have Appkit/Cocoa knowledge, please contribute! https://github.com/asweigart/pygetwindow') # TODO - implement mac + # raise NotImplementedError('PyGetWindow currently does not support macOS. If you have Appkit/Cocoa knowledge, please contribute! https://github.com/asweigart/pygetwindow') # TODO - implement mac + from ._pygetwindow_macos import MacOSWindow, getActiveWindow, getWindowsAt, getAllTitles, activate + Window = MacOSWindow elif sys.platform == 'win32': from ._pygetwindow_win import Win32Window, getActiveWindow, getWindowsAt, getWindowsWithTitle, getAllWindows, getAllTitles Window = Win32Window From 3c2e6cd956360c72ae6ca5bb2f90d21bf9b29f7d Mon Sep 17 00:00:00 2001 From: Darth-Veitcher Date: Tue, 2 Jul 2019 12:44:13 +0100 Subject: [PATCH 2/9] initial commit --- src/pygetwindow/_pygetwindow_macos.py | 45 +++++++++++++++++++-------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/pygetwindow/_pygetwindow_macos.py b/src/pygetwindow/_pygetwindow_macos.py index d890dba..394580b 100644 --- a/src/pygetwindow/_pygetwindow_macos.py +++ b/src/pygetwindow/_pygetwindow_macos.py @@ -1,11 +1,12 @@ import Quartz +import AppKit import pygetwindow +import pyrect def getAllTitles(): """Returns a list of strings of window titles for all visible windows. """ - # Source: https://stackoverflow.com/questions/53237278/obtain-list-of-all-window-titles-on-macos-from-a-python-script/53985082#53985082 windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) return ['%s %s' % (win[Quartz.kCGWindowOwnerName], win.get(Quartz.kCGWindowName, '')) for win in windows] @@ -13,12 +14,11 @@ def getAllTitles(): def getActiveWindow(): """Returns a Window object of the currently active Window.""" - # Source: https://stackoverflow.com/questions/5286274/front-most-window-using-cgwindowlistcopywindowinfo windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) for win in windows: if win['kCGWindowLayer'] == 0: - return '%s %s' % (win[Quartz.kCGWindowOwnerName], win.get(Quartz.kCGWindowName, '')) # Temporary. For now, we'll just return the title of the active window. + return MacOSWindow(win['kCGWindowNumber']) raise Exception('Could not find an active window.') # Temporary hack. @@ -33,16 +33,21 @@ def getWindowsAt(x, y): -def activate(): - # TEMP - this is not a real api, I'm just using this name to store these notes for now. +def activate(title): + """ Uses the `activateWithOptions_` to bring the window to the foreground. - # Source: https://stackoverflow.com/questions/7460092/nswindow-makekeyandorderfront-makes-window-appear-but-not-key-or-front?rq=1 - # Source: https://stackoverflow.com/questions/4905024/is-it-possible-to-bring-window-to-front-without-taking-focus?rq=1 - pass + See https://developer.apple.com/documentation/appkit/nsrunningapplication?language=objc + """ + windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) + for win in windows: + if title in '%s %s' % (win[Quartz.kCGWindowOwnerName], win.get(Quartz.kCGWindowName, '')): + ap = AppKit.NSRunningApplication.\ + runningApplicationWithProcessIdentifier_(win['kCGWindowOwnerPID']) + if not ap.isActive(): + ap.activateWithOptions_(AppKit.NSApplicationActivateIgnoringOtherApps) def getWindowGeometry(title): - # TEMP - this is not a real api, I'm just using this name to stoe these notes for now. windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) for win in windows: if title in '%s %s' % (win[Quartz.kCGWindowOwnerName], win.get(Quartz.kCGWindowName, '')): @@ -51,7 +56,6 @@ def getWindowGeometry(title): def isVisible(title): - # TEMP - this is not a real api, I'm just using this name to stoe these notes for now. windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) for win in windows: if title in '%s %s' % (win[Quartz.kCGWindowOwnerName], win.get(Quartz.kCGWindowName, '')): @@ -65,11 +69,26 @@ def isMinimized(): # I'm not sure how kCGWindowListOptionOnScreenOnly interferes with this. pass -# TODO: This class doesn't work yet. I've copied the Win32Window class and will make adjustments as needed here. +def _getWindowRect(hWnd): + """Returns `Rect` for specified MacOS window based on CGWindowID""" + # hWnd equivalent in MacOS is CGWindowID + # https://developer.apple.com/documentation/coregraphics/cgwindowid?language=objc + windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) + for win in windows: + if hWnd == win['kCGWindowNumber']: + w = win['kCGWindowBounds'] + return pygetwindow.Rect( + left=w['X'], + top=w['Y'], + right=w['X']+w['Width'], + bottom=w['Y']+w['Height']) + class MacOSWindow(): - def __init__(self, hWnd): - self._hWnd = hWnd # TODO fix this, this is a LP_c_long insead of an int. + def __init__(self, hWnd=None): + # hWnd equivalent in MacOS is CGWindowID + # https://developer.apple.com/documentation/coregraphics/cgwindowid?language=objc + self._hWnd = hWnd def _onRead(attrName): r = _getWindowRect(self._hWnd) From 34b06927e534b77a7053fb9f6e7d8ad8bab4bd2f Mon Sep 17 00:00:00 2001 From: Darth-Veitcher Date: Tue, 2 Jul 2019 12:44:56 +0100 Subject: [PATCH 3/9] ignore vscode --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0418182..5bd05e1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,7 @@ htmlcov/ dist/ build/ -*.egg-info/ \ No newline at end of file +*.egg-info/ + +# IDE +.vscode \ No newline at end of file From 6c6e3517af0b3fa1568098b60b24e32c056b219b Mon Sep 17 00:00:00 2001 From: Darth-Veitcher Date: Tue, 2 Jul 2019 13:36:10 +0100 Subject: [PATCH 4/9] add application and activate functionality --- src/pygetwindow/_pygetwindow_macos.py | 45 ++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/src/pygetwindow/_pygetwindow_macos.py b/src/pygetwindow/_pygetwindow_macos.py index 394580b..cc1a99c 100644 --- a/src/pygetwindow/_pygetwindow_macos.py +++ b/src/pygetwindow/_pygetwindow_macos.py @@ -69,6 +69,23 @@ def isMinimized(): # I'm not sure how kCGWindowListOptionOnScreenOnly interferes with this. pass + +def _getWindowByTitle(title, exact=False): + """Returns a MacOSWindow object for matched title. + + :param exact: Whether to only return where title is an exact match. + """ + windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) + for win in windows: + if exact: + if (title == win[Quartz.kCGWindowOwnerName]) or \ + (title == win.get(Quartz.kCGWindowName, '')): + return MacOSWindow(win['kCGWindowNumber']) + if title in '%s %s' % (win[Quartz.kCGWindowOwnerName], win.get(Quartz.kCGWindowName, '')): + return MacOSWindow(win['kCGWindowNumber']) + raise Exception('Could not find a matching window.') # HACK: Temporary hack. + + def _getWindowRect(hWnd): """Returns `Rect` for specified MacOS window based on CGWindowID""" # hWnd equivalent in MacOS is CGWindowID @@ -89,6 +106,7 @@ def __init__(self, hWnd=None): # hWnd equivalent in MacOS is CGWindowID # https://developer.apple.com/documentation/coregraphics/cgwindowid?language=objc self._hWnd = hWnd + self._app = None def _onRead(attrName): r = _getWindowRect(self._hWnd) @@ -104,6 +122,7 @@ def _onChange(oldBox, newBox): r = _getWindowRect(self._hWnd) self._rect = pyrect.Rect(r.left, r.top, r.right - r.left, r.bottom - r.top, onChange=_onChange, onRead=_onRead) + def __str__(self): r = _getWindowRect(self._hWnd) width = r.right - r.left @@ -116,7 +135,26 @@ def __repr__(self): def __eq__(self, other): - return isinstance(other, Win32Window) and self._hWnd == other._hWnd + return isinstance(other, MacOSWindow) and self._hWnd == other._hWnd + + + @property + def app(self): + """Represents a running associated NSApplication instance.""" + if self._app: + return self._app + else: + windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) + for win in windows: + if self._hWnd == win['kCGWindowNumber']: + ap = AppKit.NSRunningApplication.\ + runningApplicationWithProcessIdentifier_(win['kCGWindowOwnerPID']) + self._app = ap + return ap + + @property + def isActive(self): + return self.app.active def close(self): @@ -146,9 +184,8 @@ def restore(self): def activate(self): """Activate this window and make it the foreground window.""" - result = ctypes.windll.user32.SetForegroundWindow(self._hWnd) - if result == 0: - _raiseWithLastError() + if not self.isActive: + self.app.activateWithOptions_(AppKit.NSApplicationActivateIgnoringOtherApps) def resizeRel(self, widthOffset, heightOffset): From cc48ef16f565b3f2da3b72543e1ce3b16502fd3f Mon Sep 17 00:00:00 2001 From: Darth-Veitcher Date: Sat, 6 Jul 2019 21:47:51 +0100 Subject: [PATCH 5/9] skip windows tests if not on windows --- Pipfile | 1 + Pipfile.lock | 94 +++++++++++++++++- .../PyGetWindow.dist-info/AUTHORS.txt | 8 ++ .../PyGetWindow.dist-info/LICENSE.txt | 27 +++++ .../PyGetWindow.dist-info/METADATA | 99 +++++++++++++++++++ .../PyGetWindow.dist-info/top_level.txt | 1 + src/pygetwindow/_pygetwindow_macos.py | 41 ++++---- tests/test_pygetwindow.py | 3 + 8 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 pip-wheel-metadata/PyGetWindow.dist-info/AUTHORS.txt create mode 100644 pip-wheel-metadata/PyGetWindow.dist-info/LICENSE.txt create mode 100644 pip-wheel-metadata/PyGetWindow.dist-info/METADATA create mode 100644 pip-wheel-metadata/PyGetWindow.dist-info/top_level.txt diff --git a/Pipfile b/Pipfile index 96dd2b7..10966c1 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ name = "pypi" "win32gui" = "*" [dev-packages] +pylint = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index cfa0f77..df3a48b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "7784f0f6975b90e48343668fa565820995cdf41c5caf637ef05ca7c417bf7005" + "sha256": "b905477b61f0aba6e3199a051711fcc955f4804e82823f3ba859a7b4eafd9a5b" }, "pipfile-spec": 6, "requires": { - "python_version": "3.7" + "python_version": "3.6" }, "sources": [ { @@ -38,5 +38,93 @@ "version": "==221.6" } }, - "develop": {} + "develop": { + "astroid": { + "hashes": [ + "sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4", + "sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4" + ], + "version": "==2.2.5" + }, + "isort": { + "hashes": [ + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + ], + "version": "==4.3.21" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661", + "sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f", + "sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13", + "sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821", + "sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71", + "sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e", + "sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea", + "sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229", + "sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4", + "sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e", + "sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20", + "sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16", + "sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b", + "sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7", + "sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c", + "sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a", + "sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e", + "sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1" + ], + "version": "==1.4.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pylint": { + "hashes": [ + "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", + "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" + ], + "index": "pypi", + "version": "==2.3.1" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "typed-ast": { + "hashes": [ + "sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", + "sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", + "sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", + "sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", + "sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", + "sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", + "sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", + "sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", + "sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", + "sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", + "sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", + "sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", + "sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", + "sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", + "sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12" + ], + "markers": "implementation_name == 'cpython'", + "version": "==1.4.0" + }, + "wrapt": { + "hashes": [ + "sha256:3a5bcada415eb6e2ed3a12b912813a0dda62a687c46349a6b94cea230dec038c", + "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" + ], + "version": "==1.11.2" + } + } } diff --git a/pip-wheel-metadata/PyGetWindow.dist-info/AUTHORS.txt b/pip-wheel-metadata/PyGetWindow.dist-info/AUTHORS.txt new file mode 100644 index 0000000..c56c6ae --- /dev/null +++ b/pip-wheel-metadata/PyGetWindow.dist-info/AUTHORS.txt @@ -0,0 +1,8 @@ +Here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- +people who have submitted patches, reported bugs, added translations, helped +answer newbie questions, and generally made PyGetWindow that much better: + +Al Sweigart https://github.com/asweigart/ +Kudria https://github.com/Kudria +Randall White +Ronald Oussoren \ No newline at end of file diff --git a/pip-wheel-metadata/PyGetWindow.dist-info/LICENSE.txt b/pip-wheel-metadata/PyGetWindow.dist-info/LICENSE.txt new file mode 100644 index 0000000..ccfe620 --- /dev/null +++ b/pip-wheel-metadata/PyGetWindow.dist-info/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2015, Al Sweigart +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the PyAutoGUI nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pip-wheel-metadata/PyGetWindow.dist-info/METADATA b/pip-wheel-metadata/PyGetWindow.dist-info/METADATA new file mode 100644 index 0000000..c9f250f --- /dev/null +++ b/pip-wheel-metadata/PyGetWindow.dist-info/METADATA @@ -0,0 +1,99 @@ +Metadata-Version: 2.1 +Name: PyGetWindow +Version: 0.0.6 +Summary: A simple, cross-platform module for obtaining GUI information on application's windows. +Home-page: https://github.com/asweigart/pygetwindow +Author: Al Sweigart +Author-email: al@inventwithpython.com +License: BSD +Keywords: gui window geometry resize minimize maximize close title +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Win32 (MS Windows) +Classifier: Environment :: X11 Applications +Classifier: Environment :: MacOS X +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Description-Content-Type: text/markdown +Requires-Dist: pyrect + +PyGetWindow +=========== + +A simple, cross-platform module for obtaining GUI information on and controlling application's windows. + + +Still under development. Currently only the Windows platform is implemented. If you want to help contribute, please contact al@inventwithpython.com! + + +Install +------- + + pip install pygetwindow + + +Examples +-------- + +(For this example, I'm using Winodws and opened the Notepad application, which has a title of "Untitled - Notepad". Most of the effects of these functions can't be seen in text.) + +PyGetWindow has functions for obtaining ``Window`` objects from a place on the screen, from the window title, or just getting all windows. (``hWnd`` is specific to the Windows platform.) + + >>> import pygetwindow as gw + >>> gw.getAllTitles() + ('', 'C:\\WINDOWS\\system32\\cmd.exe - pipenv shell - python', 'C:\\github\\PyGetWindow\\README.md • - Sublime Text', "asweigart/PyGetWindow: A simple, cross-platform module for obtaining GUI information on application's windows. - Google Chrome", 'Untitled - Notepad', 'C:\\Users\\Al\\Desktop\\xlibkey.py • - Sublime Text', 'https://tronche.com/gui/x/xlib/ - Google Chrome', 'Xlib Programming Manual: XGetWindowAttributes - Google Chrome', 'Generic Ubuntu Box [Running] - Oracle VM VirtualBox', 'Oracle VM VirtualBox Manager', 'Microsoft Edge', 'Microsoft Edge', 'Microsoft Edge', '', 'Microsoft Edge', 'Settings', 'Settings', 'Microsoft Store', 'Microsoft Store', '', '', 'Backup and Sync', 'Google Hangouts - asweigart@gmail.com', 'Downloads', '', '', 'Program Manager') + >>> gw.getAllWindows() + (Win32Window(hWnd=131318), Win32Window(hWnd=1050492), Win32Window(hWnd=67206), Win32Window(hWnd=66754), Win32Window(hWnd=264354), Win32Window(hWnd=329210), Win32Window(hWnd=1114374), Win32Window(hWnd=852550), Win32Window(hWnd=328358), Win32Window(hWnd=66998), Win32Window(hWnd=132508), Win32Window(hWnd=66964), Win32Window(hWnd=66882), Win32Window(hWnd=197282), Win32Window(hWnd=393880), Win32Window(hWnd=66810), Win32Window(hWnd=328466), Win32Window(hWnd=132332), Win32Window(hWnd=262904), Win32Window(hWnd=65962), Win32Window(hWnd=65956), Win32Window(hWnd=197522), Win32Window(hWnd=131944), Win32Window(hWnd=329334), Win32Window(hWnd=395034), Win32Window(hWnd=132928), Win32Window(hWnd=65882)) + >>> gw.getWindowsWithTitle('Untitled') + (Win32Window(hWnd=264354),) + >>> gw.getFocusedWindow() + Win32Window(hWnd=1050492) + >>> gw.getFocusedWindow().title + 'C:\\WINDOWS\\system32\\cmd.exe - pipenv shell - python' + >>> gw.getWindowsAt(10, 10) + (Win32Window(hWnd=67206), Win32Window(hWnd=66754), Win32Window(hWnd=329210), Win32Window(hWnd=1114374), Win32Window(hWnd=852550), Win32Window(hWnd=132508), Win32Window(hWnd=66964), Win32Window(hWnd=66882), Win32Window(hWnd=197282), Win32Window(hWnd=393880), Win32Window(hWnd=66810), Win32Window(hWnd=328466), Win32Window(hWnd=395034), Win32Window(hWnd=132928), Win32Window(hWnd=65882)) + + +``Window`` objects can be minimized/maximized/restored/focused/resized/moved/closed and also have attributes for their current position, size, and state. + + >>> notepadWindow = gw.getWindowsWithTitle('Untitled')[0] + >>> notepadWindow.isMaximized + False + >>> notepadWindow.maximize() + >>> notepadWindow.isMaximized + True + >>> notepadWindow.restore() + >>> notepadWindow.minimize() + >>> notepadWindow.restore() + >>> notepadWindow.focus() + >>> notepadWindow.resize(10, 10) # increase by 10, 10 + >>> notepadWindow.resizeTo(100, 100) # set size to 100x100 + >>> notepadWindow.move(10, 10) # move 10 pixels right and 10 down + >>> notepadWindow.moveTo(10, 10) # move window to 10, 10 + >>> notepadWindow.size + (132, 100) + >>> notepadWindow.width + 132 + >>> notepadWindow.height + 100 + >>> notepadWindow.topleft + (10, 10) + >>> notepadWindow.top + 10 + >>> notepadWindow.left + 10 + >>> notepadWindow.bottomright + (142, 110) + >>> notepadWindow.close() + >>> + + diff --git a/pip-wheel-metadata/PyGetWindow.dist-info/top_level.txt b/pip-wheel-metadata/PyGetWindow.dist-info/top_level.txt new file mode 100644 index 0000000..392e30f --- /dev/null +++ b/pip-wheel-metadata/PyGetWindow.dist-info/top_level.txt @@ -0,0 +1 @@ +pygetwindow diff --git a/src/pygetwindow/_pygetwindow_macos.py b/src/pygetwindow/_pygetwindow_macos.py index cc1a99c..b49a17a 100644 --- a/src/pygetwindow/_pygetwindow_macos.py +++ b/src/pygetwindow/_pygetwindow_macos.py @@ -32,19 +32,13 @@ def getWindowsAt(x, y): return matches - def activate(title): """ Uses the `activateWithOptions_` to bring the window to the foreground. See https://developer.apple.com/documentation/appkit/nsrunningapplication?language=objc """ - windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) - for win in windows: - if title in '%s %s' % (win[Quartz.kCGWindowOwnerName], win.get(Quartz.kCGWindowName, '')): - ap = AppKit.NSRunningApplication.\ - runningApplicationWithProcessIdentifier_(win['kCGWindowOwnerPID']) - if not ap.isActive(): - ap.activateWithOptions_(AppKit.NSApplicationActivateIgnoringOtherApps) + w = _getWindowByTitle(title) + w.activate() def getWindowGeometry(title): @@ -61,13 +55,11 @@ def isVisible(title): if title in '%s %s' % (win[Quartz.kCGWindowOwnerName], win.get(Quartz.kCGWindowName, '')): return win['kCGWindowAlpha'] != 0.0 -def isMinimized(): - # TEMP - this is not a real api, I'm just using this name to stoe these notes for now. - # Source: https://stackoverflow.com/questions/10258676/how-to-know-whether-a-window-is-minimised-or-not - # Use the kCGWindowIsOnscreen to check this. Minimized windows are considered to not be on the screen. (But I'm not sure if there are other situations where a window is "off screen".) - # I'm not sure how kCGWindowListOptionOnScreenOnly interferes with this. - pass +def isMinimized(title): + # https://developer.apple.com/documentation/appkit/nsrunningapplication/1525949-hidden?language=objc + w = _getWindowByTitle(title) + return w.app.hidden def _getWindowByTitle(title, exact=False): @@ -121,6 +113,7 @@ def _onChange(oldBox, newBox): r = _getWindowRect(self._hWnd) self._rect = pyrect.Rect(r.left, r.top, r.right - r.left, r.bottom - r.top, onChange=_onChange, onRead=_onRead) + self.__get_kCGWindow_dict() def __str__(self): @@ -138,6 +131,14 @@ def __eq__(self, other): return isinstance(other, MacOSWindow) and self._hWnd == other._hWnd + def __get_kCGWindow_dict(self): + """Sets the specific keys returned by Quartz for later re-use""" + windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) + for win in windows: + if self._hWnd == win['kCGWindowNumber']: + for key, value in win.items(): + setattr(self, key, value) + @property def app(self): """Represents a running associated NSApplication instance.""" @@ -162,19 +163,21 @@ def close(self): quit?" dialogs or other actions that prevent the window from actually closing. This is identical to clicking the X button on the window.""" - result = ctypes.windll.user32.PostMessageA(self._hWnd, WM_CLOSE, 0, 0) - if result == 0: - _raiseWithLastError() + result = self.app.terminate() + if not result: + raise Exception("Unable to terminate application") def minimize(self): """Minimizes this window.""" - ctypes.windll.user32.ShowWindow(self._hWnd, SW_MINIMIZE) + # https://developer.apple.com/documentation/appkit/nsrunningapplication/1526608-hide?language=objc + self.app.hide() def maximize(self): """Maximizes this window.""" - ctypes.windll.user32.ShowWindow(self._hWnd, SW_MAXIMIZE) + self.app.unhide() + self.activate() def restore(self): diff --git a/tests/test_pygetwindow.py b/tests/test_pygetwindow.py index add8b92..99753f4 100644 --- a/tests/test_pygetwindow.py +++ b/tests/test_pygetwindow.py @@ -9,6 +9,9 @@ RUNNING_PYTHON_2 = sys.version_info[0] == 2 +if sys.platform != 'win32': + pytest.skip("skipping windows-only tests", allow_module_level=True) + try: if RUNNING_PYTHON_2: import Tkinter as tk From 330517e174fc10dc2668624bbce6a2e2567d315b Mon Sep 17 00:00:00 2001 From: Darth-Veitcher Date: Sat, 6 Jul 2019 21:48:43 +0100 Subject: [PATCH 6/9] ignore pip-wheel-metadata --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5bd05e1..bd7a677 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ htmlcov/ dist/ build/ *.egg-info/ +pip-wheel-metadata/ # IDE .vscode \ No newline at end of file From ca038cde663a5d6d8f3f644bddfabbbd30f5fc6a Mon Sep 17 00:00:00 2001 From: Darth-Veitcher Date: Mon, 22 Jul 2019 11:23:00 +0100 Subject: [PATCH 7/9] remove pip-wheel-metadata --- .../PyGetWindow.dist-info/AUTHORS.txt | 8 -- .../PyGetWindow.dist-info/LICENSE.txt | 27 ----- .../PyGetWindow.dist-info/METADATA | 99 ------------------- .../PyGetWindow.dist-info/top_level.txt | 1 - src/pygetwindow/_pygetwindow_macos.py | 37 ++++--- 5 files changed, 17 insertions(+), 155 deletions(-) delete mode 100644 pip-wheel-metadata/PyGetWindow.dist-info/AUTHORS.txt delete mode 100644 pip-wheel-metadata/PyGetWindow.dist-info/LICENSE.txt delete mode 100644 pip-wheel-metadata/PyGetWindow.dist-info/METADATA delete mode 100644 pip-wheel-metadata/PyGetWindow.dist-info/top_level.txt diff --git a/pip-wheel-metadata/PyGetWindow.dist-info/AUTHORS.txt b/pip-wheel-metadata/PyGetWindow.dist-info/AUTHORS.txt deleted file mode 100644 index c56c6ae..0000000 --- a/pip-wheel-metadata/PyGetWindow.dist-info/AUTHORS.txt +++ /dev/null @@ -1,8 +0,0 @@ -Here is an inevitably incomplete list of MUCH-APPRECIATED CONTRIBUTORS -- -people who have submitted patches, reported bugs, added translations, helped -answer newbie questions, and generally made PyGetWindow that much better: - -Al Sweigart https://github.com/asweigart/ -Kudria https://github.com/Kudria -Randall White -Ronald Oussoren \ No newline at end of file diff --git a/pip-wheel-metadata/PyGetWindow.dist-info/LICENSE.txt b/pip-wheel-metadata/PyGetWindow.dist-info/LICENSE.txt deleted file mode 100644 index ccfe620..0000000 --- a/pip-wheel-metadata/PyGetWindow.dist-info/LICENSE.txt +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2015, Al Sweigart -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the PyAutoGUI nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pip-wheel-metadata/PyGetWindow.dist-info/METADATA b/pip-wheel-metadata/PyGetWindow.dist-info/METADATA deleted file mode 100644 index c9f250f..0000000 --- a/pip-wheel-metadata/PyGetWindow.dist-info/METADATA +++ /dev/null @@ -1,99 +0,0 @@ -Metadata-Version: 2.1 -Name: PyGetWindow -Version: 0.0.6 -Summary: A simple, cross-platform module for obtaining GUI information on application's windows. -Home-page: https://github.com/asweigart/pygetwindow -Author: Al Sweigart -Author-email: al@inventwithpython.com -License: BSD -Keywords: gui window geometry resize minimize maximize close title -Platform: UNKNOWN -Classifier: Development Status :: 4 - Beta -Classifier: Environment :: Win32 (MS Windows) -Classifier: Environment :: X11 Applications -Classifier: Environment :: MacOS X -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Description-Content-Type: text/markdown -Requires-Dist: pyrect - -PyGetWindow -=========== - -A simple, cross-platform module for obtaining GUI information on and controlling application's windows. - - -Still under development. Currently only the Windows platform is implemented. If you want to help contribute, please contact al@inventwithpython.com! - - -Install -------- - - pip install pygetwindow - - -Examples --------- - -(For this example, I'm using Winodws and opened the Notepad application, which has a title of "Untitled - Notepad". Most of the effects of these functions can't be seen in text.) - -PyGetWindow has functions for obtaining ``Window`` objects from a place on the screen, from the window title, or just getting all windows. (``hWnd`` is specific to the Windows platform.) - - >>> import pygetwindow as gw - >>> gw.getAllTitles() - ('', 'C:\\WINDOWS\\system32\\cmd.exe - pipenv shell - python', 'C:\\github\\PyGetWindow\\README.md • - Sublime Text', "asweigart/PyGetWindow: A simple, cross-platform module for obtaining GUI information on application's windows. - Google Chrome", 'Untitled - Notepad', 'C:\\Users\\Al\\Desktop\\xlibkey.py • - Sublime Text', 'https://tronche.com/gui/x/xlib/ - Google Chrome', 'Xlib Programming Manual: XGetWindowAttributes - Google Chrome', 'Generic Ubuntu Box [Running] - Oracle VM VirtualBox', 'Oracle VM VirtualBox Manager', 'Microsoft Edge', 'Microsoft Edge', 'Microsoft Edge', '', 'Microsoft Edge', 'Settings', 'Settings', 'Microsoft Store', 'Microsoft Store', '', '', 'Backup and Sync', 'Google Hangouts - asweigart@gmail.com', 'Downloads', '', '', 'Program Manager') - >>> gw.getAllWindows() - (Win32Window(hWnd=131318), Win32Window(hWnd=1050492), Win32Window(hWnd=67206), Win32Window(hWnd=66754), Win32Window(hWnd=264354), Win32Window(hWnd=329210), Win32Window(hWnd=1114374), Win32Window(hWnd=852550), Win32Window(hWnd=328358), Win32Window(hWnd=66998), Win32Window(hWnd=132508), Win32Window(hWnd=66964), Win32Window(hWnd=66882), Win32Window(hWnd=197282), Win32Window(hWnd=393880), Win32Window(hWnd=66810), Win32Window(hWnd=328466), Win32Window(hWnd=132332), Win32Window(hWnd=262904), Win32Window(hWnd=65962), Win32Window(hWnd=65956), Win32Window(hWnd=197522), Win32Window(hWnd=131944), Win32Window(hWnd=329334), Win32Window(hWnd=395034), Win32Window(hWnd=132928), Win32Window(hWnd=65882)) - >>> gw.getWindowsWithTitle('Untitled') - (Win32Window(hWnd=264354),) - >>> gw.getFocusedWindow() - Win32Window(hWnd=1050492) - >>> gw.getFocusedWindow().title - 'C:\\WINDOWS\\system32\\cmd.exe - pipenv shell - python' - >>> gw.getWindowsAt(10, 10) - (Win32Window(hWnd=67206), Win32Window(hWnd=66754), Win32Window(hWnd=329210), Win32Window(hWnd=1114374), Win32Window(hWnd=852550), Win32Window(hWnd=132508), Win32Window(hWnd=66964), Win32Window(hWnd=66882), Win32Window(hWnd=197282), Win32Window(hWnd=393880), Win32Window(hWnd=66810), Win32Window(hWnd=328466), Win32Window(hWnd=395034), Win32Window(hWnd=132928), Win32Window(hWnd=65882)) - - -``Window`` objects can be minimized/maximized/restored/focused/resized/moved/closed and also have attributes for their current position, size, and state. - - >>> notepadWindow = gw.getWindowsWithTitle('Untitled')[0] - >>> notepadWindow.isMaximized - False - >>> notepadWindow.maximize() - >>> notepadWindow.isMaximized - True - >>> notepadWindow.restore() - >>> notepadWindow.minimize() - >>> notepadWindow.restore() - >>> notepadWindow.focus() - >>> notepadWindow.resize(10, 10) # increase by 10, 10 - >>> notepadWindow.resizeTo(100, 100) # set size to 100x100 - >>> notepadWindow.move(10, 10) # move 10 pixels right and 10 down - >>> notepadWindow.moveTo(10, 10) # move window to 10, 10 - >>> notepadWindow.size - (132, 100) - >>> notepadWindow.width - 132 - >>> notepadWindow.height - 100 - >>> notepadWindow.topleft - (10, 10) - >>> notepadWindow.top - 10 - >>> notepadWindow.left - 10 - >>> notepadWindow.bottomright - (142, 110) - >>> notepadWindow.close() - >>> - - diff --git a/pip-wheel-metadata/PyGetWindow.dist-info/top_level.txt b/pip-wheel-metadata/PyGetWindow.dist-info/top_level.txt deleted file mode 100644 index 392e30f..0000000 --- a/pip-wheel-metadata/PyGetWindow.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -pygetwindow diff --git a/src/pygetwindow/_pygetwindow_macos.py b/src/pygetwindow/_pygetwindow_macos.py index b49a17a..f76a687 100644 --- a/src/pygetwindow/_pygetwindow_macos.py +++ b/src/pygetwindow/_pygetwindow_macos.py @@ -82,7 +82,8 @@ def _getWindowRect(hWnd): """Returns `Rect` for specified MacOS window based on CGWindowID""" # hWnd equivalent in MacOS is CGWindowID # https://developer.apple.com/documentation/coregraphics/cgwindowid?language=objc - windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) + # windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) + windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements, Quartz.kCGNullWindowID) for win in windows: if hWnd == win['kCGWindowNumber']: w = win['kCGWindowBounds'] @@ -182,7 +183,7 @@ def maximize(self): def restore(self): """If maximized or minimized, restores the window to it's normal size.""" - ctypes.windll.user32.ShowWindow(self._hWnd, SW_RESTORE) + self.maximize() def activate(self): @@ -193,41 +194,36 @@ def activate(self): def resizeRel(self, widthOffset, heightOffset): """Resizes the window relative to its current size.""" - result = ctypes.windll.user32.SetWindowPos(self._hWnd, HWND_TOP, self.left, self.top, self.width + widthOffset, self.height + heightOffset, 0) - if result == 0: - _raiseWithLastError() + raise NotImplementedError("Method not implemented in MacOS") def resizeTo(self, newWidth, newHeight): """Resizes the window to a new width and height.""" - result = ctypes.windll.user32.SetWindowPos(self._hWnd, HWND_TOP, self.left, self.top, newWidth, newHeight, 0) - if result == 0: - _raiseWithLastError() + raise NotImplementedError("Method not implemented in MacOS") def moveRel(self, xOffset, yOffset): """Moves the window relative to its current position.""" - result = ctypes.windll.user32.SetWindowPos(self._hWnd, HWND_TOP, self.left + xOffset, self.top + yOffset, self.width, self.height, 0) - if result == 0: - _raiseWithLastError() + raise NotImplementedError("Method not implemented in MacOS") def moveTo(self, newLeft, newTop): """Moves the window to new coordinates on the screen.""" - result = ctypes.windll.user32.SetWindowPos(self._hWnd, HWND_TOP, newLeft, newTop, self.width, self.height, 0) - if result == 0: - _raiseWithLastError() + raise NotImplementedError("Method not implemented in MacOS") @property def isMinimized(self): """Returns True if the window is currently minimized.""" - return ctypes.windll.user32.IsIconic(self._hWnd) != 0 + try: + return self.app.hidden + except AttributeError: + return False @property def isMaximized(self): """Returns True if the window is currently maximized.""" - return ctypes.windll.user32.IsZoomed(self._hWnd) != 0 + raise NotImplementedError("Method not implemented in MacOS") @property def isActive(self): @@ -237,13 +233,14 @@ def isActive(self): @property def title(self): """Returns the window title as a string.""" - return _getWindowText(self._hWnd) + return self.kCGWindowName @property def visible(self): - return isWindowVisible(self._hWnd) - - + windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements, Quartz.kCGNullWindowID) + for win in windows: + if self._hWnd == win[Quartz.kCGWindowNumber]: + return win['kCGWindowAlpha'] != 0.0 # Wrappers for pyrect.Rect object's properties. @property From a618fad448df914fb6994b4a3e040abcdd495136 Mon Sep 17 00:00:00 2001 From: Darth-Veitcher Date: Mon, 22 Jul 2019 11:24:00 +0100 Subject: [PATCH 8/9] add tests for macos --- tests/test_pygetwindow_macos.py | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/test_pygetwindow_macos.py diff --git a/tests/test_pygetwindow_macos.py b/tests/test_pygetwindow_macos.py new file mode 100644 index 0000000..e87792b --- /dev/null +++ b/tests/test_pygetwindow_macos.py @@ -0,0 +1,64 @@ +from __future__ import division, print_function + +import pytest +import pygetwindow + +import sys + + +RUNNING_PYTHON_2 = sys.version_info[0] == 2 + +if sys.platform != 'darwin': + pytest.skip("skipping macos-only tests", allow_module_level=True) + + +def test_get_all_titles(): + """Should return a list of window titles""" + wl = pygetwindow.getAllTitles() + assert len(wl) > 0 + assert isinstance(wl[0], str) + + +def test_getActiveWindow(): + """Should return a Window object of the currently active Window.""" + w = pygetwindow.getActiveWindow() + assert isinstance(w, pygetwindow.Window) + + +def test_windowHasBounds(): + """Should return a Window object of the currently active Window.""" + w = pygetwindow.getActiveWindow() + # Check bounds have been set + bounds = [ + 'area', + 'bottom', + 'bottomleft', + 'bottomright', + 'box', + 'center', + 'centerx', + 'centery', + 'height', + 'left', + 'midbottom', + 'midleft', + 'midright', + 'midtop', + 'right', + 'size', + 'top', + 'topleft', + 'topright', + 'width', + ] + for k in bounds: + assert hasattr(w, k) + v = getattr(w, k) + assert(v != None) + + +def test_window_geometry(): + """Tests that we can find a logical center of the window""" + w = pygetwindow.getActiveWindow() + assert w.centerx == int(w.box.width / 2 + w.box.left) + assert w.centery == int(w.box.height / 2 + w.box.top) From b8685b95e264649feb6698586e4acf31f39447dc Mon Sep 17 00:00:00 2001 From: Darth-Veitcher Date: Mon, 22 Jul 2019 11:44:52 +0100 Subject: [PATCH 9/9] update README --- README.md | 20 ++++++++++++++++++++ src/pygetwindow/__init__.py | 1 + src/pygetwindow/_pygetwindow_macos.py | 26 ++++++++++++++++++++------ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 16b2d54..070b2b1 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,23 @@ PyGetWindow has functions for obtaining ``Window`` objects from a place on the s (142, 110) >>> notepadWindow.close() >>> + +## MacOS +MacOS implementation is not yet feature complete and is experimental so YMMV. However, the following is possible. + + >>> import pygetwindow as gw + >>> gw.getAllTitles() + ['SystemUIServer AppleClockExtra', 'OneDrive Item-0', 'SystemUIServer AirPortExtra', 'MindNode Quick Entry Item-0', 'Tunnelblick Item-0', 'ClickShare Client Item-0', 'Docker Item-0', 'OneDrive Item-0', 'Alfred 3 Item-0', 'VMware Fusion Start Menu Item-0', 'SystemUIServer AppleTimeMachineExtra', 'SystemUIServer AppleVPNExtra', 'SystemUIServer AppleBluetoothExtra', 'SystemUIServer BatteryExtra', 'SystemUIServer AppleVolumeExtra', 'Spotlight Item-0', 'SystemUIServer NotificationCenter', 'Window Server Menubar', 'Dock Dock', 'Code README.md — PyGetWindow', "Safari darth-veitcher/PyGetWindow: A simple, cross-platform module for obtaining GUI information on application's windows.", 'Amazon WorkSpaces Amazon WorkSpaces', 'Slack Slack - MyCompany', 'Finder Data Driven Organisation', 'Microsoft OneNote myname @ MyCompany O365', 'Terminal public — core@asimov:~/coco-annotator — python -m http.server — 161×53', 'Terminal myname — jupyter_mac.command — -bash — 80×24'] + >>> gw.getAllWindows() + [MacOSWindow(hWnd=13408), MacOSWindow(hWnd=53), MacOSWindow(hWnd=3854), MacOSWindow(hWnd=45), MacOSWindow(hWnd=103), MacOSWindow(hWnd=100), MacOSWindow(hWnd=89), MacOSWindow(hWnd=106), MacOSWindow(hWnd=3852), MacOSWindow(hWnd=87), MacOSWindow(hWnd=85), MacOSWindow(hWnd=41), MacOSWindow(hWnd=61), MacOSWindow(hWnd=37), MacOSWindow(hWnd=49), MacOSWindow(hWnd=57), MacOSWindow(hWnd=35), MacOSWindow(hWnd=24), MacOSWindow(hWnd=3), MacOSWindow(hWnd=33), MacOSWindow(hWnd=13264), MacOSWindow(hWnd=13320), MacOSWindow(hWnd=6359), MacOSWindow(hWnd=8566), MacOSWindow(hWnd=9331), MacOSWindow(hWnd=6585), MacOSWindow(hWnd=74), MacOSWindow(hWnd=77)] + +As with Windows implementation, MacOS windows can be activated and minimised. + + >>> safari = gw.getWindowsWithTitle('Safari')[0] + >>> safari.isActive + False + >>> safari.activate() + >>> safari.activate(); time.sleep(3); safari.isActive + True + >>> safari.size + Size(width=870.0, height=560.0) diff --git a/src/pygetwindow/__init__.py b/src/pygetwindow/__init__.py index 4834993..41d90b6 100644 --- a/src/pygetwindow/__init__.py +++ b/src/pygetwindow/__init__.py @@ -36,6 +36,7 @@ def pointInRect(x, y, left, top, width, height): if sys.platform == 'darwin': # raise NotImplementedError('PyGetWindow currently does not support macOS. If you have Appkit/Cocoa knowledge, please contribute! https://github.com/asweigart/pygetwindow') # TODO - implement mac from ._pygetwindow_macos import MacOSWindow, getActiveWindow, getWindowsAt, getAllTitles, activate + from ._pygetwindow_macos import _getWindowsByTitle as getWindowsWithTitle, _getAllWindows as getAllWindows Window = MacOSWindow elif sys.platform == 'win32': from ._pygetwindow_win import Win32Window, getActiveWindow, getWindowsAt, getWindowsWithTitle, getAllWindows, getAllTitles diff --git a/src/pygetwindow/_pygetwindow_macos.py b/src/pygetwindow/_pygetwindow_macos.py index f76a687..0e88410 100644 --- a/src/pygetwindow/_pygetwindow_macos.py +++ b/src/pygetwindow/_pygetwindow_macos.py @@ -37,7 +37,7 @@ def activate(title): See https://developer.apple.com/documentation/appkit/nsrunningapplication?language=objc """ - w = _getWindowByTitle(title) + w = _getWindowsByTitle(title)[0] w.activate() @@ -58,23 +58,37 @@ def isVisible(title): def isMinimized(title): # https://developer.apple.com/documentation/appkit/nsrunningapplication/1525949-hidden?language=objc - w = _getWindowByTitle(title) + w = _getWindowsByTitle(title)[0] return w.app.hidden -def _getWindowByTitle(title, exact=False): - """Returns a MacOSWindow object for matched title. +def _getAllWindows(): + """Returns a list of MacOSWindow objects for all visible windows""" + matched = [] + windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) + for win in windows: + matched.append(MacOSWindow(win['kCGWindowNumber'])) + if len(matched) > 0: + return matched + raise Exception('Could not find a window.') # HACK: Temporary hack. + + +def _getWindowsByTitle(title, exact=False): + """Returns a list of MacOSWindow objects that substring match the title. :param exact: Whether to only return where title is an exact match. """ + matched = [] windows = Quartz.CGWindowListCopyWindowInfo(Quartz.kCGWindowListExcludeDesktopElements | Quartz.kCGWindowListOptionOnScreenOnly, Quartz.kCGNullWindowID) for win in windows: if exact: if (title == win[Quartz.kCGWindowOwnerName]) or \ (title == win.get(Quartz.kCGWindowName, '')): - return MacOSWindow(win['kCGWindowNumber']) + matched.append(MacOSWindow(win['kCGWindowNumber'])) if title in '%s %s' % (win[Quartz.kCGWindowOwnerName], win.get(Quartz.kCGWindowName, '')): - return MacOSWindow(win['kCGWindowNumber']) + matched.append(MacOSWindow(win['kCGWindowNumber'])) + if len(matched) > 0: + return matched raise Exception('Could not find a matching window.') # HACK: Temporary hack.