Search code examples
pythonpyqtpyqt5qpainter

How to show a preview of the line I'm drawing with QPainter in PyQt5


My code is drawing lines on a QImage using mousePressEvent and mouseReleaseEvent. It works fine but I would like a dynamic preview line to appear when I'm drawing the said line (ie on MouseMoveEvent). Right now the line just appears when I release the left mouse button and I can't see what I'm drawing.

I want the preview of the line to appear and update as I move my mouse, and only "fixate" when I release the left mouse button. Exactly like the MS Paint Line tool : https://youtu.be/YIw9ybdoM6o?t=207

Here is my code (it is derived from the Scribble Example):

from PyQt5.QtCore import QPoint, QRect, QSize, Qt
from PyQt5.QtGui import QImage, QPainter, QPen, QColor, qRgb
from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow
import sys

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

        self.setAttribute(Qt.WA_StaticContents)
        self.scribbling = False
        self.myPenWidth = 1
        self.myPenColor = QColor('#000000') 
        self.image = QImage()
        self.startPoint = QPoint()

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.startPoint = event.pos()
            self.scribbling = True

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton and self.scribbling:
            self.drawLineTo(event.pos())
            self.scribbling = False

    def paintEvent(self, event):
        painter = QPainter(self)
        dirtyRect = event.rect()
        painter.drawImage(dirtyRect, self.image, dirtyRect)

    def resizeEvent(self, event):
        if self.width() > self.image.width() or self.height() > self.image.height():
            newWidth = max(self.width() + 128, self.image.width())
            newHeight = max(self.height() + 128, self.image.height())
            self.resizeImage(self.image, QSize(newWidth, newHeight))
            self.update()

        super(DrawingArea, self).resizeEvent(event)

    def drawLineTo(self, endPoint):
        painter = QPainter(self.image)
        painter.setPen(QPen(self.myPenColor, self.myPenWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
        painter.drawLine(self.startPoint, endPoint)

        rad = self.myPenWidth / 2 + 2
        self.update(QRect(self.startPoint, endPoint).normalized().adjusted(-rad, -rad, +rad, +rad))

    def resizeImage(self, image, newSize):
        if image.size() == newSize:
            return

        newImage = QImage(newSize, QImage.Format_RGB32)
        newImage.fill(qRgb(255, 255, 255))
        painter = QPainter(newImage)
        painter.drawImage(QPoint(0, 0), image)
        self.image = newImage


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.setCentralWidget(DrawingArea())
        self.show()

def main():
    app = QApplication(sys.argv)
    ex = MainWindow()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

I can't figure out how to show the preview of the line I'm drawing and I haven't found a suitable answer yet. How can I go about doing this ?


Solution

  • You can draw the lines just within the paintEvent() method instead than directly on the image, then paint on the image when the mouse is actually released.

    class DrawingArea(QWidget):
        def __init__(self, parent=None):
            super(DrawingArea, self).__init__(parent)
    
            self.setAttribute(Qt.WA_StaticContents)
            self.scribbling = False
            self.myPenWidth = 1
            self.myPenColor = QColor('#000000') 
            self.image = QImage()
            self.startPoint = self.endPoint = None
    
        def mousePressEvent(self, event):
            if event.button() == Qt.LeftButton:
                self.startPoint = event.pos()
    
        def mouseMoveEvent(self, event):
            if self.startPoint:
                self.endPoint = event.pos()
                self.update()
    
        def mouseReleaseEvent(self, event):
            if self.startPoint and self.endPoint:
                self.updateImage()
    
        def paintEvent(self, event):
            painter = QPainter(self)
            dirtyRect = event.rect()
            painter.drawImage(dirtyRect, self.image, dirtyRect)
            if self.startPoint and self.endPoint:
                painter.drawLine(self.startPoint, self.endPoint)
    
        def updateImage(self):
            if self.startPoint and self.endPoint:
                painter = QPainter(self.image)
                painter.setPen(QPen(self.myPenColor, self.myPenWidth, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
                painter.drawLine(self.startPoint, self.endPoint)
                painter.end()
                self.startPoint = self.endPoint = None
                self.update()
    

    Note that you don't need to call update() within the resize event, as it's automatically called.

    I also removed the unnecessary update rect calls, as it's almost useless in this case: specifying a rectangle in which the update should happen is usually done when very complex widgets are drawn (especially when lots of computations are executed to correctly draw everything and only a small part of the widget actually needs updates). In your case, it's almost more time consuming to compute the actual update rectangle than painting all the contents of the widget.