Search code examples
pythonpyqtpyqt4

How to create a joystick/controller widget in PyQt?


I want to create a joystick widget similar to this

enter image description here

My current implementation uses QToolButton() for the side arrows but I'm not sure how to create the circle in the middle. When the user clicks on the middle point and drags it towards an arrow, it should register the movement. I'm thinking about using paintEvent() and drawEclipse() or maybe even QDial() but I'm not sure how to do this.

enter image description here

from PyQt4 import QtCore, QtGui
import sys

class JoystickWidget(QtGui.QWidget):
    def __init__(self, parent=None):
        super(JoystickWidget, self).__init__(parent)

        self.field_joystick_up_button = QtGui.QToolButton()
        self.field_joystick_up_button.setArrowType(QtCore.Qt.UpArrow)
        self.field_joystick_up_button.clicked.connect(self.joystick_up)
        self.field_joystick_up_button.setFixedWidth(75)
        self.field_joystick_down_button = QtGui.QToolButton()
        self.field_joystick_down_button.setArrowType(QtCore.Qt.DownArrow)
        self.field_joystick_down_button.clicked.connect(self.joystick_down)
        self.field_joystick_down_button.setFixedWidth(75)
        self.field_joystick_right_button = QtGui.QToolButton()
        self.field_joystick_right_button.setArrowType(QtCore.Qt.RightArrow)
        self.field_joystick_right_button.clicked.connect(self.joystick_right)
        self.field_joystick_right_button.setFixedWidth(75)
        self.field_joystick_left_button = QtGui.QToolButton()
        self.field_joystick_left_button.setArrowType(QtCore.Qt.LeftArrow)
        self.field_joystick_left_button.clicked.connect(self.joystick_left)
        self.field_joystick_left_button.setFixedWidth(75)

        self.joystick_layout = QtGui.QVBoxLayout()
        self.joystick_layout.addWidget(self.field_joystick_up_button,alignment=QtCore.Qt.AlignCenter)
        self.joystick_layout_row = QtGui.QHBoxLayout()
        self.joystick_layout_row.addWidget(self.field_joystick_left_button)
        self.joystick_layout_row.addWidget(self.field_joystick_right_button)
        self.joystick_layout.addLayout(self.joystick_layout_row)
        self.joystick_layout.addWidget(self.field_joystick_down_button,alignment=QtCore.Qt.AlignCenter)

    def get_joystick_layout(self):
        return self.joystick_layout
    def joystick_up(self):
        print("Up")
    def joystick_down(self):
        print("Down")
    def joystick_right(self):
        print("Right")
    def joystick_left(self):
        print("Left")

if __name__ == '__main__':
    # Create main application window
    app = QtGui.QApplication([])
    app.setStyle(QtGui.QStyleFactory.create("Cleanlooks"))
    mw = QtGui.QMainWindow()
    mw.setWindowTitle('Joystick example')

    # Create and set widget layout
    # Main widget container
    cw = QtGui.QWidget()
    ml = QtGui.QGridLayout()
    cw.setLayout(ml)
    mw.setCentralWidget(cw)

    # Create joystick 
    joystick = JoystickWidget()

    ml.addLayout(joystick.get_joystick_layout(),0,0)

    mw.show()

    ## Start Qt event loop unless running in interactive mode or using pyside.
    if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
        QtGui.QApplication.instance().exec_()


Solution

  • enter image description here

    Sometimes, it's easier to start from scratch.

    If you use QWidget::mousePressEvent(), QWidget::mouseReleaseEvent(), QWidget::mouseMoveEvent() and QWidget::paintEvent(), you will be able to handle your joystick.

    Use QWidget::paintEvent() to draw your joystick at the center of your widget.

    QWidget::mousePressEvent() will be called whenever the use press a button his mouse. You can use it the start the moving of your joystick.

    QWidget::mouseReleaseEvent() is called when the user release the mouse button. Use it to reset the joystick.

    QWidget::mouseMoveEvent() is called the the mouse is moving. Use it to compute the offset of the joystick and the direction (up, left, down or right). If you want some kind of analog joystick, you can also use the distance between the center and the joystick to get a number between 0 (no moving) and 1 (at the maximum).

    For example:

    from PyQt4.QtGui import *
    from PyQt4.QtCore import *
    import sys
    from enum import Enum
    
    class Direction(Enum):
        Left = 0
        Right = 1
        Up = 2
        Down = 3
    
    class Joystick(QWidget):
        def __init__(self, parent=None):
            super(Joystick, self).__init__(parent)
            self.setMinimumSize(100, 100)
            self.movingOffset = QPointF(0, 0)
            self.grabCenter = False
            self.__maxDistance = 50
    
        def paintEvent(self, event):
            painter = QPainter(self)
            bounds = QRectF(-self.__maxDistance, -self.__maxDistance, self.__maxDistance * 2, self.__maxDistance * 2).translated(self._center())
            painter.drawEllipse(bounds)
            painter.setBrush(Qt.black)
            painter.drawEllipse(self._centerEllipse())
    
        def _centerEllipse(self):
            if self.grabCenter:
                return QRectF(-20, -20, 40, 40).translated(self.movingOffset)
            return QRectF(-20, -20, 40, 40).translated(self._center())
    
        def _center(self):
            return QPointF(self.width()/2, self.height()/2)
    
    
        def _boundJoystick(self, point):
            limitLine = QLineF(self._center(), point)
            if (limitLine.length() > self.__maxDistance):
                limitLine.setLength(self.__maxDistance)
            return limitLine.p2()
    
        def joystickDirection(self):
            if not self.grabCenter:
                return 0
            normVector = QLineF(self._center(), self.movingOffset)
            currentDistance = normVector.length()
            angle = normVector.angle()
    
            distance = min(currentDistance / self.__maxDistance, 1.0)
            if 45 <= angle < 135:
                return (Direction.Up, distance)
            elif 135 <= angle < 225:
                return (Direction.Left, distance)
            elif 225 <= angle < 315:
                return (Direction.Down, distance)
            return (Direction.Right, distance)
    
    
        def mousePressEvent(self, ev):
            self.grabCenter = self._centerEllipse().contains(ev.pos())
            return super().mousePressEvent(ev)
    
        def mouseReleaseEvent(self, event):
            self.grabCenter = False
            self.movingOffset = QPointF(0, 0)
            self.update()
    
        def mouseMoveEvent(self, event):
            if self.grabCenter:
                print("Moving")
                self.movingOffset = self._boundJoystick(event.pos())
                self.update()
            print(self.joystickDirection())
    
    if __name__ == '__main__':
        # Create main application window
        app = QApplication([])
        app.setStyle(QStyleFactory.create("Cleanlooks"))
        mw = QMainWindow()
        mw.setWindowTitle('Joystick example')
    
        # Create and set widget layout
        # Main widget container
        cw = QWidget()
        ml = QGridLayout()
        cw.setLayout(ml)
        mw.setCentralWidget(cw)
    
        # Create joystick 
        joystick = Joystick()
    
        # ml.addLayout(joystick.get_joystick_layout(),0,0)
        ml.addWidget(joystick,0,0)
    
        mw.show()
    
        ## Start Qt event loop unless running in interactive mode or using pyside.
        if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
            QApplication.instance().exec_()
    

    You can use the same logic to create the buttons: define four areas for your buttons and check when the mouse is pressed in these areas.