Search code examples
pythonpyqtpyqt5qpainterpan

QPaint with mouse Event panning


I have created a paint widget, that I want to implement a pan event with hand icon tool, which usually sees in many softwares. it is when user press on mouse button and hold it then move over within Qpainter canvas, the drawing follows the mouse movement. I can't find out how to do it in PyQt5.

VISUAL EXAMPLE

The current screen:

enter image description here

The desired screen event:

enter image description here

The Code:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets

class Foo(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Foo, self).__init__(parent)
        self.setGeometry(QtCore.QRect(200, 100, 1200, 600))        
        self.paint = Paint()
        self.sizeHint()
        self.lay = QtWidgets.QVBoxLayout()
        self.lay.addWidget(self.paint)
        self.setLayout(self.lay)

class Paint(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Paint, self).__init__(parent)
        self.setBackgroundRole(QtGui.QPalette.Base)     
        self.setAutoFillBackground(True)

        self._width = 350
        self._height = 250

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        painter.setBrush(QtGui.QBrush( QtCore.Qt.cyan))
        painter.setPen(QtCore.Qt.darkCyan)

        r = QtCore.QRect(QtCore.QPoint(), QtCore.QSize(self._width, self._height))
        r.moveCenter(self.rect().center())
        painter.drawRect(r)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = Foo()
    w.show()
    sys.exit(app.exec_())

I appreciate any help and thanks in advance.

Update of code:

Visualization:

Well,that's what happens when I implement the updated code in my central code. it is when user clicks and picks section from circular then plots within the widget, right after click and select another section from rectangular. The widget should update and erase previous drawing. But it does't do that. In my previous code the section drawing appears directly in the center of widget, now it sticks to upperleft edge of canvas.

enter image description here

A part of code:

from PyQt5 import QtCore, QtGui, QtWidgets

class Foo(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Foo, self).__init__(parent)
        self.setGeometry(QtCore.QRect(200, 100, 800, 800))

        self.button = Button()
        self.paint = Createpaintwidget()
        self.button.valuesChanged.connect(self.paint.set_size_squares)
        self.button.valueChanged.connect(self.paint.set_size_round)

        self.lay = QtWidgets.QVBoxLayout(self)
        self.lay.addWidget(self.paint)
        self.lay.addWidget(self.button)

class Createpaintwidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.sizeHint()        
        self.setBackgroundRole(QtGui.QPalette.Base)     
        self.setAutoFillBackground(True)

        self._size = QtCore.QSizeF()
        self._path = QtGui.QPainterPath()
        self._rect = QtCore.QRectF()
        self._type = QtGui.QRegion.Rectangle
        self._factor = 1.0

        self._pos = QtCore.QPointF()
        self._initial_flag = False
        fnt = self.font() 
        fnt.setPointSize(20) 
        self.setFont(fnt) 

    def showEvent(self, event):
        if not self._initial_flag:
            self._pos = self.rect().center()
            self._initial_flag = True

    @QtCore.pyqtSlot(int,int)
    def set_size_squares(self, w, h):
        self._size = QtCore.QSizeF(w, h)
        self._type = QtGui.QRegion.Rectangle
        self.updatePath()

    @QtCore.pyqtSlot(int)
    def set_size_round(self, v):
        self._size = QtCore.QSizeF(v, v)
        self._type = QtGui.QRegion.Ellipse
        self.updatePath()

    def paintEvent(self, event):
        pen = QtGui.QPen()
        brush = QtGui.QBrush(QtCore.Qt.black)
        painter = QtGui.QPainter(self)
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        painter.setPen(pen)
        painter.setBrush(brush)

        painter.translate(self.rect().center())
        painter.scale(self._factor, self._factor)
        painter.translate(-self.rect().center())

        painter.translate(self._pos)
        painter.drawPath(self._path)
        if self._type == QtGui.QRegion.Rectangle:
            painter.fillRect(self._rect, QtGui.QBrush(QtCore.Qt.gray, QtCore.Qt.Dense7Pattern))
            painter.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
            painter.drawRect(self._rect)
        elif self._type == QtGui.QRegion.Ellipse:
            painter.setBrush(QtGui.QBrush(QtCore.Qt.gray, QtCore.Qt.Dense7Pattern))
            painter.drawEllipse(self._rect)

    def mousePressEvent(self, event):
        QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
        self._initial_pos = event.pos()
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        delta = event.pos() - self._initial_pos
        self._path.translate(delta)
        self._rect.translate(delta)
        self.update()
        self._initial_pos = event.pos()
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        QtWidgets.QApplication.restoreOverrideCursor()
        super().mouseReleaseEvent(event)

    def updatePath(self):
        fm = QtGui.QFontMetrics(self.font())
        r = QtCore.QRectF(QtCore.QPointF(), self._size)
        r.moveCenter(QtCore.QPointF())
        self._rect = QtCore.QRectF(r)
        self._path.moveTo(QtCore.QPointF())
        p = QtCore.QPointF(self._size.width()/2 + 75 ,0)
        self._path.lineTo(p)
        self._path.lineTo(p + QtCore.QPoint(-16, -16))
        self._path.lineTo(p)
        self._path.moveTo(p)
        self._path.lineTo(p + QtCore.QPoint(-16, 16))
        self._path.lineTo(p)
        self._path.addText(p + QtCore.QPoint(16, 0) , self.font(), "x")

        self._path.moveTo(QtCore.QPointF())
        p = QtCore.QPointF(0, -self._size.height()/2 - 75)
        self._path.lineTo(p)
        self._path.lineTo(p + QtCore.QPoint(16, 16))
        self._path.lineTo(p)
        self._path.moveTo(p)
        self._path.lineTo(p + QtCore.QPoint(-16, 16))
        self._path.addText(p + QtCore.QPoint(0, -16) , self.font(), "y")

        if self._type == QtGui.QRegion.Rectangle:
            pl = r.bottomLeft() + QtCore.QPointF(0, 75)
            pr = r.bottomRight() + QtCore.QPointF(0, 75)
            self._path.moveTo(pl)
            self._path.lineTo(pr)
            for p in (pl, pr):
                self._path.moveTo(p+ QtCore.QPoint(0, -40))
                self._path.lineTo(p+ QtCore.QPoint(0, 20))
                self._path.moveTo(p+ QtCore.QPoint(10, -10))
                self._path.lineTo(p+ QtCore.QPoint(-10, 10))
            word = "{}".format(self._size.width())
            p = QtCore.QPointF(r.center().x() - 0.5*fm.width(word) , r.bottom() +  100)
            self._path.addText(p , self.font(), word)

            pt = r.topLeft() + QtCore.QPointF(-75, 0)
            pb = r.bottomLeft() + QtCore.QPointF(-75, 0)
            self._path.moveTo(pt)
            self._path.lineTo(pb)
            for p in (pt, pb):
                self._path.moveTo(p+ QtCore.QPoint(40, 0))
                self._path.lineTo(p+ QtCore.QPoint(-20, 0))
                self._path.moveTo(p+ QtCore.QPoint(10, -10))
                self._path.lineTo(p+ QtCore.QPoint(-10, 10))
            word = "{}".format(self._size.height())
            p = QtCore.QPointF(r.left() -80 - fm.width(word) , r.center().y() + 0.5*fm.height())
            self._path.addText(p , self.font(), word)
        if self._type == QtGui.QRegion.Ellipse:
            pl = r.bottomLeft() + QtCore.QPointF(0, 75)
            pr = r.bottomRight() + QtCore.QPointF(0, 75)
            self._path.moveTo(pl)
            self._path.lineTo(pr)
            for p in (pl, pr):
                self._path.moveTo(p+ QtCore.QPoint(0, -self._size.height()/2 - 20))
                self._path.lineTo(p+ QtCore.QPoint(0, 10))
                self._path.moveTo(p+ QtCore.QPoint(10, -10))
                self._path.lineTo(p+ QtCore.QPoint(-10, 10))
            word = "{}".format(self._size.width())
            p = QtCore.QPointF(r.center().x() - 0.5*fm.width(word) , r.bottom() +  100)
            self._path.addText(p , self.font(), word)
        self.update()

    def wheelEvent(self, event):
        self._factor *= 1.01**(event.angleDelta().y()/15.0)
        self.update()
        super().wheelEvent(event)

