python - PyQt5 QGraphicsView绘图分辨率

标签 python pyqt5 qgraphicsview qgraphicsscene

我是 Qt 的新手,我正在尝试使用 QGraphicsScene 和 QGraphicsView 制作一个绘画应用程序。我发现的唯一绘制方法是在 mouseMoveEvent 上向 QGraphicsScene 添加圆圈和线条。它工作正常,但有没有办法像 FabricJS 那样绘制(当添加的项目与图像具有相同的分辨率时)?

PyQt绘图:

PyQt drawing

fabricJS 绘图:

fabricJS drawing

我的代码:

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *


class QDMWorkingAreaScene(QGraphicsScene):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._color_background = QColor("#393939")

        self.textItems = []
        self.drawingItems = []

        self.empty = True
        self.mainImage = QGraphicsPixmapItem()
        self.mainImage.setTransformationMode(Qt.SmoothTransformation)
        self.dirtySpeechBubbles = []
        self.setBackgroundBrush(self._color_background)


    def setImage(self, pixmap=None):
        if pixmap and not pixmap.isNull():
            self.empty = False
            self.mainImage.setPixmap(pixmap)
        else:
            self.empty = True
            self.mainImage.setPixmap(QPixmap())
        self.addItem(self.mainImage)
        #self.fitInView()

    def hasPhoto(self):
        return not self.empty

    def drawCircle(self, x, y, brushSize, pen, brush):
        self.drawingItems.append(self.addEllipse(x, y, brushSize, brushSize, pen, brush))
        print(len(self.drawingItems))

    def drawLine(self, start_x, start_y, x, y, pen):
        self.drawingItems.append(self.addLine(start_x, start_y, x, y, pen))
        print(len(self.drawingItems))


