【Python】スクリーンショットをマルチモニタ対応化

PILがマルチディスプレイに非対応

pythonのPILを使ってスクリーンショットを取るプログラムを作ったのだけど、メインディスプレイの範囲しか保存対象にならない!!
なぜかと思って色々調べてみたところ、PILモジュールのImageGrab.grab()自体がマルチモニタをサポートしてないようでした。

マルチディスプレイに対応させよう

対応させる方法はいろんなところに色々書かれてるけど、ぶっちゃけ素人だからもっとわかりやすく書いてくれ!!(記事は本当にありがとうございます!!!)
と思ったので、ないなら備忘録にもなるし自分でわかりやすいように書けば自分と同じ困っている初心者にはきっとわかりやすいはず。
ただし、この修正で他の部分に影響が出たらごめんなさい。

修正はたった2カ所!!

1カ所目変更箇所:
C:\Users\【ユーザー名】\AppData\Local\Programs\Python\Python38\Lib\site-packages\pyscreeze
ファイル名:【__init__.py】
記述箇所:450行目付近
変更後の記述内容

@requiresPillow
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(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

中央付近にある
im = ImageGrab.grab()

im = ImageGrab.grab(all_screens=True)
に書き換えただけです。

2カ所目変更箇所:
C:\Users\【ユーザー名】\AppData\Local\Programs\Python\Python38\Lib\site-packages\PIL
ファイル名:ImageGrab.py
記述箇所:28行目付近
変更後の記述内容

def grab(bbox=None, include_layered_windows=False, all_screens=True, xdisplay=None):
    if xdisplay is None:
        if sys.platform == "darwin":
            fh, filepath = tempfile.mkstemp(".png")
            os.close(fh)
            subprocess.call(["screencapture", "-x", filepath])
            im = Image.open(filepath)
            im.load()
            os.unlink(filepath)
            if bbox:
                im_cropped = im.crop(bbox)
                im.close()
                return im_cropped
            return im
        elif sys.platform == "win32":
            offset, size, data = Image.core.grabscreen_win32(
                include_layered_windows, all_screens
            )
            im = Image.frombytes(
                "RGB",
                size,
                data,
                # RGB, 32-bit line padding, origin lower left corner
                "raw",
                "BGR",
                (size[0] * 3 + 3) & -4,
                -1,
            )
            if bbox:
                x0, y0 = offset
                left, top, right, bottom = bbox
                im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
            return im
    # use xdisplay=None for default display on non-win32/macOS systems
    if not Image.core.HAVE_XCB:
        raise OSError("Pillow was built without XCB support")
    size, data = Image.core.grabscreen_x11(xdisplay)
    im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
    if bbox:
        im = im.crop(bbox)
    return im

最上段にある
def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=None):

def grab(bbox=None, include_layered_windows=False, all_screens=True, xdisplay=None):
に書き換えただけです。


実際に作ったコード

大したものではありませんが、実際に作って思うように動いてくれたコードです

import tkinter as tk
from PIL import ImageGrab
import os

# キャプチャ対象のウィンドウを作成
root = tk.Tk()

root.title("Capture Window")
root.wm_attributes("-transparentcolor", "red")
root.wm_attributes("-topmost", 1)
root.geometry("500x500")


# キャプチャ対象のフレームを作成
capture_frame = tk.Frame(root, bg="red")
capture_frame.pack(fill=tk.BOTH, expand=tk.YES)

# capture_frameの背景を透明にする
if tk.TkVersion >= 8.6:
    capture_frame.configure(bg='red')
else:
    capture_frame.configure(bg='red')

# スクリーンショットを撮る関数
def capture_screenshot():
    # キャプチャする領域を指定
    x = capture_frame.winfo_rootx()
    y = capture_frame.winfo_rooty()
    width = capture_frame.winfo_width()
    height = capture_frame.winfo_height()
    box = (x, y, x + width, y + height)

    # スクリーンショットを撮る
    image = ImageGrab.grab(bbox=box)

    # 保存するファイル名を決定する
    folder = os.path.join(os.path.expanduser('~'), 'Pictures', 'screenshots')
    if not os.path.exists(folder):
        os.makedirs(folder)
    num_existing_files = len(os.listdir(folder))
    
    file_name = "screenshot"+str(num_existing_files + 1 ) + ".png"
    for num in range(num_existing_files+5):
        if not file_name in os.listdir(folder):
            break
        else:
            file_name = "screenshot"+str(num_existing_files+ 1 + num )



    file_path = os.path.join(folder, f"screenshot{num_existing_files + 1 + num}.png")
    
    # スクリーンショットを保存する
    image.save(file_path)

    # 保存するファイル名を指定して保存
    #image.save("capture.png")


def open_my_documents():
    os.startfile(os.path.expanduser("~/Pictures/Screenshots"))


button1 = tk.Button(root, text="保存先を開く", command=open_my_documents)
button1.pack(side="right")

# スクリーンショットボタンを作成
capture_button = tk.Button(root, text=" 画像保存 ", command=capture_screenshot, font=("Arial", 13))
capture_button.pack(side="right")



root.mainloop()

動きとしては、

  • tkinterで枠を作成

  • 枠内を透明化することで疑似的に後ろに表示されているものをtkinter内に表示

  • ボタンを押すことでmypicture内のscreenshotsフォルダへ連番で画像を保存

  • 連番が中抜けになっていても上書きではない追加になるように(連番自体は無茶苦茶になってもいい)

  • おまけで保存先を開くコマンドも追加

  • アクティブではない時も常に前面に表示

なぜかわかりませんが、tkinterの透明化と常に前面表示をすることでtkinter枠内のクリックが貫通してオーバーレイのような動きになってしまいました。
使い勝手が良いためとりあえずそのままにしています。

もしかしたら

pyautoguiの画像認識でマルチディスプレイ環境下のサブディスプレイ側で動かない問題も解決するかもしれない
なんかあれが動かないのもImageGrabがメインディスプレイ側しか画像取得してないからとか書かれていた人がいた気がします。
ただ試せる環境と時間がないので、どなたか試していただければ幸いです
さて、今日も金型設計頑張ろう

この記事が気に入ったらサポートをしてみませんか?