class Button(QtWidgets.QWidget):
    valueChanged = QtCore.pyqtSignal(int)
    valuesChanged = QtCore.pyqtSignal(int,int)
    def __init__(self, parent=None):
        super(Button, self).__init__(parent)
        roundbutton = QtWidgets.QPushButton('Round')
        squarebutton = QtWidgets.QPushButton('Square')
        Alay = QtWidgets.QVBoxLayout(self)
        Alay.addWidget(roundbutton)
        Alay.addWidget(squarebutton)
        self.value = QtWidgets.QLabel()
        roundbutton.clicked.connect(self.getbuttonfunc)
        squarebutton.clicked.connect(self.sqaurebuttonfunc)

    @QtCore.pyqtSlot()
    def getbuttonfunc(self):
        number, ok = QtWidgets.QInputDialog.getInt(self, self.tr("Set Number"),
                                         self.tr("Input:"), 1, 1)
        if ok:
            self.valueChanged.emit(number)

    @QtCore.pyqtSlot()
    def sqaurebuttonfunc(self):
        number, ok = QtWidgets.QInputDialog.getInt(self, self.tr("Set Number"),
                                         self.tr("Input:"), 1, 1)
        if ok:
            self.valuesChanged.emit(number, number)


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = Foo()
    w.show()
    sys.exit(app.exec_())

