Reputation: 33
I'm new to Qt, I am trying to make a paint application using QGraphicsScene and QGraphicsView. The only way to draw i found out is to add circles and lines to QGraphicsScene on mouseMoveEvent. It works fine, but is there a way to draw like in FabricJS(when added items has the same resolution as an image)?
PyQt drawing:
fabricJS drawing:
My code:
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_())
Edit: I found out another way to draw recently:
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_())
Upvotes: 2
Views: 816
Reputation: 48529
This is caused by the fact that shape items are always vectorial, so there is no concept of "resolution": no matter the scale, a circle is always a circle, as opposed to raster images which use the concept of pixels.
Since the smooth transformation used for scaling is similar to the blur effect, a possibility is to use the QGraphicsBlurEffect for the items, with a blurRadius
value of 1
(as in "1 pixel").
While you could set the effect on each item, that wouldn't be a good choice for performance reasons: you should instead group all those items in a single parent item. Qt provides the QGraphicsGroupItem class that can be easily created by using 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)
Consider that you might want to temporarily disable the graphics effect whenever you are going to export the image.
Further notes:
addEllipse()
, as they always use integer values), you should always round scene positions, as did above; alternatively, just convert the scene point to a QPoint and a QPointF again: scenePos = QPointF(self.mapToScene(event.pos()).toPoint())
self.grScene.drawLine(self.lastPoint, scenePos, self.brush_line_pen)
self.lastPoint = scenePos
self.setSceneRect(self.mainImage.sceneBoundingRect())
in setImage()
(not in fitInView()
); then, if the mouse moves outside of the view very fast, the item will still be visible somewhere in the scene: consider toggling its visibility in the enterEvent
and leaveEvent
of the view; 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)
Upvotes: 2