class QDMGraphicsView(QGraphicsView):
    def __init__(self, grScene, parent = None):
        super().__init__(parent)
        self.empty = True

        #brush drawing settings
        self.drawingMode = True
        self.brushSize = 10
        self.brushColor = Qt.black
        self.lastPoint = QPoint()
        self.brush_line_pen = QPen(self.brushColor, self.brushSize, Qt.SolidLine, Qt.RoundCap)

        #scene settings
        self.grScene = grScene
        self.initUI()
        self.setScene(self.grScene)

        #pan settings
        self.setDragMode(QGraphicsView.RubberBandDrag)
        self._isPanning = False
        self._mousePressed = False

        #zoom settings
        self.zoomInFactor = 1.25
        self.zoomOutFactor = 0.8
        self.zoomClamp = False
        self.zoom = 10
        self.zoomStep = 1
        self.zoomRange = [0, 20]
        if self.drawingMode:
            self.brush = self.grScene.addEllipse(0, 0, self.brushSize, self.brushSize, QPen(Qt.NoPen), self.brushColor)
            self.brush.setFlag(QGraphicsItem.ItemIsMovable)
            self.brush.setZValue(100)

    def initUI(self):
        self.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)
        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)

    def setMainImage(self, pixmapItem):
        self.grScene.setImage(pixmapItem)

    def mousePressEvent(self,  event):
        if self.drawingMode and (event.button() == Qt.LeftButton):
            x = self.mapToScene(event.pos()).x()
            y = self.mapToScene(event.pos()).y()
            self.grScene.drawCircle(x - self.brushSize / 2, y - self.brushSize / 2, self.brushSize, QPen(Qt.NoPen), self.brushColor)
            self.lastPoint = self.mapToScene(event.pos())
        elif event.button() == Qt.LeftButton:
            self._mousePressed = True
            if self._isPanning:
                self.setCursor(Qt.ClosedHandCursor)
                self._dragPos = event.pos()
                event.accept()
            else:
                super(QDMGraphicsView, self).mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if self.drawingMode:
            x = self.mapToScene(event.pos()).x()
            y = self.mapToScene(event.pos()).y()
            self.brush.setPos(x - self.brushSize / 2, y - self.brushSize / 2)
        if(event.buttons() & Qt.LeftButton) & self.drawingMode:
            x = self.mapToScene(event.pos()).x()
            y = self.mapToScene(event.pos()).y()
            self.grScene.drawLine(self.lastPoint.x(), self.lastPoint.y(), x, y, self.brush_line_pen)
            self.lastPoint = self.mapToScene(event.pos())

        elif self._mousePressed and self._isPanning:
            newPos = event.pos()
            diff = newPos - self._dragPos
            self._dragPos = newPos
            self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - diff.x())
            self.verticalScrollBar().setValue(self.verticalScrollBar().value() - diff.y())
            event.accept()
        else:
            super(QDMGraphicsView, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            if event.modifiers() & Qt.ControlModifier:
                self.setCursor(Qt.OpenHandCursor)
            else:
                self._isPanning = False
                self.setCursor(Qt.ArrowCursor)
            self._mousePressed = False
        super(QDMGraphicsView, self).mouseReleaseEvent(event)

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Control and not self._mousePressed:
            self._isPanning = True
            self.setCursor(Qt.OpenHandCursor)
        else:
            super(QDMGraphicsView, self).keyPressEvent(event)

    def keyReleaseEvent(self, event):
        if event.key() == Qt.Key_Control:
            if not self._mousePressed:
                self._isPanning = False
                self.setCursor(Qt.ArrowCursor)
        elif event.key() == Qt.Key_Delete:
            self.deleteSelected()
        else:
            super(QDMGraphicsView, self).keyPressEvent(event)

    def deleteSelected(self):
        for item in self.grScene.selectedItems():
            self.grScene.removeItem(item)

    def getZoomStep(self, mode):
        if mode == "+":
            if self.zoom + self.zoomStep not in range(self.zoomRange[0], self.zoomRange[1]):
                return self.zoom, 1
            else:
                return self.zoom + self.zoomStep, self.zoomInFactor
        elif mode == "-":
            if self.zoom - self.zoomStep not in range(self.zoomRange[0], self.zoomRange[1]):
                return self.zoom, 1
            else:
                return self.zoom - self.zoomStep, self.zoomOutFactor
        return 10, 1

    def wheelEvent(self, event):
        if event.angleDelta().y() > 0:
            self.zoom, zoomFactor = self.getZoomStep("+")
        else:
            self.zoom, zoomFactor = self.getZoomStep("-")
        self.scale(zoomFactor, zoomFactor)

    def fitInView(self, scale=True):
        rect = QRectF(self.grScene.mainImage.pixmap().rect())
        if not rect.isNull():
            self.setSceneRect(rect)
            if self.grScene.hasPhoto():
                unity = self.transform().mapRect(QRectF(0, 0, 1, 1))
                self.scale(1 / unity.width(), 1 / unity.height())
            self.zoom = 5

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

    def loadImage(self):
        self.view.setMainImage(QPixmap('roi.jpg'))

    def initUI(self):
        self.layout = QVBoxLayout()
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(self.layout)
        self.grScene = QDMWorkingAreaScene()
        self.view = QDMGraphicsView(self.grScene, self)
        self.layout.addWidget(self.view)
        gl = QOpenGLWidget()
        gl.setMouseTracking(True)
        format = QSurfaceFormat()
        format.setSamples(4)
        gl.setFormat(format)
        self.view.setViewport(gl)
        self.setWindowTitle("AutoMangaCleaner")
        self.loadImage()
        self.show()
        #self.showMaximized()

if __name__ == "__main__":
    app = QApplication(sys.argv)

    window = WorkingArea()

    sys.exit(app.exec_())

编辑: 最近发现了另一种画法:

from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys


class Canvas(QGraphicsPixmapItem):
    def __init__(self, image=None):
        super().__init__()
        self.last_pos = QPoint()

    def setImage(self, image):
        self.pixmap = image
        self.pixmap_clone = self.pixmap.copy()
        self.last_pos = QPoint()
        self.setPixmap(self.pixmap)

    def mousePressEvent(self,  event):
        pos = self.mapToParent(event.pos())
        p = QPainter(self.pixmap_clone)
        p.setBrush(Qt.black)
        p.drawEllipse(pos, 5, 5)
        self.last_pos = pos
        self.setPixmap(self.pixmap_clone)

    def mouseMoveEvent(self, event):
        pos = self.mapToScene(event.pos())
        if(event.buttons() & Qt.LeftButton):
            p = QPainter(self.pixmap_clone)
            p.setPen(QPen(Qt.black, 10, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
            p.drawLine(self.last_pos, event.pos())
            self.last_pos = pos
            self.setPixmap(self.pixmap_clone)



class QDMWorkingAreaScene(QGraphicsScene):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.empty = True
        self._color_background = QColor("#393939")
        self.mainImage = Canvas()
        self.mainImage.setTransformationMode(Qt.SmoothTransformation)
        self.dirtySpeechBubbles = []
        self.setBackgroundBrush(self._color_background)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)

    def setImage(self, pixmap=None):
        if pixmap and not pixmap.isNull():
            self.empty = False
            self.mainImage.setImage(pixmap)
        else:
            self.empty = True
            self.mainImage.setPixmap(QPixmap())
        self.addItem(self.mainImage)
        #self.fitInView()

    def hasPhoto(self):
        return not self.empty


class QDMGraphicsView(QGraphicsView):
    def __init__(self, grScene, parent = None):
        super().__init__(parent)

        self.empty = True
        self.photo = QGraphicsPixmapItem()

        #text settings
        #fonts, color, outline etc.

        #brush drawing settings
        #self.brush = QGraphicsEllipseItem
        self.drawingMode = True
        self.is_drawing = True
        self.brushSize = 10
        self.brushColor = Qt.black
        self.lastPoint = QPoint()
        self.brush_line_pen = QPen(self.brushColor, self.brushSize, Qt.SolidLine, Qt.RoundCap)
        # self.brush = self.grScene.addEllipse(0, 0, self.brushSize, self.brushSize, QPen(Qt.NoPen), self.brushColor)
        #scene settings
        self.grScene = grScene
        self.initUI()
        self.setScene(self.grScene)

        #pan settings
        self.setDragMode(QGraphicsView.RubberBandDrag)
        self.setDragMode(QGraphicsView.NoDrag)
        self._isPanning = False
        self._mousePressed = False

        #zoom settings
        self.zoomInFactor = 1.25
        self.zoomOutFactor = 0.8
        self.zoomClamp = False
        self.zoom = 10
        self.zoomStep = 1
        self.zoomRange = [0, 20]
        if self.drawingMode:
            self.setDragMode(QGraphicsView.NoDrag)
            self.brush = self.grScene.addEllipse(0, 0, self.brushSize, self.brushSize, QPen(Qt.NoPen), self.brushColor)
            self.brush.setAcceptedMouseButtons(Qt.NoButton)
            self.brush.setFlag(QGraphicsItem.ItemIsMovable)
            self.brush.setZValue(100)

    def initUI(self):
        self.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform)
        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)

        #self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        #self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)


    def setMainImage(self, pixmapItem):
        self.grScene.setImage(pixmapItem)
        self.fitInView()

    def mousePressEvent(self,  event):
        #print("view pos:", self.mapToScene(event.pos()))
        if self.drawingMode and (event.button() == Qt.LeftButton):
            super(QDMGraphicsView, self).mousePressEvent(event)
            #self.grScene.mainImage.mousePressEvent(event)
        if event.button() == Qt.LeftButton:
            self._mousePressed = True
            if self._isPanning:
                self.setCursor(Qt.ClosedHandCursor)
                self._dragPos = event.pos()
                event.accept()
            else:
                super(QDMGraphicsView, self).mousePressEvent(event)


    def mouseMoveEvent(self, event):
        if self.drawingMode:
            x = self.mapToScene(event.pos()).x()
            y = self.mapToScene(event.pos()).y()
            self.brush.setPos(x - self.brushSize / 2, y - self.brushSize / 2)
        if(event.buttons() == Qt.LeftButton) & self.drawingMode:
            super(QDMGraphicsView, self).mouseMoveEvent(event)
        elif self._mousePressed and self._isPanning:
            newPos = event.pos()
            diff = newPos - self._dragPos
            self._dragPos = newPos
            self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - diff.x())
            self.verticalScrollBar().setValue(self.verticalScrollBar().value() - diff.y())
            event.accept()
        else:
            super(QDMGraphicsView, self).mouseMoveEvent(event)
        super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            if event.modifiers() == Qt.ControlModifier:
                self.setCursor(Qt.OpenHandCursor)
            else:
                self._isPanning = False
                self.setCursor(Qt.ArrowCursor)
            self._mousePressed = False
        super(QDMGraphicsView, self).mouseReleaseEvent(event)

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Control and not self._mousePressed:
            self.drawingMode = False
            self._isPanning = True
            self.setCursor(Qt.OpenHandCursor)
        else:
            super(QDMGraphicsView, self).keyPressEvent(event)

    def keyReleaseEvent(self, event):
        if event.key() == Qt.Key_Control:
            if self.is_drawing:
                self.drawingMode = True
            if not self._mousePressed:
                self._isPanning = False
                self.setCursor(Qt.ArrowCursor)
        elif event.key() == Qt.Key_Delete:
            self.deleteSelected()
        else:
            super(QDMGraphicsView, self).keyPressEvent(event)

    def deleteSelected(self):
        for item in self.grScene.selectedItems():
            self.grScene.removeItem(item)

    def wheelEvent(self, event):
        if event.angleDelta().y() > 0:
            step = self.zoomStep
            fact = self.zoomInFactor
        else:
            step = -self.zoomStep
            fact = self.zoomOutFactor
        zoom = max(self.zoomRange[0], min(self.zoom + step, self.zoomRange[1]))
        if zoom != self.zoom:
            self.zoom = zoom
            self.scale(fact, fact)

    def fitInView(self, scale=True):
        rect = QRectF(self.grScene.mainImage.pixmap.rect())
        if not rect.isNull():
            self.setSceneRect(rect)
            if self.grScene.hasPhoto():
                unity = self.transform().mapRect(QRectF(0, 0, 1, 1))
                self.scale(1 / unity.width(), 1 / unity.height())
            self.zoom = 5


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

    def loadImage(self):
        self.view.setMainImage(QPixmap('roi.jpg'))

    def initUI(self):
        self.setGeometry(0, 0, 800, 800)
        self.layout = QVBoxLayout()
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(self.layout)

        self.grScene = QDMWorkingAreaScene()

        self.view = QDMGraphicsView(self.grScene, self)
        self.layout.addWidget(self.view)
        gl = QOpenGLWidget()
        gl.setMouseTracking(True)
        format = QSurfaceFormat()
        format.setSamples(4)
        gl.setFormat(format)
        self.view.setViewport(gl)
        self.setWindowTitle("AutoMangaCleaner")
        self.loadImage()
        self.show()
        #self.view.setFocus()

    def mouseMoveEvent(self, event):
        self.view.mouseMoveEvent()