Solution

  • To make the pan you must follow the following steps:

    • Get the initial position in mousePressEvent

    • In mouseMoveEvent move the rectangle with the difference between the current position and the initial position, and update the initial position with the current position.

    The implementation is as follows:

    class Paint(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Paint, self).__init__(parent)
            self.setBackgroundRole(QtGui.QPalette.Base)     
            self.setAutoFillBackground(True)
    
            self._width = 350
            self._height = 250
            self._rect = QtCore.QRect(QtCore.QPoint(), QtCore.QSize(self._width, self._height))
            self._initial_flag = False
            self._initial_pos = QtCore.QPoint()
    
        def showEvent(self, event):
            if not self._initial_flag:
                # set initial pos
                self._rect.moveCenter(self.rect().center())
                self._initial_flag = True
            super(Paint, self).showEvent(event)
    
        def paintEvent(self, event):
            painter = QtGui.QPainter(self)
            painter.setRenderHint(QtGui.QPainter.Antialiasing)
            painter.setBrush(QtGui.QBrush( QtCore.Qt.cyan))
            painter.setPen(QtCore.Qt.darkCyan)
            painter.drawRect(self._rect)
    
        def mousePressEvent(self, event):
            if self._rect.contains(event.pos()):
                QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
                self._initial_pos = event.pos()
            super(Paint, self).mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
            if self._rect.contains(event.pos()):
                delta = event.pos() - self._initial_pos
                self._rect.translate(delta)
                self.update()
                self._initial_pos = event.pos()
            super(Paint, self).mouseMoveEvent(event)
    
        def mouseReleaseEvent(self, event):
            QtWidgets.QApplication.restoreOverrideCursor()
            super(Paint, self).mouseReleaseEvent(event)
    

    But instead of doing it Qt already offers classes that allow you to do the tasks that you point out in a simple way, in this case it is to use QGraphicsView with the QGraphicsItem:

    class GraphicsRectItem(QtWidgets.QGraphicsRectItem):
        def mousePressEvent(self, event):
            QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
            super(GraphicsRectItem, self).mousePressEvent(event)
    
        def mouseReleaseEvent(self, event):
            QtWidgets.QApplication.restoreOverrideCursor()
            super(GraphicsRectItem, self).mouseReleaseEvent(event)
    
    class Paint(QtWidgets.QGraphicsView):
        def __init__(self, parent=None):
            super(Paint, self).__init__(parent)
            self.setBackgroundRole(QtGui.QPalette.Base)     
            self.setAutoFillBackground(True)
    
            scene = QtWidgets.QGraphicsScene(self)
            self.setScene(scene)
            rect_item = GraphicsRectItem(0, 0, 350, 250)
            rect_item.setBrush(QtGui.QBrush( QtCore.Qt.cyan))
            rect_item.setPen(QtCore.Qt.darkCyan)
            rect_item.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
            self.scene().addItem(rect_item)
    

    UPDATE:

    class Paint(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(Paint, self).__init__(parent)
            self.setBackgroundRole(QtGui.QPalette.Base)     
            self.setAutoFillBackground(True)
    
            self._width = 350
            self._height = 250
            self._initial_flag = False
            self._initial_pos = QtCore.QPoint()
            self._pos = QtCore.QPoint()
    
        def showEvent(self, event):
            if not self._initial_flag:
                # set initial pos
                self._pos = self.rect().center()
                self._initial_flag = True
            super(Paint, self).showEvent(event)
    
        def paintEvent(self, event):
            painter = QtGui.QPainter(self)
            painter.setRenderHint(QtGui.QPainter.Antialiasing)
            painter.setBrush(QtGui.QBrush( QtCore.Qt.cyan))
            painter.setPen(QtCore.Qt.darkCyan)
            r = QtCore.QRect(QtCore.QPoint(), QtCore.QSize(self._width, self._height))
            r.moveCenter(self._pos)
            painter.drawRect(r)
    
        def mousePressEvent(self, event):
            r = QtCore.QRect(QtCore.QPoint(), QtCore.QSize(self._width, self._height))
            r.moveCenter(self._pos)
            if r.contains(event.pos()):
                QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
                self._initial_pos = event.pos()
            super(Paint, self).mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
            r = QtCore.QRect(QtCore.QPoint(), QtCore.QSize(self._width, self._height))
            r.moveCenter(self._pos)
            if r.contains(event.pos()):
                delta = event.pos() - self._initial_pos
                self._pos += delta 
                self._initial_pos = event.pos()
                self.update()
            super(Paint, self).mouseMoveEvent(event)
    
        def mouseReleaseEvent(self, event):
            QtWidgets.QApplication.restoreOverrideCursor()
            super(Paint, self).mouseReleaseEvent(event)
    

    Update:

    One way to implement the task is to have an element or set connected element, in this case it is a QPainterPath plus a QRectF, the first will draw the arrows, lines, etc. and the second the rectangle or central circle. After that, it is enough to move those elements according to the case.

    class Createpaintwidget(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            self.sizeHint()        
            self.setBackgroundRole(QtGui.QPalette.Base)     
            self.setAutoFillBackground(True)
    
            self._size = QtCore.QSizeF()
            self._path = QtGui.QPainterPath()
            self._rect = QtCore.QRectF()
            self._type = QtGui.QRegion.Rectangle
            self._factor = 1.0
    
            self._pos = QtCore.QPointF()
            self._initial_flag = False
            fnt = self.font() 
            fnt.setPointSize(20) 
            self.setFont(fnt) 
    
        def showEvent(self, event):
            if not self._initial_flag:
                self._pos = self.rect().center()
                self._initial_flag = True
    
        @QtCore.pyqtSlot(int, int)
        def set_size_squares(self, w, h):
            self._path = QtGui.QPainterPath()
            self._size = QtCore.QSizeF(w, h)
            self._type = QtGui.QRegion.Rectangle
            self.updatePath()
    
        @QtCore.pyqtSlot(int)
        def set_size_round(self, v):
            self._path = QtGui.QPainterPath()
            self._size = QtCore.QSizeF(v, v)
            self._type = QtGui.QRegion.Ellipse
            self.updatePath()
    
        def paintEvent(self, event):
            pen = QtGui.QPen()
            brush = QtGui.QBrush(QtCore.Qt.black)
            painter = QtGui.QPainter(self)
            painter.setRenderHint(QtGui.QPainter.Antialiasing)
            painter.setPen(pen)
            painter.setBrush(brush)
    
            painter.translate(self.rect().center())
            painter.scale(self._factor, self._factor)
            painter.translate(-self.rect().center())
    
            painter.translate(self._pos)
            painter.drawPath(self._path)
            if self._type == QtGui.QRegion.Rectangle:
                painter.fillRect(self._rect, QtGui.QBrush(QtCore.Qt.gray, QtCore.Qt.Dense7Pattern))
                painter.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
                painter.drawRect(self._rect)
            elif self._type == QtGui.QRegion.Ellipse:
                painter.setBrush(QtGui.QBrush(QtCore.Qt.gray, QtCore.Qt.Dense7Pattern))
                painter.drawEllipse(self._rect)
    
        def mousePressEvent(self, event):
            QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
            self._initial_pos = event.pos()
            super().mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
            delta = event.pos() - self._initial_pos
            self._path.translate(delta)
            self._rect.translate(delta)
            self.update()
            self._initial_pos = event.pos()
            super().mouseMoveEvent(event)
    
        def mouseReleaseEvent(self, event):
            QtWidgets.QApplication.restoreOverrideCursor()
            super().mouseReleaseEvent(event)
    
        def updatePath(self):
            fm = QtGui.QFontMetrics(self.font())
            r = QtCore.QRectF(QtCore.QPointF(), self._size)
            r.moveCenter(QtCore.QPointF())
            self._rect = QtCore.QRectF(r)
            self._path.moveTo(QtCore.QPointF())
            p = QtCore.QPointF(self._size.width()/2 + 75 ,0)
            self._path.lineTo(p)
            self._path.lineTo(p + QtCore.QPoint(-16, -16))
            self._path.lineTo(p)
            self._path.moveTo(p)
            self._path.lineTo(p + QtCore.QPoint(-16, 16))
            self._path.lineTo(p)
            self._path.addText(p + QtCore.QPoint(16, 0) , self.font(), "x")
    
            self._path.moveTo(QtCore.QPointF())
            p = QtCore.QPointF(0, -self._size.height()/2 - 75)
            self._path.lineTo(p)
            self._path.lineTo(p + QtCore.QPoint(16, 16))
            self._path.lineTo(p)
            self._path.moveTo(p)
            self._path.lineTo(p + QtCore.QPoint(-16, 16))
            self._path.addText(p + QtCore.QPoint(0, -16) , self.font(), "y")
    
            if self._type == QtGui.QRegion.Rectangle:
                pl = r.bottomLeft() + QtCore.QPointF(0, 75)
                pr = r.bottomRight() + QtCore.QPointF(0, 75)
                self._path.moveTo(pl)
                self._path.lineTo(pr)
                for p in (pl, pr):
                    self._path.moveTo(p+ QtCore.QPoint(0, -40))
                    self._path.lineTo(p+ QtCore.QPoint(0, 20))
                    self._path.moveTo(p+ QtCore.QPoint(10, -10))
                    self._path.lineTo(p+ QtCore.QPoint(-10, 10))
                word = "{}".format(self._size.width())
                p = QtCore.QPointF(r.center().x() - 0.5*fm.width(word) , r.bottom() +  100)
                self._path.addText(p , self.font(), word)
    
                pt = r.topLeft() + QtCore.QPointF(-75, 0)
                pb = r.bottomLeft() + QtCore.QPointF(-75, 0)
                self._path.moveTo(pt)
                self._path.lineTo(pb)
                for p in (pt, pb):
                    self._path.moveTo(p+ QtCore.QPoint(40, 0))
                    self._path.lineTo(p+ QtCore.QPoint(-20, 0))
                    self._path.moveTo(p+ QtCore.QPoint(10, -10))
                    self._path.lineTo(p+ QtCore.QPoint(-10, 10))
                word = "{}".format(self._size.height())
                p = QtCore.QPointF(r.left() -80 - fm.width(word) , r.center().y() + 0.5*fm.height())
                self._path.addText(p , self.font(), word)
            if self._type == QtGui.QRegion.Ellipse:
                pl = r.bottomLeft() + QtCore.QPointF(0, 75)
                pr = r.bottomRight() + QtCore.QPointF(0, 75)
                self._path.moveTo(pl)
                self._path.lineTo(pr)
                for p in (pl, pr):
                    self._path.moveTo(p+ QtCore.QPoint(0, -self._size.height()/2 - 20))
                    self._path.lineTo(p+ QtCore.QPoint(0, 10))
                    self._path.moveTo(p+ QtCore.QPoint(10, -10))
                    self._path.lineTo(p+ QtCore.QPoint(-10, 10))
                word = "{}".format(self._size.width())
                p = QtCore.QPointF(r.center().x() - 0.5*fm.width(word) , r.bottom() +  100)
                self._path.addText(p , self.font(), word)
            self.update()
    
        def wheelEvent(self, event):
            self._factor *= 1.01**(event.angleDelta().y()/15.0)
            self.update()
            super().wheelEvent(event)