PyQt5 - 撤消实现 [英] PyQt5 - Undo implementation

查看:150
本文介绍了PyQt5 - 撤消实现的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I need to implement the undo functionality in this widget, activated with the key combination Ctrl + Z. I can draw lines on an image passed in input to the constructor. The idea is therefore to remove the last item from the list of lines (I add a line to this list every time I draw one) and redraw all the other lines when pressing Ctrl + Z. How can I implement this refresh? Is there a more effective way to do such a thing?

代码:

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_())
    

推荐答案

当要实现撤销支持时,undoable"对象必须能够恢复其先前的状态.对于基于光栅的图像来说,这显然是不可能的,绘画"是基于光栅的图像.被认为具有破坏性:一旦像素颜色发生变化,就无法知道其先前的状态.

When an undo support is going to be implemented, the "undoable" object must be able to restore its previous state. This is obviously not possible for raster based images, for which the "painting" is considered destructive: once a pixel color is altered, there's no way to know its previous state.

一种可能是存储之前的光栅状态,但这种方法当然不建议:如果你总是存储完整的图像,你将面临使用太多内存的风险,并且实现一个只存储图像部分的系统已修改的选项肯定不适合您的情况.

A possibility is to store the previous raster state, but this approach is certainly not suggested: if you always store the full image, you'll risk using too much memory, and implementing a system that only stores the portions of the image that has been modified is certainly not an option for your situation.

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

When dealing with vector graphics, the easiest way is to store the changes as painting "routines" and only save the image when actually needed, so that the modifications are only painted with the widget's paintEvent (obviously you need to modify the updateImage function to actually store the image).
This is usually much faster and allows to remove painting functions arbitrarily.

在下面的示例中,我使用的是您已经创建的 self.lines,但做了一些修改以使事情更简单、更清晰.

In the example below, I'm using the self.lines which you already created, but with some modifications to makes things simpler and more clear.

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 键是一个 修饰符因此在寻找键盘组合时不能与标准键组合,因此您需要检查 event.modifiers() 代替.
这显然是一个非常基本的实现,你可以添加一个重做"仅通过存储当前命令"的索引来支持.

Some notes about the modifications.
There's absolutely no need for NumPy for what you're doing. As you can see you can work out everything you need just with Qt's classes and functions; most importantly, in this case, I'm using a QLineF, which is an abstract representation of a floating point precision vector between two points (the distance between two points can be obtained with QLineF(p1, p2).length()). While this is obviously a little slower than python's math or numpy's function, using a QLine in such situations is certainly better, for the following reasons: you'll need a line anyway, you don't need a 30-40mb python module to compute a Pythagorean distance, it's a single object representing a single object, it keeps code simpler.
Key events cannot be used with binary operators, as they are integers, not binary flags: in fact, your code would call undo even when pressing Z only or with other modifiers; the Ctrl key is a modifier and as such cannot be combined with standard keys when looking for keyboard combination, so you need to check event.modifiers() instead.
This is obviously a very basic implementation, you can add a "redo" support just by storing the index of the current "command".

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

Finally, for more complex user cases, there's the QUndo framework, which is a bit complex than what you're probably going to need, but it's still important to know about it and understand when it's actually needed.

这篇关于PyQt5 - 撤消实现的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