pythonで簡単なスクリーンショットツールを作る

2018年3月19日月曜日

python windows

t f B! P L

スクリーンショットを撮りたい。pythonで作りたい。作ろう! というわけで作ります。

最終的に以下のような動きをするコードができる。

  1. スクリプトを起動する
  2. マウスで撮りたいウィンドウをクリックする
  3. 好きなだけprintscreenを押す
  4. するとディレクトリにjpgファイルが
  5. escで終了

大雑把にはこんな環境

  • python3.5
  • Windows10

pillow、keybaord、mouseというライブラリを使うよ!

とりあえずスクリーンショットを撮ってみる

まずは画面全体の画像を取得してみる。
ライブラリはPillowを使う。

pip install pillow
# -*- coding: utf-8 -*-


from PIL import ImageGrab

TARGET_NAME = ''

grabed_image = ImageGrab.grab()
grabed_image.save('ss.jpg')

実行すると画面全体のスクリーンショットがss.jpgという名前で保存される。

特定のウィンドウだけを撮る

画面全体ではなく、特定のウィンドウのスクリーンショットを保存したい。
ウィンドウの領域を取得して、全体からその領域を切り取るという方向でよさそう。

画像の切り抜きはImageオブジェクトにcropという関数がある。
領域の取得はwindowsのAPIを使う。pywin32が手軽。

pip install pywin32

FindWindowの2つめの引数にタイトルバーに表示される文字列を指定するとそのウィンドウのハンドルが返る。
GetWindowRectにハンドルを渡すとそのウィンドウのサイズがタプルで返ってくる。
返り値はleft, top, right, bottomの順で格納されている。

とりあえずElonaのssを撮ってみる。

# -*- coding: utf-8 -*-

import time

import win32gui
from PIL import ImageGrab

TARGET_NAME = 'Elona ver 1.22 ELM Release 60b'

time.sleep(1)  # この隙に移したいウィンドウを最前面に持ってくる

handle = win32gui.FindWindow(None, TARGET_NAME)
rect = win32gui.GetWindowRect(handle)

grabed_image = ImageGrab.grab()
croped_image = grabed_image.crop(rect)

croped_image.save('saved.jpg')

Elonaにしか使えない上にsleepで止めてgrabが発動する前にウィンドウを前に持ってくる操作技術が要求される使いづらいssツールができた。

環境によってはうまくいくこともあるが、今回は領域が惜しい。
Windows7以降のWindowsではエアロを有効にしているとGetWindowRectはウィンドウの影を含んだ領域を取得する。

*このスクリプトを初めて起動するとき、若干時間がかかることがある。

影の部分を取り除いたスクリーンショットを撮る

正しい領域を取得するにはDwmGetWindowAttributeを使う。
エアロ環境以外だと問題があるらしいが、こまけぇことはいいんだよ! ということでどうかよろしくおねがいします。 pywin32に見当たらなかったのでctypesを使う。
RECT構造体として受け取るのでタプルに直してからcropする。

# -*- coding: utf-8 -*-

import time
import ctypes
from ctypes import sizeof
from ctypes.wintypes import RECT

import win32gui
from PIL import ImageGrab

TARGET_NAME = 'Elona ver 1.22 ELM Release 60b'

time.sleep(1)  # この隙に移したいウィンドウを最前面に持ってくる

handle = win32gui.FindWindow(None, TARGET_NAME)

# rect = win32gui.GetWindowRect(handle)
rect = RECT()
DwmGetWindowAttribute = ctypes.windll.dwmapi.DwmGetWindowAttribute
DWMWA_EXTENDED_FRAME_BOUNDS = 9
DwmGetWindowAttribute(handle, DWMWA_EXTENDED_FRAME_BOUNDS,
                      rect, sizeof(rect))

# そのままrectをcropに渡したらTypeError: 'RECT' object is not iterableと言われる
rect = (rect.left, rect.top, rect.right, rect.bottom)

grabed_image = ImageGrab.grab()
croped_image = grabed_image.crop(rect)

croped_image.save('saved2.jpg')

ボーダーやタイトルバーを取り除きたいなどの理由がなければこれが期待するスクリーンショット。

最初にクリックしたウィンドウのssを特定のキーを押した時に撮る

もう少し使いやすいものにしたい。
バックグラウンドで動いていて、特定のキーを押すと動く。こういうのがほしい
これはフックの出番だ。
こういうときにkeyboardmouseという便利なライブラリがある。

pip install keyboard
pip install mouse

keyboardのサンプル。mouseも似たようなコードで動く。
私の環境だとsuppressあたりの動きが少々怪しかったがssツールを作る分には問題ない。
kとjを押すとabcやdefが出力される。escキーで終了。

import keyboard

keyboard.add_hotkey('k', print, args=('abc',), suppress=False)
keyboard.add_hotkey('j', print, args=('def',), suppress=True)
keyboard.wait('esc')

特定のキーを押したらスクリーンショットを撮るという処理はなんとかなりそう。
あとは撮りたいウィンドウのハンドルをどうするかである。
WindowsのAPIにはGetForegroundWindowという最前面のウィンドウのハンドルを取る関数がある。
マウスでクリックして、その時の最前面のウィンドウを取得。これでよさそう。
クリックするとき、どうしてもタスクバーをクリックしてしまうのでタイトルが無かったらもう一度待ち受けるという処理も必要。
あとmouseのwaitのtargettypeをupのみにしないとonclickが発動せずにwaitが終わる。

# -*- coding: utf-8 -*-

import datetime
import ctypes
from ctypes import sizeof
from ctypes.wintypes import RECT

import win32gui
from PIL import ImageGrab
import keyboard
import mouse


print('撮りたいウィンドウをクリック')
handle = None
text = ''


def mouse_callback():
    global handle
    global text
    handle = win32gui.GetForegroundWindow()
    text = win32gui.GetWindowText(handle)
mouse.on_click(mouse_callback)

while not text:
    mouse.wait(button='left', target_types=('up',))
print('このウィンドウを撮る: ' + text)
print('違うなら再起動')


rect = RECT()
DwmGetWindowAttribute = ctypes.windll.dwmapi.DwmGetWindowAttribute
DWMWA_EXTENDED_FRAME_BOUNDS = 9
DwmGetWindowAttribute(handle, DWMWA_EXTENDED_FRAME_BOUNDS,
                      rect, sizeof(rect))
rect = (rect.left, rect.top, rect.right, rect.bottom)


def key_callback():
    grabed_image = ImageGrab.grab()
    croped_image = grabed_image.crop(rect)
    name = datetime.datetime.now().strftime('%Y-%m-%d-%H%M%S')
    filename = name + '.jpg'
    croped_image.save(filename)
keyboard.add_hotkey('print screen', key_callback,
                    suppress=False)

print('escキーで終了')
keyboard.wait('esc')

printscreenキーでssが撮れるようになった。
ついでにファイル名にタイムスタンプを付与。
色々怪しいところもあるが、こまけぇことはいいんだよ! ということで完成。
マウスを使って撮りたいウィンドウを取得する処理がちょっとぎこちない気がするがこまけぇことは(略。

QooQ