How can i make 2-layers in QGraphicsView?

2020-04-19 19:16发布

问题:

In the program below, load the background image and paint it on it.

But, I got a problem.

In this program, when i use 'eraser' tool, the background image is erased too!

Actually, I just want to erase what i painted, except background image.

And then, I'd like to save only the painted ones(layer) as an image.

In this case, What should i do?

import sys

from PyQt5.QtCore import *
from PyQt5.QtCore import Qt
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtWidgets import (QApplication, QCheckBox, QGridLayout, QGroupBox,
                             QPushButton, QVBoxLayout, QWidget, QSlider)

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)

        # 그룹박스4
        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)

        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 createExampleGroup(self):
        groupBox = QGroupBox("Slider Example")

        slider = QSlider(Qt.Horizontal)
        slider.setFocusPolicy(Qt.StrongFocus)
        slider.setTickPosition(QSlider.TicksBothSides)
        slider.setTickInterval(10)
        slider.setSingleStep(1)

        vbox = QVBoxLayout()
        vbox.addWidget(slider)
        vbox.addStretch(1)
        groupBox.setLayout(vbox)

        return groupBox

    def showColorDlg(self):

        color = QColorDialog.getColor()

        sender = self.sender()

        self.pencolor = color
        self.penbtn.setStyleSheet('background-color: {}'.format(color.name()))


# QGraphicsView display QGraphicsScene
class CView(QGraphicsView):

    def __init__(self, parent):

        super().__init__(parent)
        self.scene = QGraphicsScene()

        self.setScene(self.scene)

        self.items = []

        self.start = QPointF()
        self.end = QPointF()

        self.backgroundImage = None
        self.graphicsPixmapItem = None

        self.setRenderHint(QPainter.HighQualityAntialiasing)

        self.open()

    def moveEvent(self, e):
        rect = QRectF(self.rect())
        rect.adjust(0, 0, -2, -2)

        self.scene.setSceneRect(rect)

    def mousePressEvent(self, e):

        if e.button() == Qt.LeftButton:
            # 시작점 저장
            self.start = e.pos()
            self.end = e.pos()

    def mouseMoveEvent(self, e):

        # e.buttons()는 정수형 값을 리턴, e.button()은 move시 Qt.Nobutton 리턴
        if e.buttons() & Qt.LeftButton:

            self.end = e.pos()

            if self.parent().checkbox.isChecked():
                pen = QPen(QColor(255, 255, 255), 10)
                path = QPainterPath()
                path.moveTo(self.start)
                path.lineTo(self.end)
                self.scene.addPath(path, pen)
                self.start = e.pos()
                return None

            pen = QPen(self.parent().pencolor, self.parent().slider.value())

            # Path 이용
            path = QPainterPath()
            path.moveTo(self.start)
            path.lineTo(self.end)
            self.scene.addPath(path, pen)

            # 시작점을 다시 기존 끝점으로
            self.start = e.pos()

    def stretch(self, state):
        self._set_image(state == 2)

    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)

        if stretch:
            tempImg = tempImg.scaled(self.scene.width(), self.scene.height())

        if self.graphicsPixmapItem is not None:
            self.scene.removeItem(self.graphicsPixmapItem)

        self.graphicsPixmapItem = QGraphicsPixmapItem(tempImg)
        self.scene.addItem(self.graphicsPixmapItem)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = CWidget()
    w.show()
    sys.exit(app.exec_())

回答1:

You can create another transparent item where you draw and that is on the QGraphicsPixmapItem. For painting it is only necessary to draw on a transparent QPixmap that is in the transparent item, and for the deletion we use the composition mode QPainter::CompositionMode_Clear as I indicate in this answer.

Considering the above the solution is:

import sys
from PyQt5 import QtCore, QtGui, QtWidgets


class LayerItem(QtWidgets.QGraphicsRectItem):
    DrawState, EraseState = range(2)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.current_state = LayerItem.DrawState
        self.setPen(QtGui.QPen(QtCore.Qt.NoPen))

        self.m_line_eraser = QtCore.QLineF()
        self.m_line_draw = QtCore.QLineF()
        self.m_pixmap = QtGui.QPixmap()

    def reset(self):
        r = self.parentItem().pixmap().rect()
        self.setRect(QtCore.QRectF(r))
        self.m_pixmap = QtGui.QPixmap(r.size())
        self.m_pixmap.fill(QtCore.Qt.transparent)

    def paint(self, painter, option, widget=None):
        super().paint(painter, option, widget)
        painter.save()
        painter.drawPixmap(QtCore.QPoint(), self.m_pixmap)
        painter.restore()

    def mousePressEvent(self, event):
        if self.current_state == LayerItem.EraseState:
            self._clear(event.pos().toPoint())
        elif self.current_state == LayerItem.DrawState:
            self.m_line_draw.setP1(event.pos())
            self.m_line_draw.setP2(event.pos())
        super().mousePressEvent(event)
        event.accept()

    def mouseMoveEvent(self, event):
        if self.current_state == LayerItem.EraseState:
            self._clear(event.pos().toPoint())
        elif self.current_state == LayerItem.DrawState:
            self.m_line_draw.setP2(event.pos())
            self._draw_line(
                self.m_line_draw, QtGui.QPen(self.pen_color, self.pen_thickness)
            )
            self.m_line_draw.setP1(event.pos())
        super().mouseMoveEvent(event)

    def _draw_line(self, line, pen):
        painter = QtGui.QPainter(self.m_pixmap)
        painter.setPen(pen)
        painter.drawLine(line)
        painter.end()
        self.update()

    def _clear(self, pos):
        painter = QtGui.QPainter(self.m_pixmap)
        r = QtCore.QRect(QtCore.QPoint(), 10 * QtCore.QSize())
        r.moveCenter(pos)
        painter.setCompositionMode(QtGui.QPainter.CompositionMode_Clear)
        painter.eraseRect(r)
        painter.end()
        self.update()

    @property
    def pen_thickness(self):
        return self._pen_thickness

    @pen_thickness.setter
    def pen_thickness(self, thickness):
        self._pen_thickness = thickness

    @property
    def pen_color(self):
        return self._pen_color

    @pen_color.setter
    def pen_color(self, color):
        self._pen_color = color

    @property
    def current_state(self):
        return self._current_state

    @current_state.setter
    def current_state(self, state):
        self._current_state = state


