Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
422 views
in Technique[技术] by (71.8m points)

python - How to undo an edit of a QListWidgetItem in PySide/PyQt?

Short version

How do you implement undo functionality for edits made on QListWidgetItems in PySide/PyQt?

Hint from a Qt tutorial?

The following tutorial written for Qt users (c++) likely has the answer, but I am not a c++ person, so get a bit lost: Using Undo/Redo with Item Views

Longer version

I am using a QListWidget to learn my way around PyQt's Undo Framework (with the help of an article on the topic). I am fine with undo/redo when I implement a command myself (like deleting an item from the list).

I also want to make the QListWidgetItems in the widget editable. This is easy enough: just add the ItemIsEditable flag to each item. The problem is, how can I push such edits onto the undo stack, so I can then undo/redo them?

Below is a simple working example that shows a list, lets you delete items,and undo/redo such deletions. The application displays both the list and the the undo stack. What needs to be done to get edits onto that stack?

Simple working example

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

Note I posted an earlier version of this question at QtCentre.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

That tutorial you mentioned is really not very helpful. There are indeed many approaches to undo-redo implementation for views, we just need to choose the simplest one. If you deal with small lists, the simpliest way is to save all data on each change and restore full list from scratch on each undo or redo operation.

If you still want atomic changes list, you can track user-made edits with QListWidget::itemChanged signal. There are two problems with that:

  • Any other item change in the list will also trigger this signal, so you need to wrap any code that changes items into QObject::blockSignals calls to block unwanted signals.
  • There is no way to get previous text, you can only get new text. The solution is either save all list data to variable, use and update it on change or save the edited item's text before it's edited. QListWidget is pretty reticent about its internal editor state, so I decided to use QListWidget::currentItemChanged assuming that user won't find a way to edit an item without making is current first.

So this is the changes that will make it work (besides adding ItemIsEditable flag in two places):

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

And the new change class:

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)

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...