Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

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

qt - How to undo edit of QStandardItem in PySide/PyQt?

Using the identical question asked about QListWidgets as a guide, I am trying to make a QStandardItemModel in which I can undo the edit of an item.

As can be seen in the SSCCE below, I'm pretty much copying the example exactly, but with some minor tweaking because currentItemChanged isn't available for QStandardItemModel. To get around that, I'm using the clicked signal instead to fix the previous text for an item.

Strangely, the correct description shows up in the undostack, but when I click the undo button it doesn't actually undo anything.

Note the present question is superficially the same as this question. The answer that was accepted at that other version is less an answer, more a hint. It's a hint I am trying to implement here, but it is not working yet. Since this question is more specific and detailed, it shouldn't count as a duplicate, IMO.

SSCCE

from PySide import QtGui, QtCore
import sys

class CommandItemEdit(QtGui.QUndoCommand):
    def __init__(self, model, item, textBeforeEdit, description = "Item edited"):
        QtGui.QUndoCommand.__init__(self, description)
        self.model = model
        self.item = item
        self.textBeforeEdit = textBeforeEdit
        self.textAfterEdit = item.text()

    def redo(self):
        self.model.blockSignals(True)  
        self.item.setText(self.textAfterEdit)
        self.model.blockSignals(False)

    def undo(self):
        self.model.blockSignals(True)
        self.item.setText(self.textBeforeEdit)
        self.model.blockSignals(False)     


class UndoableTree(QtGui.QWidget):
    def __init__(self, parent = None):
        QtGui.QWidget.__init__(self, parent = None)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.view = QtGui.QTreeView()
        self.model = self.createModel()
        self.view.setModel(self.model)
        self.view.expandAll()
        self.undoStack = QtGui.QUndoStack(self)
        undoView = QtGui.QUndoView(self.undoStack)
        buttonLayout = self.buttonSetup()
        mainLayout = QtGui.QHBoxLayout(self)
        mainLayout.addWidget(undoView)
        mainLayout.addWidget(self.view)
        mainLayout.addLayout(buttonLayout)
        self.setLayout(mainLayout)
        self.makeConnections()
        #For undo/redo editing
        self.textBeforeEdit = ""

    def makeConnections(self):
        self.view.clicked.connect(self.itemClicked)
        self.model.itemChanged.connect(self.itemChanged)
        self.quitButton.clicked.connect(self.close)
        self.undoButton.clicked.connect(self.undoStack.undo)
        self.redoButton.clicked.connect(self.undoStack.redo)

    def itemClicked(self, index):
        item = self.model.itemFromIndex(index)
        self.textBeforeEdit = item.text()  

    def itemChanged(self, item):
        command = CommandItemEdit(self.model, item, self.textBeforeEdit, 
            "Renamed '{0}' to '{1}'".format(self.textBeforeEdit, item.text()))
        self.undoStack.push(command)


    def buttonSetup(self):
        self.undoButton = QtGui.QPushButton("Undo")
        self.redoButton = QtGui.QPushButton("Redo")
        self.quitButton = QtGui.QPushButton("Quit")
        buttonLayout = QtGui.QVBoxLayout()
        buttonLayout.addStretch()
        buttonLayout.addWidget(self.undoButton)
        buttonLayout.addWidget(self.redoButton)
        buttonLayout.addStretch()
        buttonLayout.addWidget(self.quitButton)
        return buttonLayout

    def createModel(self):
        model = QtGui.QStandardItemModel()
        model.setHorizontalHeaderLabels(['Titles', 'Summaries'])
        rootItem = model.invisibleRootItem()
        #First top-level row and children 
        item0 = [QtGui.QStandardItem('Title0'), QtGui.QStandardItem('Summary0')]
        item00 = [QtGui.QStandardItem('Title00'), QtGui.QStandardItem('Summary00')]
        item01 = [QtGui.QStandardItem('Title01'), QtGui.QStandardItem('Summary01')]
        rootItem.appendRow(item0)
        item0[0].appendRow(item00)
        item0[0].appendRow(item01)
        #Second top-level item and its children
        item1 = [QtGui.QStandardItem('Title1'), QtGui.QStandardItem('Summary1')]
        item10 = [QtGui.QStandardItem('Title10'), QtGui.QStandardItem('Summary10')]
        item11 = [QtGui.QStandardItem('Title11'), QtGui.QStandardItem('Summary11')]
        rootItem.appendRow(item1)
        item1[0].appendRow(item10)
        item1[0].appendRow(item11)

        return model


def main():
    app = QtGui.QApplication(sys.argv)
    newTree = UndoableTree()
    newTree.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
See Question&Answers more detail:os

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

1 Answer

0 votes
by (71.8m points)

The issue appears to arise because blockSignals() is preventing the treeview from being told to repaint. I think this is because the model emits a signal to the treeview when data in the model is modified, which is obviously being blocked when you call model.blockSignals(True). If you manually resize the window after clicking undo/redo (obviously only works if there is something to undo/redo), you see that the undo/redo has actually been applied, it just didn't initially show it.

To work around this, I've modified the code so that instead of blocking signals, we disconnect the relevant signal and reconnect it. This allows the model and treeview to continue to communicate correctly while the undo/redo is in progress.

