[MacBook] Pythonでウィンドウを操作する(PyWinCtl)

2022.05.03

こんにちは、CX事業本部 IoT事業部の若槻です。

今回は、Pythonを使ってMacBook上で軽いRPA的なこと(ウィンドウの操作)がしたかったので、方法を調べてみました。

環境

$ hostinfo
Mach kernel version:
         Darwin Kernel Version 20.6.0: Mon Aug 30 06:12:21 PDT 2021; root:xnu-7195.141.6~3/RELEASE_X86_64
Kernel configured for up to 8 processors.
4 processors are physically available.
4 processors are logically available.
Processor type: x86_64h (Intel x86-64h Haswell)
Processors active: 0 1 2 3 4 5 6 7
Primary memory available: 16.00 gigabytes
Default processor set: 517 tasks, 2476 threads, 8 processors
Load average: 4.55, Mach factor: 4.66

$ python --version
Python 3.9.6

やってみた

今回試してみたのはPyWinCtlです。

PyWinCtlはPyGetWindowのフォークです。PyGetWindowでサポートしているのはMS Windowsのみですが、PyWinCtlではMacOSやLinuxについてもサポート対象となっています。

インストール、Permission設定

PyWinCtlをインスールします。(環境によってはインストール量が結構多くなります。)

$ pip install pywinctl

Pythonを実行するアプリケーションにMacのアクセス権限を付与します。今回はVS Codeです。

$ python
>>> import pywinctl as pwc

# Permission設定前ならFalseになる。
>>> pwc.checkPermissions()
False

# 適当なコマンドでPermission設定を出す。
>>> pwc.getActiveWindow()

# Permission設定済みならTrueになる。
>>> pwc.checkPermissions()
True

次のようなPermission設定が出るので許可します。

ウィンドウを操作してみる

現在、次のようにFinderアプリのウィンドウがアクティブかつ最大化となっています。また外付けディスプレイを2枚使用しています。

現在アクティブなウィンドウの一覧です。MacBook本体+外付け2枚で合計3つのアクティブウィンドウが取得できています。

>>> pwc.getAllWindows()
[MacOSWindow(hWnd=<NSRunningApplication: 0x7fb2ac068bb0 (com.microsoft.VSCode - 796) LSASN:{hi=0x0;lo=0x24024}>), MacOSWindow(hWnd=<NSRunningApplication: 0x7fb2ac0b4ed0 (com.apple.finder - 803) LSASN:{hi=0x0;lo=0x2e02e}>), MacOSWindow(hWnd=<NSRunningApplication: 0x7fb2ac0cdf00 (com.google.Chrome - 28691) LSASN:{hi=0x0;lo=0x192192}>)]

getAllTitlesで、現在アクティブなウィンドウのタイトル一覧を取得できます。Finderでデスクトップを開いているのでDesktopとなっています。

>>> pwc.getAllTitles()
['Python — devio', 'Desktop', 'クラスメソッド発「やってみた」系技術メディア | DevelopersIO - Google Chrome']

getWindowsWithTitleで、タイトルを指定して特定のウィンドウを取得できます。

>>> pwc.getWindowsWithTitle('Desktop')
[MacOSWindow(hWnd=<NSRunningApplication: 0x7fb2ac0b4ed0 (com.apple.finder - 803) LSASN:{hi=0x0;lo=0x2e02e}>)]

restoreで、ウィンドウの最大化を解除できます。

>>> win = pwc.getWindowsWithTitle('Desktop')[0]
>>> win.restore()
True

初回の場合はPremissionを求められます。

最大化を解除した状態であれば、hideで、ウィンドウを最小化できます。

>>> win.hide()
False

maximizeで、ウィンドウを最大化できます。

>>> win.maximize()
True

activateで、指定のウィンドウをアクティブにできます。

>>> win.activate()
True

例えば実行前のアクティブウィンドウがGoogle Chromeなら、

実行するとFinder(デスクトップ)に切り替わります。

注意点

アクティブでないウィンドウは取得できない

ちなみにアクティブでないウィンドウはgetWindowsWithTitleを使っても取得できません。

# Finder(デスクトップ)が非アクティブの場合
>>> desktop = pwc.getWindowsWithTitle('Desktop')[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

よって、スクリプトなどで自動化したい場合はあらかじめpickleでファイル化するなどしてウィンドウのオブジェクトを保持しておきたいところですが、Objective-Cオブジェクトのため出来ないようです。

>>> import pickle
>>> with open('pic.bin', 'wb') as p:
...     pickle.dump(skitch, p)
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: Cannot pickle Objective-C objects

よってウィンドウオブジェクトの保存時/復元時にPyObjCなどを使用してObjective-Cオブジェクトとバイナリ間でブリッジしてあげる必要がありそうです。

おわりに

Pythonを使ってMacBook上で軽いRPA的なこと(ウィンドウの操作)がしたかったので、方法を調べてみました。

当初はさほど難しくないと思っていましたが、Apple製品特有のPermissionのガードの高さゆえ、同じPythonライブラリを使用して他のOSで出来ることがMacOSでは出来ないと、いうことが調べていてよくありました。注意したいですね。

以上