モーダルを閉じる工作HardwareHub ロゴ画像

工作HardwareHubは、ロボット工作や電子工作に関する情報やモノが行き交うコミュニティサイトです。さらに詳しく

利用規約プライバシーポリシー に同意したうえでログインしてください。

目次目次を開く/閉じる

X11 アプリケーションを Python Xlib から操作

モーダルを閉じる

ステッカーを選択してください

お支払い手続きへ
モーダルを閉じる

お支払い内容をご確認ください

購入商品
」ステッカーの表示権
メッセージ
料金
(税込)
決済方法
GooglePayマーク
決済プラットフォーム
確認事項

利用規約をご確認のうえお支払いください

※カード情報はGoogleアカウント内に保存されます。本サイトやStripeには保存されません

※記事の執筆者は購入者のユーザー名を知ることができます

※購入後のキャンセルはできません

作成日作成日
2019/03/21
最終更新最終更新
2023/11/17
記事区分記事区分
一般公開

目次

    プログラミング教育者。ScratchやPythonを教えています。

    Qt 等で開発された X11 アプリケーションを利用するためには X サーバのクライアントが必要になります。X サーバは仮想的に Xvfb コマンドで作成することができます。X サーバのクライアントには fluxbox などの X11 ウィンドウマネージャ、ssh -Xx11vnc、その他 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()
    
    Likeボタン(off)0
    詳細設定を開く/閉じる
    アカウント プロフィール画像

    プログラミング教育者。ScratchやPythonを教えています。

    記事の執筆者にステッカーを贈る

    有益な情報に対するお礼として、またはコメント欄における質問への返答に対するお礼として、 記事の読者は、執筆者に有料のステッカーを贈ることができます。

    >>さらに詳しくステッカーを贈る
    ステッカーを贈る コンセプト画像

    Feedbacks

    Feedbacks コンセプト画像

      ログインするとコメントを投稿できます。

      ログインする

      関連記事