PyQt5 - 撤消实现

PyQt5 - Undo implementation

我需要在此小部件中实现撤消功能,使用组合键 Ctrl + Z 激活。我可以在输入到构造函数的图像上画线。因此,我的想法是从行列表中删除最后一项(我每次绘制一行时都会在此列表中添加一行)并在按 Ctrl + Z 时重绘所有其他行。如何实现此刷新?有没有更有效的方法来做这样的事情?

代码:

from PyQt5 import QtWidgets, Qt
from PyQt5.QtCore import QSize, QPoint
from PyQt5.QtGui import QImage
import numpy as np
import sys
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QPixmap, QPainter, QPen

class DistanceWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(DistanceWindow, self).__init__(parent)
        self.axial = np.random.rand(512, 512)
        print(self.axial.shape)
        self.axial = QPixmap(QImage(self.axial, self.axial.shape[1], self.axial.shape[0], QImage.Format_Indexed8))
        self.axialWidget = DrawWidget(self.axial)


class DrawWidget(QtWidgets.QWidget):
    def __init__(self, image):
        super().__init__()
        self.drawing = False
        self.startPoint = None
        self.endPoint = None
        self.image = image
        self.setGeometry(100, 100, 500, 300)
        self.resize(self.image.width(), self.image.height())
        self.show()
        self.lines = []

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.startPoint = event.pos()

    def mouseMoveEvent(self, event):
        if self.startPoint:
            self.endPoint = event.pos()
            self.update()

    def mouseReleaseEvent(self, event):
        if self.startPoint and self.endPoint:
            self.updateImage()

    def paintEvent(self, event):
        painter = QPainter(self)
        dirtyRect = event.rect()
        painter.drawImage(dirtyRect, QImage(self.image), dirtyRect)
        if self.startPoint and self.endPoint:
            painter.drawLine(self.startPoint, self.endPoint)

    def updateImage(self):
        if self.startPoint and self.endPoint:
            painter = QPainter(self.image)
            painter.setPen(QPen(Qt.red, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
            painter.drawLine(self.startPoint, self.endPoint)
            firstPoint = np.array([self.startPoint.x(), self.startPoint.y()])
            secondPoint = np.array([self.endPoint.x(), self.endPoint.y()])
            distance = np.sqrt((secondPoint[0]-firstPoint[0])**2 + (secondPoint[1]-firstPoint[1])**2)
            painter.setPen(QPen(Qt.yellow))
            painter.drawText(secondPoint[0], secondPoint[1] + 10, str(distance) + 'mm')
            #line info
            line = {}
            line['points'] = [self.startPoint, self.endPoint]
            line['distance'] = distance
            self.lines.append(line)
            #####################################
            painter.end()
            self.startPoint = self.endPoint = None
            self.update()

    def keyPressEvent(self, event):
        if event.key() == (Qt.Key_Control and Qt.Key_Z):
            self.undo()

    def undo(self):
        #Delete the last line from self.lines and draw all the others

if __name__ == '__main__':
    app = QApplication(sys.argv)
    main = DistanceWindow()
    sys.exit(app.exec_())
    

更新.

我修复了bug。

如果多次画线,多次Ctrl+Z,重做,

你得到了最新的图像。

所以,我修复了我的代码,你可以来一一执行。对不起。

请试试这个。

画线后,

然后依次按下 Ctrl+Z & Ctrl+Y

您可以撤消和重做实施。

我想比较一下如何实现它们。

一般情况下,如果要实现undo&redo,可以使用QUndoStack。 具体执行写在QUndoCommand.

from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt, QLineF
from PyQt5.QtGui import QPainter, QImage, QPen
import sys
import numpy as np

class UndoCommand(QtWidgets.QUndoCommand):
    def __init__(self, startPoint, endPoint, image, parent=None):
        super(UndoCommand, self).__init__()
        self.draw_widget = parent
        self._startPoint = startPoint
        self._endPoint = endPoint
        self.image = image
        #originalimage
        self._image = QImage(image)
        self._init = True
        self.last_line = None
    def redo(self):
        #the contents of updateImage
        if self._startPoint and self._endPoint and self._init:
            painter = QPainter(self.image)
            painter.setPen(QPen(Qt.red, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
            painter.drawLine(self._startPoint, self._endPoint)
            firstPoint = np.array([self._startPoint.x(), self._startPoint.y()])
            secondPoint = np.array([self._endPoint.x(), self._endPoint.y()])
            delta = (secondPoint - firstPoint)
            delta = np.multiply(delta, self.draw_widget.pixelSpacing)
            distance = np.sqrt(delta[0] ** 2 + delta[1] ** 2)
            painter.setPen(QPen(Qt.yellow))
            painter.drawText(secondPoint[0], secondPoint[1] + 10, str(distance) + 'mm')
            #line info              
            line = {}
            line['points'] = [self._startPoint, self._endPoint]
            line['distance'] = distance
            self.draw_widget.lines.append(line)
            #####################################
            self.last_line = line            
            painter.end()            
            self.draw_widget.startPoint = self.draw_widget.endPoint = None
            self.draw_widget.image = self.image
            self.draw_widget.update()
            self._init = False
        else:
            self.draw_widget.lines.append(self.last_line)
            self.draw_widget.image = self.image
            self.draw_widget.update()
    def undo(self):
        self.draw_widget.image = self._image
        self.draw_widget.lines.remove(self.last_line) 
        self.draw_widget.update()
class DrawWidget(QtWidgets.QWidget):
    def __init__(self, image, pixelSpacing):
        super().__init__()
        self.undostack = QtWidgets.QUndoStack()
        self.lines = []
        self.drawing = False
        self.startPoint = None
        self.endPoint = None
        self.pixelSpacing = pixelSpacing
        self.image = image
        self.setGeometry(100, 100, 500, 300)
        self.resize(self.image.width(), self.image.height())        
        self.show()        
        
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.startPoint = event.pos()

    def mouseMoveEvent(self, event):
        if self.startPoint:
            self.endPoint = event.pos()
            self.update()

    def mouseReleaseEvent(self, event):
        if self.startPoint and self.endPoint:
            undocommand = UndoCommand(self.startPoint, self.endPoint, QImage(self.image), self)
            self.undostack.push(undocommand)
            #self.undostack.redo does the same thing instead of this method.You need the same code in the redo method and add some codes for it.
#            self.updateImage()

    def paintEvent(self, event):
        painter = QPainter(self)
        dirtyRect = event.rect()
        painter.drawImage(dirtyRect, QImage(self.image), dirtyRect)
        if self.startPoint and self.endPoint:
            painter.drawLine(self.startPoint, self.endPoint)


#    def updateImage(self):
#        if self.startPoint and self.endPoint:
#            painter = QPainter(self.image)
#            painter.setPen(QPen(Qt.red, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
#            painter.drawLine(self.startPoint, self.endPoint)
#            firstPoint = np.array([self.startPoint.x(), self.startPoint.y()])
#            secondPoint = np.array([self.endPoint.x(), self.endPoint.y()])
#            delta = (secondPoint - firstPoint)
#            delta = np.multiply(delta, self.pixelSpacing)
#            distance = np.sqrt(delta[0] ** 2 + delta[1] ** 2)
#            painter.setPen(QPen(Qt.yellow))
#            painter.drawText(secondPoint[0], secondPoint[1] + 10, str(distance) + 'mm')
#            #line info
#            line = {}
#            line['points'] = [self.startPoint, self.endPoint]
#            line['distance'] = distance
#            self.lines.append(line)
#            #####################################
#            painter.end()
#            self.startPoint = self.endPoint = None
#            self.update()

    def keyPressEvent(self, event):
        if event.key() == (Qt.Key_Control and Qt.Key_Y):
            self.undostack.redo()
        if event.key() == (Qt.Key_Control and Qt.Key_Z):
            self.undostack.undo()

    def undo(self):
        #this is not used.
        pass
        #Delete the last line from self.lines and draw all the others
        
def main():
    
    app = QtWidgets.QApplication([]) if QtWidgets.QApplication.instance() is None else QtWidgets.QApplication.instance()
    app.setStyle(QtWidgets.QStyleFactory.create('Fusion'))
    d =  DrawWidget(QImage("first.png"), 10)
    d.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

当要实现撤消支持时,“可撤消”对象必须能够恢复其先前的状态。这对于基于光栅的图像来说显然是不可能的,因为“绘画”被认为是破坏性的:一旦像素颜色被改变,就无法知道它之前的状态。

一种可能性是存储以前的光栅状态,但肯定不建议使用这种方法:如果您总是存储完整的图像,您将面临使用过多内存的风险,并且实施只存储部分图像的系统已修改的图像肯定不适合您的情况。

处理矢量图形时,最简单的方法是将更改存储为绘画“例程”,只在实际需要时才保存图像,这样修改就只用小部件的 paintEvent 绘制(显然您需要修改 updateImage 函数以实际存储图像)。
这通常要快得多,并且允许任意删除绘画功能。

在下面的示例中,我使用了您已经创建的 self.lines,但进行了一些修改以使事情变得更简单和更清晰。

class DrawWidget(QtWidgets.QWidget):
    # ...

    def mouseMoveEvent(self, event):
        if self.startPoint:
            self.endPoint = event.pos()
            self.update()

    def mouseReleaseEvent(self, event):
        if self.startPoint and self.endPoint:
            line = QLineF(self.startPoint, self.endPoint)
            self.lines.append({
                'points': line, 
                'distance': line.length() * self.pixelSpacing, 
            })
            self.startPoint = self.endPoint = None
            self.update()

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHints(painter.Antialiasing)
        dirtyRect = event.rect()
        painter.drawImage(dirtyRect, QImage(self.image), dirtyRect)
        if self.startPoint and self.endPoint:
            painter.drawLine(self.startPoint, self.endPoint)
        linePen = QPen(Qt.red, 2, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
        for lineData in self.lines:
            line = lineData['points']            
            painter.setPen(linePen)
            painter.drawLine(line.p1(), line.p2())
            painter.setPen(Qt.yellow)
            painter.drawText(line.p2() + QPoint(0, 10), 
                '{}mm'.format(lineData['distance']))

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Z and event.modifiers() == Qt.ControlModifier:
            self.undo()

    def undo(self):
        if self.lines:
            self.lines.pop(-1)
            self.update()

关于修改的一些说明。
你正在做的事情绝对不需要 NumPy。如您所见,您可以使用 Qt 的 类 和函数计算出您需要的一切;最重要的是,在本例中,我使用了 QLineF,它是两点之间浮点精度向量的抽象表示(两点之间的距离可以通过 QLineF(p1, p2).length() 获得)。虽然这显然比 python 的 math 或 numpy 的函数慢一点,但在这种情况下使用 QLine 肯定更好,原因如下:无论如何你都需要一条线,你不需要不需要 30-40mb python 模块来计算毕达哥拉斯距离,它是代表单个对象的单个对象,它使代码更简单。
按键事件不能与二元运算符一起使用,因为它们是整数,而不是二进制标志:实际上,即使仅按 Z 或使用其他修饰符,您的代码也会调用 undoCtrl 键是 modifier and as such cannot be combined with standard keys when looking for keyboard combination, so you need to check event.modifiers()
这显然是一个非常基本的实现,您可以通过存储当前“命令”的索引来添加“重做”支持。

最后,对于更复杂的用户案例,还有 QUndo framework,它比您可能需要的要复杂一些,但了解它并了解何时真正需要它仍然很重要.