You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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

火山引擎 最新活动