class GraphicsView(QtWidgets.QGraphicsView):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setScene(QtWidgets.QGraphicsScene(self))
        self.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
        self.setAlignment(QtCore.Qt.AlignCenter)

        self.background_item = QtWidgets.QGraphicsPixmapItem()
        self.foreground_item = LayerItem(self.background_item)

        self.scene().addItem(self.background_item)

    def set_image(self, image):
        self.scene().setSceneRect(
            QtCore.QRectF(QtCore.QPointF(), QtCore.QSizeF(image.size()))
        )
        self.background_item.setPixmap(image)
        self.foreground_item.reset()
        self.fitInView(self.background_item, QtCore.Qt.KeepAspectRatio)
        self.centerOn(self.background_item)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        menu = self.menuBar().addMenu(self.tr("File"))
        open_action = menu.addAction(self.tr("Open image..."))
        open_action.triggered.connect(self.open_image)

        pen_group = QtWidgets.QGroupBox(self.tr("Pen settings"))
        eraser_group = QtWidgets.QGroupBox(self.tr("Eraser"))

        self.pen_button = QtWidgets.QPushButton(clicked=self.showColorDlg)
        color = QtGui.QColor(0, 0, 0)
        self.pen_button.setStyleSheet(
            "background-color: {}".format(color.name())
        )
        self.pen_slider = QtWidgets.QSlider(
            QtCore.Qt.Horizontal,
            minimum=3,
            maximum=21,
            value=5,
            focusPolicy=QtCore.Qt.StrongFocus,
            tickPosition=QtWidgets.QSlider.TicksBothSides,
            tickInterval=1,
            singleStep=1,
            valueChanged=self.onThicknessChanged,
        )

        self.eraser_checkbox = QtWidgets.QCheckBox(
            self.tr("Eraser"), stateChanged=self.onStateChanged
        )

        self.view = GraphicsView()
        self.view.foreground_item.pen_thickness = self.pen_slider.value()
        self.view.foreground_item.pen_color = color

        # layouts
        pen_lay = QtWidgets.QFormLayout(pen_group)
        pen_lay.addRow(self.tr("Pen color"), self.pen_button)
        pen_lay.addRow(self.tr("Pen thickness"), self.pen_slider)

        eraser_lay = QtWidgets.QVBoxLayout(eraser_group)
        eraser_lay.addWidget(self.eraser_checkbox)

        vlay = QtWidgets.QVBoxLayout()
        vlay.addWidget(pen_group)
        vlay.addWidget(eraser_group)
        vlay.addStretch()

        central_widget = QtWidgets.QWidget()
        self.setCentralWidget(central_widget)

        lay = QtWidgets.QHBoxLayout(central_widget)
        lay.addLayout(vlay, stretch=0)
        lay.addWidget(self.view, stretch=1)

        self.resize(640, 480)

    @QtCore.pyqtSlot(int)
    def onStateChanged(self, state):
        self.view.foreground_item.current_state = (
            LayerItem.EraseState
            if state == QtCore.Qt.Checked
            else LayerItem.DrawState
        )

    @QtCore.pyqtSlot(int)
    def onThicknessChanged(self, value):
        self.view.foreground_item.pen_thickness = value

    @QtCore.pyqtSlot()
    def showColorDlg(self):
        color = QtWidgets.QColorDialog.getColor(
            self.view.foreground_item.pen_color, self
        )
        self.view.foreground_item.pen_color = color
        self.pen_button.setStyleSheet(
            "background-color: {}".format(color.name())
        )

    def open_image(self):
        filename, _ = QtWidgets.QFileDialog.getOpenFileName(
            self,
            "Open File",
            QtCore.QDir.currentPath(),
            filter="Images (*.png *.xpm *.jpg *jpeg)",
        )
        if filename:
            pixmap = QtGui.QPixmap(filename)
            if pixmap.isNull():
                QtWidgets.QMessageBox.information(
                    self, "Image Viewer", "Cannot load %s." % filename
                )
                return
            self.view.set_image(pixmap)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())