pyautoguiを【超適当に】マルチディスプレイ環境に対応させる Part2

前回の記事:[pyautoguiを【超適当に】マルチディスプレイ環境に対応させる Part1](https://qiita.com/kznSk2/items/a6833c095aec3b8ce72e)

# 目的
pyautogui.locateOnScreen()で得た座標を、そのままpyautogui.click()に渡したい。

# 環境
python 3.8.5
pyautogui 0.9.50
pyscreez 0.1.26
OS windows限定
[win32api](https://github.com/mhammond/pywin32/releases)
上記はpipからインストールできないので、リンクから自分の環境にあったものをダウンロードしてインストールする必要がある。

# 調査
locateOnXXX()系の関数は大体locateOnScreen()を呼び出しているので、この関数を修正する。

~~~python:pyscreez\__init__.py

def locateOnScreen(image, minSearchTime=0, **kwargs):
    """TODO - rewrite this
    minSearchTime - amount of time in seconds to repeat taking
    screenshots and trying to locate a match.  The default of 0 performs
    a single search.
    """
    start = time.time()
    while True:
        try:
            screenshotIm = screenshot(region=None) # the locateAll() function must handle cropping to return accurate coordinates, so don't pass a region here.
            retVal = locate(image, screenshotIm, **kwargs)
            try:
                screenshotIm.fp.close()
            except AttributeError:
                # Screenshots on Windows won't have an fp since they came from
                # ImageGrab, not a file. Screenshots on Linux will have fp set
                # to None since the file has been unlinked
                pass
            if retVal or time.time() - start > minSearchTime:
                return retVal
        except ImageNotFoundException:
            if time.time() - start > minSearchTime:
                if USE_IMAGE_NOT_FOUND_EXCEPTION:
                    raise
                else:
                    return None
~~~

# 実装内容

## win32apiをインポート
インポート箇所でプラットフォームがwin32と判定されてる場所ならどこでも良いです。
私は一番最初の25行目以降の箇所に記載しました。

~~~python:pyscreez\__init__.py
    from PIL import Image
    from PIL import ImageOps
    from PIL import ImageDraw
    if sys.platform == 'win32': # TODO - Pillow now supports ImageGrab on macOS.
        # win32apiのインポートを追加
        import win32api
        from PIL import ImageGrab
    _PILLOW_UNAVAILABLE = False
~~~

## locateOnScreen()の修正
retValに画像認識とマッチしたleft,top,width,heightの座標及び値を格納しているので、戻り値として返す前にretValの中身をちょちょいと書き換える。

~~~python:pyscreez\__init__.py

def locateOnScreen(image, minSearchTime=0, **kwargs):
    """TODO - rewrite this
    minSearchTime - amount of time in seconds to repeat taking
    screenshots and trying to locate a match.  The default of 0 performs
    a single search.
    """
    start = time.time()
    while True:
        try:
            screenshotIm = screenshot(region=None) # the locateAll() function must handle cropping to return accurate coordinates, so don't pass a region here.
            retVal = locate(image, screenshotIm, **kwargs)
            # ここから
            if not(retVal == None) and sys.platform == 'win32':
                displays = win32api.EnumDisplayMonitors()
                left_min = min([display[2][0] for display in displays])
                top_min = min([display[2][1] for display in displays])
                retVal = Box(
                    left = retVal[0] + left_min,
                    top = retVal[1] + top_min,
                    width = retVal[2],
                    height = retVal[3]
                )
            # ここまで追加分
            try:
                screenshotIm.fp.close()
            except AttributeError:
                # Screenshots on Windows won't have an fp since they came from
                # ImageGrab, not a file. Screenshots on Linux will have fp set
                # to None since the file has been unlinked
                pass
            if retVal or time.time() - start > minSearchTime:
                return retVal
        except ImageNotFoundException:
            if time.time() - start > minSearchTime:
                if USE_IMAGE_NOT_FOUND_EXCEPTION:
                    raise
                else:
                    return None
~~~

## _screenshot_win32()の修正内容の確認
[前回の記事](https://qiita.com/kznSk2/items/a6833c095aec3b8ce72e)で修正した_screenshot_win32()も、念の為確認。

~~~python:pyscreez\__init__.py
def _screenshot_win32(imageFilename=None, region=None):
    """
    TODO
    """

    # TODO - Use the winapi to get a screenshot, and compare performance with ImageGrab.grab()
    # https://stackoverflow.com/a/3586280/1893164
    #im = ImageGrab.grab()
    im = ImageGrab.grab(all_screens=True)

    if region is not None:
        assert len(region) == 4, 'region argument must be a tuple of four ints'
        region = [int(x) for x in region]
        im = im.crop((region[0], region[1], region[2] + region[0], region[3] + region[1]))
    if imageFilename is not None:
        im.save(imageFilename)
    return im
~~~

# 結果
pyautogui.locateOnScreen()で取得した座標をpyautogui.click()で使えるようになった。

# 問題点
## 画像認識速度が落ちる
当然といえば当然ではあるのですが、2つなら画像サイズは倍3つなら3倍になるので、それに比例するように画像認識速度が落ちる。
pyautoguiが公式にマルチディスプレイに対応しないのも、この速度の問題が大きいんじゃないかなというのが個人的な感想。
## pyscreezを参照する他のパッケージに影響が出る
他に参照しているパッケージがあるのかどうかは知りませんが、直接書き換える関係上現行の環境に影響がないとは言い切れません。
お遊び環境で使うに留めましょう、間違っても現状運用している重要なシステム環境等でやることではないです。
そんな人はいないと思いますが。
# 最後に
速度低下の問題が大きいので、allscreensよりは特定のdisplayを指定出来るような改修の方が運用には現実的というのが結論です。

というわけで超適当なマルチディスプレイ対応でした。
暇があればそっちの方もやってみたいですね。