【问题标题】:PyQt5 - Undo implementationPyQt5 - 撤消实现
【发布时间】:2021-02-28 14:49:16
【问题描述】:

我需要在这个小部件中实现撤消功能,使用组合键 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_())
    

【问题讨论】:

  • 这需要一个最小的可重现示例。请制作出来,以便人们可以直接运行代码并对其进行测试。
  • @JussiNurminen 谢谢,我添加了重现示例的代码

标签: python user-interface pyqt pyqt5


【解决方案1】:

更新

我修复了错误。

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

你会得到最新的图像。

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

请试试这个。

画线后,

然后你依次按下Ctrl+ZCtrl+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()

【讨论】:

  • 非常感谢您提供这个替代解决方案,重做功能的想法非常有趣
【解决方案2】:

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

一种可能是存储之前的光栅状态,但当然不建议使用这种方法:如果您始终存储完整图像,您将面临使用过多内存的风险,并实现仅存储图像部分的系统已修改的内容当然不适合您的情况。

在处理矢量图形的时候,最简单的方式就是将修改存储为绘制“例程”,只在实际需要的时候保存图片,这样修改就只用widget的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,因此在查找键盘组合时不能与标准键组合,因此您需要检查event.modifiers()
这显然是一个非常基础的实现,只需存储当前“命令”的索引即可添加“重做”支持。

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

【讨论】:

  • 非常感谢,这对我来说似乎是一个非常合理的解决方案,适合我的情况。我最近开始使用 PyQt5,所以我还是很缺乏经验。感谢您的帮助!
猜你喜欢
  • 1970-01-01
  • 2016-02-18
  • 2011-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-12-16
  • 2011-11-14
  • 2021-06-04
相关资源
最近更新 更多