Qt 等で開発された X11 アプリケーションを利用するためには X サーバのクライアントが必要になります。X サーバは仮想的に Xvfb コマンドで作成することができます。X サーバのクライアントには fluxbox
などの X11 ウィンドウマネージャ、ssh -X
や x11vnc
、その他 Qt 等で開発された X11 アプリケーションがあります。
ここでは X サーバのクライアントを Xlib で作成して X11 アプリケーションを自動操作してみます。Python の Xlib 実装は python-xlib です。
インストール
pip 等でインストールできます。
sudo pip install python-xlib
スクリーンショットの取得
仮想ディスプレイ DISPLAY=:99
を作成します。
Xvfb :99 -screen 0 1024x768x24 -listen tcp -ac
GUI アプリケーションを接続します。
sudo apt install mesa-utils
DISPLAY=localhost:99 glxgears
以下のようにしてスクリーンショットを取得できます。
DISPLAY=localhost:99 ipython
RAW 画像は pillow(PIL) の Image.frombytes を利用して NumPy の ndarray に変換すると簡単です。
from Xlib.display import Display
from Xlib.X import ZPixmap
from PIL import Image
from numpy import asarray
# DISPLAY=:99 にはウィンドウマネージャが存在しないため root をキャプチャ対象とします。
display = Display()
screen = display.screen()
window = screen.root
# RAW 画像を取得して変換します。
rawimage = window.get_image(0, 0, 1024, 768, ZPixmap, 0xFFFFFFFF).data
image = Image.frombytes('RGB', (1024, 768), rawimage, 'raw', 'RGBX')
# ファイルに保存する場合は以下のようにします。
with open('./sample.png', 'w') as f:
Image.fromarray(asarray(image)).save(f, 'PNG')
クリック、キーボード操作
仮想ディスプレイ DISPLAY=:99
を作成します。
Xvfb :99 -screen 0 1024x768x24 -listen tcp -ac
GUI アプリケーションを接続します。
sudo apt install x11-apps
DISPLAY=localhost:99 xeyes
仮想ディスプレイに VNC 接続して動作確認できるようにします。
x11vnc -display :99 -listen 0.0.0.0 -forever -xkb -shared -nopw
DISPLAY=:0 gvncviewer localhost::5900
以下のようにして左上の (0, 0)
をマウスで左クリックできます。
DISPLAY=localhost:99 ipython
fake_input を利用します。番号の 1 は左クリックです。後に利用する xev
でもマウスボタンの番号を確認できます。
from Xlib.display import Display
from Xlib.X import MotionNotify
from Xlib.X import ButtonPress
from Xlib.X import ButtonRelease
from Xlib.ext.xtest import fake_input
display = Display()
fake_input(display, MotionNotify, x=0, y=0)
fake_input(display, ButtonPress, 1)
display.sync()
同様にキーボードの操作を表現するためには以下のようにします。
DISPLAY=localhost:99 xev
DISPLAY=localhost:99 ipython
from Xlib.display import Display
from Xlib.X import KeyPress
from Xlib.X import KeyRelease
from Xlib.ext.xtest import fake_input
display = Display()
fake_input(display, KeyPress, 38) # 'a'
display.sync()
一連の GUI 操作の保存および再生
仮想ディスプレイ DISPLAY=:99
を作成します。
Xvfb :99 -screen 0 1024x768x24 -listen tcp -ac
GUI アプリケーションを接続します。
sudo apt install x11-apps
DISPLAY=localhost:99 xcalc
仮想ディスプレイに VNC 接続できるようにします。
x11vnc -display :99 -listen 0.0.0.0 -forever -xkb -shared -nopw
DISPLAY=:0 gvncviewer localhost::5900
VNC 経由で操作した一連の操作を X サーバ経由で取得してファイルに保存するスクリプトを起動します。localhost の DISPLAY=:0
ではなく Xvfb の DISPLAY=:99
に接続すると screen の root をそのまま扱えて簡単です。
DISPLAY=localhost:99 python record.py
record.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
from time import time
from pickle import dump
from Xlib.display import Display
from Xlib.ext.record import AllClients
from Xlib.ext.record import FromServer
from Xlib.protocol.rq import EventField
from Xlib.X import KeyPress
from Xlib.X import KeyRelease
from Xlib.X import MotionNotify
from Xlib.X import ButtonPress
from Xlib.X import ButtonRelease
def Main():
display = Display()
context = {}
x11ctx = display.record_create_context(
0,
[AllClients],
[{
'core_requests': (0, 0),
'core_replies': (0, 0),
'ext_requests': (0, 0, 0, 0),
'ext_replies': (0, 0, 0, 0),
'delivered_events': (0, 0),
'device_events': (KeyPress, MotionNotify),
'errors': (0, 0),
'client_started': False,
'client_died': False
}]
)
context['display'] = display
context['startTime'] = time()
context['records'] = []
try:
# X サーバからイベントを取得して時間情報と共に変数に保存します。
print('Recording started, stop with ctrl-c')
display.record_enable_context(x11ctx, lambda r: RecordCallback(context, r))
except KeyboardInterrupt:
print('Recording finished.')
finally:
display.record_disable_context(x11ctx)
display.flush()
display.record_free_context(x11ctx)
# 取得したイベントをファイルに保存します。
with open('./record.data', 'w') as f:
dump(context['records'], f)
def RecordCallback(context, reply):
# 必要な情報だけを処理します。
if reply.category != FromServer:
return
elif reply.client_swapped:
return
elif not len(reply.data) or reply.data[0] < 2:
return
# イベントから必要な情報を取り出します。
display = context['display']
records = context['records']
startTime = context['startTime']
data = reply.data
while len(data):
event, data = EventField(None).parse_binary_value(data, display.display, None, None)
if event.type in [KeyPress, KeyRelease]:
record = {
'event': 'KeyPress' if event.type == KeyPress else 'KeyRelease',
'keycode': event.detail,
'timestamp': time() - startTime
}
print(record)
records.append(record)
elif event.type in [ButtonPress, ButtonRelease, MotionNotify]:
record = {
'event': 'ButtonPress' if event.type == ButtonPress else ('ButtonRelease' if event.type == ButtonRelease else 'MotionNotify'),
'x': event.root_x,
'y': event.root_y,
'timestamp': time() - startTime
}
print(record)
records.append(record)
if __name__ == '__main__':
Main()
保存されたファイルをもとに一連の操作を X サーバに送信して再生するスクリプトを起動します。VNC 経由で再生されている様子を確認できます。保存時と再生時にスクリーンショットを取得して画像の差分を利用すれば簡単な自動テストが実現できます。
DISPLAY=localhost:99 python replay.py
replay.py
#!/usr/bin/python
# -*- coding: utf-8 -*-
from pickle import load
from time import time
from time import sleep
from Xlib.display import Display
from Xlib.ext.xtest import fake_input
from Xlib.X import KeyPress
from Xlib.X import KeyRelease
from Xlib.X import MotionNotify
from Xlib.X import ButtonPress
from Xlib.X import ButtonRelease
def Main():
display = Display()
records = []
with open('./record.data') as f:
records = load(f)
startTime = time()
for record in records:
print(record)
if record['timestamp'] > time() - startTime:
sleep(record['timestamp'] - (time() - startTime))
if record['event'] == 'KeyPress':
fake_input(display, KeyPress, record['keycode'])
elif record['event'] == 'KeyRelease':
fake_input(display, KeyRelease, record['keycode'])
elif record['event'] == 'ButtonPress':
fake_input(display, MotionNotify, x=record['x'], y=record['y'])
fake_input(display, ButtonPress, 1)
elif record['event'] == 'ButtonRelease':
fake_input(display, MotionNotify, x=record['x'], y=record['y'])
fake_input(display, ButtonRelease, 1)
elif record['event'] == 'MotionNotify':
fake_input(display, MotionNotify, x=record['x'], y=record['y'])
display.sync()
if __name__ == '__main__':
Main()
関連記事
- Python コードスニペット (条件分岐)if-elif-else sample.py #!/usr/bin/python # -*- coding: utf-8 -*- # コメント内であっても、ASCII外の文字が含まれる場合はエンコーディング情報が必須 x = 1 # 一行スタイル if x==0: print 'a' # 参考: and,or,notが使用可能 (&&,||はエラー) elif x==1: p...
- Python コードスニペット (リスト、タプル、ディクショナリ)リスト range 「0から10まで」といった範囲をリスト形式で生成します。 sample.py print range(10) # for(int i=0; i<10; ++i) ← C言語などのfor文と比較 print range(5,10) # for(int i=5; i<10; ++i) print range(5,10,2) # for(int i=5; i<10;...
- ZeroMQ (zmq) の Python サンプルコードZeroMQ を Python から利用する場合のサンプルコードを記載します。 Fixing the World To fix the world, we needed to do two things. One, to solve the general problem of "how to connect any code to any code, anywhere". Two, to wra...
- Matplotlib/SciPy/pandas/NumPy サンプルコードPython で数学的なことを試すときに利用される Matplotlib/SciPy/pandas/NumPy についてサンプルコードを記載します。 Matplotlib SciPy pandas [NumPy](https://www.numpy
- pytest の基本的な使い方pytest の基本的な使い方を記載します。 適宜参照するための公式ドキュメントページ Full pytest documentation API Reference インストール 適当なパッケージ