如何在PySide/PyQt中撤销QListWidgetItem的编辑?

3

简短版

如何在PySide/PyQt中实现对QListWidgetItems的撤销功能?

Qt教程中的提示?

下面这篇适用于Qt用户(c++)的教程可能有答案,但我不是c++人,所以有些迷惑:Using Undo/Redo with Item Views

详细版

我正在使用QListWidget来学习PyQt的Undo Framework(借助一篇文章)。当我自己实现一个命令时(比如从列表中删除一个项目),我可以很好地处理撤销/重做。

我还想让小部件中的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发布了这个问题的早期版本

(Note: I posted an earlier version of this question on QtCentre.)
3个回答

7
你提到的教程并不是很有帮助。对于视图的撤销-重做实现,确实有很多方法,我们只需要选择最简单的方法。如果你处理的是小列表,最简单的方法是在每次更改时保存所有数据,并在每次撤销或重做操作时从头开始恢复完整列表。
如果你仍然想要原子更改列表,你可以使用“QListWidget::itemChanged”信号来跟踪用户所做的编辑。但这样会有两个问题:
1. 列表中的任何其他项目更改也会触发此信号,因此您需要将更改项目的任何代码包装到“QObject::blockSignals”调用中,以阻止不需要的信号。 2. 没有办法获取先前的文本,你只能得到新的文本。解决方案是将所有列表数据保存到变量中,在更改时使用和更新它,或者在编辑之前保存编辑的项目文本。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 文档说它会发出 currentprevious 项目,但它似乎只将新选择的(当前)项目发送到其槽中,即使在 Qt 文档中它说 void QListWidget.currentItemChanged(currentItem, previousItem) (http://qt-project.org/doc/qt-4.8-snapshot/qlistwidget.html#currentItemChanged)。 - eric
Qt允许槽函数的参数比信号函数少。如果您向槽函数添加第二个参数,则会得到“previous”项。但在这里并不实用。 - Pavel Strakhov
奇怪的是,我尝试了一下,第二个参数只被读为“None”类型。幸运的是,对于这个问题来说这并不重要,但是这种行为(在PySide中)似乎与文档不符。也许需要另外一个问题或者在专门的Qt论坛上提问? - eric
我找出了问题所在:在我的第一个点击时,前一个项目被(正确地)返回为 None!这是个愚蠢的错误。一旦我点击另一个项目,我们就会像信号中预期的那样拥有当前和上一个项目。 - eric

1
我会这样做:
创建一个自定义的 QItemDelegate 并使用这两个信号:
  • editorEvent
  • closeEditor
editorEvent 上:保存当前状态
closeEditor 上:获取新状态并创建一个 QUndoCommand,将新状态设置为 Redo,旧状态设置为 Undo

你的提示很有前途,尽管 editorEvent 可能不是最有效的时间来保存某些内容,因为它被频繁调用(即使只有单击鼠标,它也会被多次调用)。也许更有效率和简单的方法是通过 createEditor() 和 setModelData() 来实现你的策略,因为每次编辑时只会调用一次。 - eric
当编辑开始和鼠标移动时,将调用editorEvent。您可以忽略鼠标事件,只使用编辑开始事件。 - Mailerdaimon

0

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

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++,我很抱歉。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接