if __name__ == "__main__":
    app = QApplication(sys.argv)

    window = WorkingArea()

    sys.exit(app.exec_())

最佳答案

这是因为形状项总是矢量,所以没有“分辨率”的概念:无论比例如何,圆总是圆,而不是光栅 使用像素概念的图像。

由于用于缩放的平滑变换类似于模糊效果,可能是使用 QGraphicsBlurEffect对于元素,带有 blurRadius 1 的值(如“1 像素”)。

虽然您可以为每个项目设置效果,但出于性能原因,这不是一个好的选择:您应该将所有这些项目分组到一个父项目中。 Qt 提供了 QGraphicsGroupItem可以使用 scene.createItemGroup() 轻松创建的类.

class QDMWorkingAreaScene(QGraphicsScene):
    def __init__(self, parent=None):
        # ...
        self.drawingGroup = self.createItemGroup([])
        blur = QGraphicsBlurEffect(blurRadius=1)
        self.drawingGroup.setGraphicsEffect(blur)
        self.drawingGroup.setZValue(100)

    # ...

    def drawCircle(self, x, y, brushSize, pen, brush):
        item = QGraphicsEllipseItem(
            round(x), round(y), 
            brushSize, brushSize, 
            self.drawingGroup
        )
        item.setPen(pen)
        item.setBrush(brush)

    def drawLine(self, start_x, start_y, x, y, pen):
        item = QGraphicsLineItem(
            round(start_x), round(start_y), 
            round(x), round(y), 
            self.drawingGroup
        )
        item.setPen(pen)

