7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonを使って仮想ジョイスティックのGUIを作る

Last updated at Posted at 2024-12-01

はじめに

この記事はPython アドベントカレンダー2日目の記事です。

PythonのTkinterでマウスで操作することができる仮想ジョイスティックを作りました。
ゲームパッド本体が手元にない場合でもこれを使用することでロボットの操作などをすることができます。

環境

以下の環境で動作を確認しています。

項目 バージョン
Windows 11
WSL 2
Ubuntu 22.04
Python 3.10.12

実装

まずソースコード全体を以下に貼ります。

import tkinter
import dataclasses


@dataclasses.dataclass
class WindowSize:
    width: int
    height: int


@dataclasses.dataclass
class Position:
    x: float
    y: float


_WINDOS_SIZE = WindowSize(200, 300)
_CANVAS_SIZE = WindowSize(200, 200)
_CIRCLE_SIZE = 20


class MousePosition(tkinter.Frame):

    def __init__(self, master):
        super().__init__(master, height=_WINDOS_SIZE.height, width=_WINDOS_SIZE.width)

        canvas = tkinter.Canvas(
            master,
            background="white",
            height=_CANVAS_SIZE.height,
            width=_CANVAS_SIZE.width,
        )
        self._canvas = canvas
        self._virtual_stick_circle = canvas.create_oval(
            _CANVAS_SIZE.width / 2 - _CIRCLE_SIZE,
            _CANVAS_SIZE.height / 2 - _CIRCLE_SIZE,
            _CANVAS_SIZE.width / 2 + _CIRCLE_SIZE,
            _CANVAS_SIZE.height / 2 + _CIRCLE_SIZE,
            tag="oval",
        )
        canvas.create_line(
            _CANVAS_SIZE.width / 2, 0, _CANVAS_SIZE.width / 2, _CANVAS_SIZE.height
        )
        canvas.create_line(
            0, _CANVAS_SIZE.height / 2, _CANVAS_SIZE.width, _CANVAS_SIZE.height / 2
        )
        canvas.grid(row=0, column=0)
        canvas.bind("<Motion>", self._mouse_callback)
        canvas.bind("<Button-1>", self._click_callback)

        self._position_label = tkinter.Label(master)
        self._position_label.grid(row=1, column=0)

        self._last_position = Position(0.0, 0.0)
        self._is_clicked = False

    def _mouse_callback(self, event):
        position_from_center = Position(
            event.x - 1 / 2 * _CANVAS_SIZE.width,
            1 / 2 * _CANVAS_SIZE.height - event.y,
        )

        self._position_label["text"] = (
            str(position_from_center.x) + ", " + str(position_from_center.y)
        )

        if self._is_clicked:
            self._canvas.move(
                self._virtual_stick_circle,
                -1 * (self._last_position.x - position_from_center.x),
                self._last_position.y - position_from_center.y,
            )
            self._last_position.x = position_from_center.x
            self._last_position.y = position_from_center.y

    def _click_callback(self, event):
        if self._is_clicked:
            self._is_clicked = False
        else:
            self._is_clicked = True


if __name__ == "__main__":
    root = tkinter.Tk()
    MousePosition(root)
    root.mainloop()

GUIの作成

図形描画用のCanvasと数値表示用のLabelを用意します。

Canvasの作成
        canvas = tkinter.Canvas(
            master,
            background="white",
            height=_CANVAS_SIZE.height,
            width=_CANVAS_SIZE.width,
        )
Labelの作成
        self._position_label = tkinter.Label(master)

TkinterのCanvas上にcreate_ovalを使用して円を描画します。
create_ovalの返り値を、canvas.moveの引数に入れることでその丸を移動させることができます。

Canvas上に円を描画
        self._virtual_stick_circle = canvas.create_oval(
            _CANVAS_SIZE.width / 2 - _CIRCLE_SIZE,
            _CANVAS_SIZE.height / 2 - _CIRCLE_SIZE,
            _CANVAS_SIZE.width / 2 + _CIRCLE_SIZE,
            _CANVAS_SIZE.height / 2 + _CIRCLE_SIZE,
            tag="oval",
        )
円を動かす
            self._canvas.move(
                self._virtual_stick_circle,
                -1 * (self._last_position.x - position_from_center.x),
                self._last_position.y - position_from_center.y,
            )

動きをつける

bindでCanvas上の上をマウスカーソルが動いた時のイベントを取得してcallback関数を呼び出します。

マウスカーソル移動のバインド
        canvas.bind("<Motion>", self._mouse_callback)
マウスカーソル移動のコールバック関数
    def _mouse_callback(self, event):
        position_from_center = Position(
            event.x - 1 / 2 * _CANVAS_SIZE.width,
            1 / 2 * _CANVAS_SIZE.height - event.y,
        )

        self._position_label["text"] = (
            str(position_from_center.x) + ", " + str(position_from_center.y)
        )

        if self._is_clicked:
            self._canvas.move(
                self._virtual_stick_circle,
                -1 * (self._last_position.x - position_from_center.x),
                self._last_position.y - position_from_center.y,
            )
            self._last_position.x = position_from_center.x
            self._last_position.y = position_from_center.y

また、このままだとON/OFFのような機能がないので、マウスクリックのイベントを取得してON/OFFのフラグを設定するようにします。

クリックのバインド
        canvas.bind("<Button-1>", self._click_callback)
クリックのコールバック関数
    def _click_callback(self, event):
        if self._is_clicked:
            self._is_clicked = False
        else:
            self._is_clicked = True

データクラスを使う

widthとheight、xとyを扱う必要があり、ややこしくなると思ったためdataclassを使用しています。
C言語などの構造体と同様に使用することができただのタプルやリストに比べて意味のあるデータとすることができるので可読性や拡張性が広がります。

@dataclasses.dataclass
class WindowSize:
    width: int
    height: int


@dataclasses.dataclass
class Position:
    x: float
    y: float

おわりに

今回は簡単なGUIを作成し、その中の図形をマウスカーソルの動きに沿って動かしてみました。
これで取得できたX,Y座標を使用してロボットなどの操作に使っていきたいです。

7
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?