【问题标题】:How to undo an edit of a QListWidgetItem in PySide/PyQt?如何撤消 PySide/PyQt 中 QListWidgetItem 的编辑?
【发布时间】:2015-05-11 08:24:49
【问题描述】:

短版

如何在 PySide/PyQt 中为 QListWidgetItems 上的编辑实现撤消功能?

Qt 教程中的提示?

下面为 Qt 用户 (c++) 编写的教程可能有答案,但我不是 c++ 人,所以有点迷茫:Using Undo/Redo with Item Views

加长版

我正在使用 QListWidget 来了解 PyQt 的 Undo Framework(在 an article 的帮助下)。当我自己执行一个命令(比如从列表中删除一个项目)时,我对撤消/重做很好。

我还想让小部件中的QListWidgetItems 可编辑。这很简单:只需将ItemIsEditable 标志添加到每个项目。问题是,我怎样才能将这些编辑推送到撤消堆栈中,这样我才能撤消/重做它们?

下面是一个简单的工作示例,它显示了一个列表,可让您删除项目以及撤消/重做此类删除。应用程序同时显示列表和撤消堆栈。需要做什么才能对该堆栈进行编辑?

简单的工作示例

from PySide import QtGui, QtCore

class TodoList(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.initUI()
        self.show()

    def initUI(self):
        self.todoList = self.makeTodoList()
        self.undoStack = QtGui.QUndoStack(self)
        undoView = QtGui.QUndoView(self.undoStack)
        buttonLayout = self.buttonSetup()
        mainLayout = QtGui.QHBoxLayout(self)
        mainLayout.addWidget(undoView)
        mainLayout.addWidget(self.todoList)
        mainLayout.addLayout(buttonLayout)
        self.setLayout(mainLayout)
        self.makeConnections()

    def buttonSetup(self):
        #Make buttons 
        self.deleteButton = QtGui.QPushButton("Delete")
        self.undoButton = QtGui.QPushButton("Undo")
        self.redoButton = QtGui.QPushButton("Redo")
        self.quitButton = QtGui.QPushButton("Quit")
        #Lay them out
        buttonLayout = QtGui.QVBoxLayout()
        buttonLayout.addWidget(self.deleteButton)
        buttonLayout.addStretch()
        buttonLayout.addWidget(self.undoButton)
        buttonLayout.addWidget(self.redoButton)
        buttonLayout.addStretch()
        buttonLayout.addWidget(self.quitButton)
        return buttonLayout

    def makeConnections(self):
        self.deleteButton.clicked.connect(self.deleteItem)
        self.quitButton.clicked.connect(self.close)
        self.undoButton.clicked.connect(self.undoStack.undo)
        self.redoButton.clicked.connect(self.undoStack.redo)

    def deleteItem(self):
        rowSelected=self.todoList.currentRow()
        rowItem = self.todoList.item(rowSelected)
        if rowItem is None:
            return
        command = CommandDelete(self.todoList, rowItem, rowSelected,
                                "Delete item '{0}'".format(rowItem.text()))
        self.undoStack.push(command)

    def makeTodoList(self):
        todoList = QtGui.QListWidget()
        allTasks = ('Fix door', 'Make dinner', 'Read', 
                    'Program in PySide', 'Be nice to everyone')
        for task in allTasks:
            todoItem=QtGui.QListWidgetItem(task)
            todoList.addItem(todoItem)
            todoItem.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
        return todoList


class CommandDelete(QtGui.QUndoCommand):
    def __init__(self, listWidget, item, row, description):
        super(CommandDelete, self).__init__(description)
        self.listWidget = listWidget
        self.string = item.text()
        self.row = row

    def redo(self):
        self.listWidget.takeItem(self.row)

    def undo(self):
        addItem = QtGui.QListWidgetItem(self.string)
        addItem.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
        self.listWidget.insertItem(self.row, addItem)

if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    myList=TodoList()
    sys.exit(app.exec_())

请注意,我在 QtCentre 发布了一个 earlier version of this question

【问题讨论】:

    标签: python pyqt pyside qlistwidget


    【解决方案1】:

    我会这样做:

    创建一个自定义QItemDelegate 并使用这两个信号:

    • editorEvent
    • closeEditor

    开启editorEvent:保存当前状态

    开启closeEditor:获取新状态并创建一个QUndoCommand,为Redo 设置新状态,为Undo 设置旧状态。

    【讨论】:

    • 您的提示是有希望的,尽管 editorEvent 可能不是保存某些内容的最有效时间,因为它经常被调用(即使单击鼠标也会被多次调用)。也许通过 createEditor() 和 setModelData() 以某种方式合并您的策略会更有效、更简单,每次编辑只调用一次?
    • editorEvent 在编辑开始和鼠标移动时被调用。您可以忽略鼠标事件而只使用编辑开始事件。
    【解决方案2】:

    你提到的那个教程真的不是很有帮助。视图的undo-redo实现确实有很多方法,我们只需要选择最简单的一种。如果您处理小列表,最简单的方法是保存每次更改的所有数据,并在每次撤消或重做操作时从头开始恢复完整列表。

    如果您仍然想要原子更改列表,您可以使用QListWidget::itemChanged 信号跟踪用户所做的编辑。这样做有两个问题:

    • 列表中的任何其他项目更改也会触发此信号,因此您需要将任何更改项目的代码包装到 QObject::blockSignals 调用中以阻止不需要的信号。
    • 无法获取以前的文本,只能获取新文本。解决方案是将所有列表数据保存到变量,在更改时使用和更新它,或者在编辑之前保存已编辑项目的文本。 QListWidget 对其内部编辑器状态非常谨慎,因此我决定使用QListWidget::currentItemChanged,假设用户在不先创建当前状态的情况下找不到编辑项目的方法。

    所以这是使它工作的更改(除了在两个地方添加 ItemIsEditable 标志):

    def __init__(self):
        #...
        self.todoList.itemChanged.connect(self.itemChanged)
        self.todoList.currentItemChanged.connect(self.currentItemChanged)
        self.textBeforeEdit = ""
    
    def itemChanged(self, item):
        command = CommandEdit(self.todoList, item, self.todoList.row(item),
            self.textBeforeEdit, 
            "Rename item '{0}' to '{1}'".format(self.textBeforeEdit, item.text()))
        self.undoStack.push(command)
    
    def currentItemChanged(self, item):
        self.textBeforeEdit = item.text()
    

    还有新的变更类:

    class CommandEdit(QtGui.QUndoCommand):
        def __init__(self, listWidget, item, row, textBeforeEdit, description):
            super(CommandEdit, self).__init__(description)
            self.listWidget = listWidget
            self.textBeforeEdit = textBeforeEdit
            self.textAfterEdit = item.text()
            self.row = row
    
        def redo(self):
            self.listWidget.blockSignals(True)
            self.listWidget.item(self.row).setText(self.textAfterEdit)
            self.listWidget.blockSignals(False)
    
        def undo(self):
            self.listWidget.blockSignals(True)
            self.listWidget.item(self.row).setText(self.textBeforeEdit)
            self.listWidget.blockSignals(False)
    

    【讨论】:

    • 多么有启发性的答案!我们甚至可以使用currentTextChanged 而不是currentItemChanged,但坦率地说,我更喜欢前者(你使用的),因为它对于复选框状态等具有更高的可扩展性。一个问题:currentItemChanged 文档说它同时发出current 项目和previous 项目。但它似乎只是将新选择的(当前)项目发送到它的插槽,即使在 Qt 文档中它说 void QListWidget.currentItemChanged(currentItem, previousItem) (qt-project.org/doc/qt-4.8-snapshot/…)。
    • Qt 允许槽的参数少于信号。如果您将第二个参数添加到插槽,您将获得previous 项目。不过这里没用。
    • 奇怪的是,我试过了,第二个参数被读取为类型None。幸运的是,这无关紧要,但行为(无论如何在 PySide 中)似乎与文档不一致。也许是一个单独的问题,或者是一个专门的 Qt 论坛的问题?
    • 我发现了问题:在我第一次点击时,上一个项目(正确)返回为None!愚蠢的错误。一旦我点击另一个项目,我们就会得到信号中预期的当前和上一个项目。
    【解决方案3】:

    每次验证并接受项目的新文本时,将其保存为列表项目数据。准半伪代码:

    OnItemEdited(Item* item)
    {
        int dataRole{ 32 }; //or greater (see ItemDataRole documentation)
    
        if (Validate(item->text()) {
    
            item->setData(dataRole, item->text());
    
        } else { //Restore previous value
    
            item->setText(item->data(dataRole).toString());
        }
    }
    

    如果它看起来太像 C++,我很抱歉。

    【讨论】:

      猜你喜欢
      • 2015-06-14
      • 1970-01-01
      • 1970-01-01
      • 2015-12-22
      • 1970-01-01
      • 1970-01-01
      • 2011-12-17
      • 2010-10-29
      • 1970-01-01
      相关资源
      最近更新 更多