QGraphicsView分层实现及擦除、图层保存问题咨询
解决PyQt5手绘与背景图层分离的问题
我来帮你搞定这两个问题!核心思路是把手绘内容和背景图片拆成两个独立的图层:背景层放在底层,手绘层放在上层,这样橡皮擦只会作用于手绘层,也能单独导出手绘内容。下面是修改后的完整代码,再逐一解释关键改动:
修改后的完整代码
import sys from PyQt5.QtCore import * from PyQt5.QtCore import Qt from PyQt5.QtGui import * from PyQt5.QtWidgets import * QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) class CWidget(QWidget): def __init__(self): super().__init__() # 整体窗体布局 formbox = QHBoxLayout() self.setLayout(formbox) # 左、右布局框 left = QVBoxLayout() right = QVBoxLayout() # 分组框2:画笔设置 gb = QGroupBox('画笔设置') left.addWidget(gb) grid = QGridLayout() gb.setLayout(grid) label = QLabel('画笔颜色') grid.addWidget(label, 1, 0) self.pencolor = QColor(0, 0, 0) self.penbtn = QPushButton() self.penbtn.setStyleSheet('background-color: rgb(0,0,0)') self.penbtn.clicked.connect(self.showColorDlg) grid.addWidget(self.penbtn, 1, 1) label = QLabel('画笔粗细') grid.addWidget(label, 2, 0) self.slider = QSlider(Qt.Horizontal) self.slider.setMinimum(3) self.slider.setMaximum(21) self.slider.setValue(5) self.slider.setFocusPolicy(Qt.StrongFocus) self.slider.setTickPosition(QSlider.TicksBothSides) self.slider.setTickInterval(1) self.slider.setSingleStep(1) grid.addWidget(self.slider) # 分组框:橡皮擦 gb = QGroupBox('Eraser') left.addWidget(gb) hbox = QHBoxLayout() gb.setLayout(hbox) self.checkbox = QCheckBox('Eraser') self.checkbox.stateChanged.connect(self.checkClicked) hbox.addWidget(self.checkbox) # 新增保存手绘图层按钮 saveBtn = QPushButton('保存手绘图层') saveBtn.clicked.connect(self.saveDrawingLayer) left.addWidget(saveBtn) left.addStretch(1) self.view = CView(self) right.addWidget(self.view) formbox.addLayout(left) formbox.addLayout(right) formbox.setStretchFactor(left, 0) formbox.setStretchFactor(right, 1) self.setGeometry(100, 100, 800, 500) def checkClicked(self, state): pass def showColorDlg(self): color = QColorDialog.getColor() sender = self.sender() if color.isValid(): # 加个判断,避免取消选择时颜色出错 self.pencolor = color self.penbtn.setStyleSheet('background-color: {}'.format(color.name())) def saveDrawingLayer(self): """保存手绘图层为图片""" fileName, _ = QFileDialog.getSaveFileName( self, "保存手绘图层", QDir.currentPath(), filter='PNG图片 (*.png);;JPG图片 (*.jpg)' ) if fileName: self.view.drawingPixmap.save(fileName) # QGraphicsView 显示 QGraphicsScene class CView(QGraphicsView): def __init__(self, parent): super().__init__(parent) self.scene = QGraphicsScene() self.setScene(self.scene) self.start = QPointF() self.end = QPointF() self.backgroundImage = None self.graphicsPixmapItem = None # 新增手绘图层:用QPixmap承载手绘内容 self.drawingPixmap = QPixmap() self.drawingLayer = QGraphicsPixmapItem() self.scene.addItem(self.drawingLayer) self.setRenderHint(QPainter.HighQualityAntialiasing) self.open() def resizeEvent(self, e): # 窗口大小变化时同步更新图层 if self.backgroundImage: self._set_image(False) super().resizeEvent(e) def mousePressEvent(self, e): if e.button() == Qt.LeftButton: # 转换为scene坐标,避免视图缩放影响位置 self.start = self.mapToScene(e.pos()) self.end = self.start def mouseMoveEvent(self, e): if e.buttons() & Qt.LeftButton: self.end = self.mapToScene(e.pos()) painter = QPainter(self.drawingPixmap) painter.setRenderHint(QPainter.HighQualityAntialiasing) if self.parent().checkbox.isChecked(): # 橡皮擦模式:用清除混合模式,只擦除手绘内容 pen = QPen(Qt.transparent, self.parent().slider.value()) pen.setCapStyle(Qt.RoundCap) # 圆形笔触更自然 painter.setCompositionMode(QPainter.CompositionMode_Clear) painter.setPen(pen) else: # 画笔模式:正常绘制 pen = QPen(self.parent().pencolor, self.parent().slider.value()) pen.setCapStyle(Qt.RoundCap) painter.setPen(pen) # 绘制线条 painter.drawLine(self.start, self.end) painter.end() # 更新手绘图层 self.drawingLayer.setPixmap(self.drawingPixmap) # 更新起点 self.start = self.end def open(self): fileName, _ = QFileDialog.getOpenFileName(self, "Open File", QDir.currentPath(), filter='Images (*.png *.xpm *.jpg *jpeg)') if fileName: image = QImage(fileName) if image.isNull(): QMessageBox.information(self, "Image Viewer", "Cannot load %s." % fileName) return self.backgroundImage = fileName self._set_image(False) def _set_image(self, stretch: bool): tempImg = QPixmap(self.backgroundImage) # 确定目标大小:拉伸时用视图大小,否则用原图大小 targetSize = QSize(self.viewport().width(), self.viewport().height()) if stretch else tempImg.size() if stretch: tempImg = tempImg.scaled(targetSize, Qt.KeepAspectRatio) # 更新背景层(放在底层) if self.graphicsPixmapItem is not None: self.scene.removeItem(self.graphicsPixmapItem) self.graphicsPixmapItem = QGraphicsPixmapItem(tempImg) self.graphicsPixmapItem.setZValue(0) self.scene.addItem(self.graphicsPixmapItem) # 更新手绘图层(放在上层,透明背景) self.drawingPixmap = QPixmap(targetSize) self.drawingPixmap.fill(Qt.transparent) self.drawingLayer.setPixmap(self.drawingPixmap) self.drawingLayer.setZValue(1) if __name__ == '__main__': app = QApplication(sys.argv) w = CWidget() w.show() sys.exit(app.exec_())
关键改动说明
1. 分离背景与手绘图层
新增了self.drawingPixmap(承载手绘内容的画布)和self.drawingLayer(显示手绘内容的场景项),并通过setZValue设置层级:
- 背景层
graphicsPixmapItem的Z值为0(底层) - 手绘层
drawingLayer的Z值为1(上层)
这样所有绘制操作都只影响手绘层,完全不会触及背景。
2. 修复橡皮擦功能
不再用白色画笔“覆盖”背景,而是使用QPainter.CompositionMode_Clear混合模式:
- 这个模式会直接清除手绘层的像素(变成透明),只会擦除你画的内容,背景层完全不受影响
- 同时给画笔设置了
Qt.RoundCap,让橡皮擦的笔触更圆润自然
3. 实现手绘图层单独保存
添加了“保存手绘图层”按钮,点击后调用self.view.drawingPixmap.save(fileName),直接将手绘画布的内容导出为图片(支持PNG/JPG格式),导出的图片背景是透明的(如果需要白色背景,可以在保存前给drawingPixmap填充白色)。
额外优化
- 将鼠标坐标转换为场景坐标(
mapToScene),避免视图缩放时绘制位置偏移 - 窗口大小变化时自动同步图层大小
- 增加了颜色选择的有效性判断,避免取消选择时出错
内容的提问来源于stack exchange,提问作者Park changhae