See the below code

from PySide import QtGui, QtCore
import sys

class CommandItemEdit(QtGui.QUndoCommand):
    def __init__(self, connectSignals, disconnectSignals, model, item, textBeforeEdit, description = "Item edited"):
        QtGui.QUndoCommand.__init__(self, description)
        self.model = model
        self.item = item
        self.textBeforeEdit = textBeforeEdit
        self.textAfterEdit = item.text()
        self.connectSignals = connectSignals
        self.disconnectSignals = disconnectSignals

    def redo(self):
        self.disconnectSignals()
        self.item.setText(self.textAfterEdit)
        self.connectSignals()

    def undo(self):
        self.disconnectSignals()
        self.item.setText(self.textBeforeEdit)
        self.connectSignals()


class UndoableTree(QtGui.QWidget):
    def __init__(self, parent = None):
        QtGui.QWidget.__init__(self, parent = None)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.view = QtGui.QTreeView()
        self.model = self.createModel()
        self.view.setModel(self.model)
        self.view.expandAll()
        self.undoStack = QtGui.QUndoStack(self)
        undoView = QtGui.QUndoView(self.undoStack)
        buttonLayout = self.buttonSetup()
        mainLayout = QtGui.QHBoxLayout(self)
        mainLayout.addWidget(undoView)
        mainLayout.addWidget(self.view)
        mainLayout.addLayout(buttonLayout)
        self.setLayout(mainLayout)
        self.makeConnections()
        #For undo/redo editing
        self.textBeforeEdit = ""

    def makeConnections(self):
        self.view.clicked.connect(self.itemClicked)
        self.model.itemChanged.connect(self.itemChanged)
        self.quitButton.clicked.connect(self.close)
        self.undoButton.clicked.connect(self.undoStack.undo)
        self.redoButton.clicked.connect(self.undoStack.redo)

    def disconnectSignal(self):    
        self.model.itemChanged.disconnect(self.itemChanged)

    def connectSignal(self):
        self.model.itemChanged.connect(self.itemChanged)

    def itemClicked(self, index):
        item = self.model.itemFromIndex(index)
        self.textBeforeEdit = item.text()  

    def itemChanged(self, item):
        command = CommandItemEdit(self.connectSignal, self.disconnectSignal, self.model, item, self.textBeforeEdit, 
            "Renamed '{0}' to '{1}'".format(self.textBeforeEdit, item.text()))
        self.undoStack.push(command)


    def buttonSetup(self):
        self.undoButton = QtGui.QPushButton("Undo")
        self.redoButton = QtGui.QPushButton("Redo")
        self.quitButton = QtGui.QPushButton("Quit")
        buttonLayout = QtGui.QVBoxLayout()
        buttonLayout.addStretch()
        buttonLayout.addWidget(self.undoButton)
        buttonLayout.addWidget(self.redoButton)
        buttonLayout.addStretch()
        buttonLayout.addWidget(self.quitButton)
        return buttonLayout

    def createModel(self):
        model = QtGui.QStandardItemModel()
        model.setHorizontalHeaderLabels(['Titles', 'Summaries'])
        rootItem = model.invisibleRootItem()
        #First top-level row and children 
        item0 = [QtGui.QStandardItem('Title0'), QtGui.QStandardItem('Summary0')]
        item00 = [QtGui.QStandardItem('Title00'), QtGui.QStandardItem('Summary00')]
        item01 = [QtGui.QStandardItem('Title01'), QtGui.QStandardItem('Summary01')]
        rootItem.appendRow(item0)
        item0[0].appendRow(item00)
        item0[0].appendRow(item01)
        #Second top-level item and its children
        item1 = [QtGui.QStandardItem('Title1'), QtGui.QStandardItem('Summary1')]
        item10 = [QtGui.QStandardItem('Title10'), QtGui.QStandardItem('Summary10')]
        item11 = [QtGui.QStandardItem('Title11'), QtGui.QStandardItem('Summary11')]
        rootItem.appendRow(item1)
        item1[0].appendRow(item10)
        item1[0].appendRow(item11)

        return model


def main():
    app = QtGui.QApplication(sys.argv)
    newTree = UndoableTree()
    newTree.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

Additional Information

I've discovered that you could use you original implementation of CommandItemEdit if you explicitly call self.model.layoutChanged.emit() after unblocking the signals. This forces the treeview to update without resulting in a call to the UndoableTree.itemChanged() slot.

Note, the treeview is connected to the model signals, while the treeview is in turn connected to the UndoableTree.itemChanged() slot.

I also tried emitting the dataChanged() signal, but this ends up ultimately calling the still connected UndoableTree.itemChanged() slot, which results in infinite recursion. I think this is signal is the target of the call to model.blockSignals(), so it makes sense not to call it explicitly!

So in the end, while one of these additional methods does work, I would still go with my first answer of explicitly disconnecting the signal. This is simply because I think it is better to leave communication between the model and treeview completely intact, rather than restrict some communication while manually triggering the signals you still want. The latter route is likely to have unintended side-effects and be a pain to debug.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

2.1m questions

2.1m answers

60 comments

57.0k users

...