考虑到您可能希望在导出图像时暂时禁用图形效果。

补充说明:

  • 同样,出于优化原因,绘制连续线条时应使用 QPainterPathItem,并向其路径添加线条,直到松开鼠标,然后在再次按下鼠标时创建新项目;
  • 在创建椭圆或开始新线/路径时,您应该区分,否则您将始终两者
  • 因为您可能希望始终使用非十进制值(这是使用基本构造函数 addEllipse() 时发生的情况,因为它们始终使用整数值),您应该始终将场景位置四舍五入,和上面一样;或者,再次将场景点转换为 QPoint 和 QPointF:
    scenePos = QPointF(self.mapToScene(event.pos()).toPoint())
    self.grScene.drawLine(self.lastPoint, scenePos, self.brush_line_pen)
    self.lastPoint = scenePos
  • 为“画笔光标”使用图形项有重要的副作用:首先,每当它移动靠近场景的边缘时,场景将增加其边界矩形,因此您应该可能在 setImage() 中执行 self.setSceneRect(self.mainImage.sceneBoundingRect()) (而不是在 fitInView() 中);然后,如果鼠标非常快地移出 View ,该项目仍将在场景中的某处可见:考虑在 View 的 enterEventleaveEvent 中切换其可见性;
  • 如果不够小心,为鼠标按钮和按键使用标志可能会很危险;使用正确的 event properties相反,哪个更简单明了:
    if event.buttons() == Qt.LeftButton:
        # left mouse button pressed *during* a mouse move event
    if event.modifiers() == Qt.ControlModifier:
        # Ctrl key pressed
  • 简化缩放功能:
    def wheelEvent(self, event):
        if event.angleDelta().y() > 0:
            step = self.zoomStep
            fact = self.zoomInFactor
        else:
            step = -self.zoomStep
            fact = self.zoomOutFactor
        zoom = max(self.zoomRange[0], min(self.zoom + step, self.zoomRange[1]))
        if zoom != self.zoom:
            self.zoom = zoom
            self.scale(fact, fact)

关于python - PyQt5 QGraphicsView绘图分辨率,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73676005/

相关文章:

python - xhtml2pdf 和 django,不同的文档大小

c++ - 如何在 qgraphicsview 中进行非平滑/逐步滚动

c++ - QGraphicsView/Scene - 项目绘制距离鼠标点击 2 倍

python - 在 Python 和 linux 中如何获取给定的用户 ID

Python:来自其他文件工作目录的execfile?

python-3.x - Python3 + Pillow + QT5 : Crash when I resize a label containing an image

python - 向应用程序用户提供视觉反馈

c++ - 在另一个类的表单上添加 QGraphicsView

python - 使用 Python 多处理的高内存使用率

python - PyQt5:使用 QtDesigner,如何将信号与模块中定义的插槽/可调用对象连接起来