【问题标题】:How to undo edit of QStandardItem in PySide/PyQt?如何撤消 PySide/PyQt 中 QStandardItem 的编辑?
【发布时间】:2015-06-14 04:13:39
【问题描述】:

使用identical question asked about QListWidgets 作为指导,我正在尝试创建一个QStandardItemModel,我可以在其中撤消对项目的编辑。

从下面的SSCCE 中可以看出,我几乎完全复制了这个示例,但做了一些小的调整,因为currentItemChanged 不适用于QStandardItemModel。为了解决这个问题,我使用clicked 信号来修复项目的先前文本。

奇怪的是,撤消堆栈中显示了正确的描述,但是当我单击undo 按钮时,它实际上并没有撤消任何操作。

请注意,目前的问题表面上与this question 相同。在其他版本中被接受的答案与其说是答案,不如说是暗示。这是我试图在这里实现的一个提示,但它还没有工作。由于这个问题更加具体和详细,因此不应视为重复,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()

【问题讨论】:

  • 我并不想对此感到太烦,但是您链接到的 SSCCE 的定义明确指出示例代码应该是自包含。告诉人们去另一个问题,复制并重命名另一个类,并将它包含在这个问题的代码中,并不能完全使其独立!
  • 其实我现在更困惑了。 CommandItemEdit 类的代码不包含在另一个问题中。有一个CommandEdit 类的代码,但它需要不同数量的实例化参数。 您实际使用的是什么代码?
  • @three_pineapples 哎呀,我打算在发帖前把它放回去,但不知何故被忽视了。修好了。

标签: qt pyqt pyqt4 pyside


【解决方案1】:

这个问题似乎是因为blockSignals() 阻止了树视图被告知重新绘制。我认为这是因为当模型中的数据被修改时,模型向树视图发出了一个信号,当你调用model.blockSignals(True) 时,这显然被阻塞了。如果您在单击撤消/重做后手动调整窗口大小(显然只有在需要撤消/重做时才有效),您会看到撤消/重做实际上已被应用,只是最初并未显示。

为了解决这个问题,我修改了代码,以便我们断开相关信号并重新连接它,而不是阻塞信号。这允许模型和树视图在撤消/重做过程中继续正确通信。

看下面的代码

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

其他信息

我发现如果您在解除信号阻塞后显式调用self.model.layoutChanged.emit(),您可以使用CommandItemEdit 的原始实现。这会强制树视图更新,而不会导致调用UndoableTree.itemChanged() 插槽。

注意,树视图连接到模型信号,而树视图又连接到UndoableTree.itemChanged() 插槽。

我也尝试发出dataChanged() 信号,但这最终会调用仍然连接的UndoableTree.itemChanged() 插槽,从而导致无限递归。我认为这是信号是调用model.blockSignals() 的目标,所以不要显式调用它是有意义的!

所以最后,虽然这些附加方法中的一种确实有效,但我仍然会选择明确断开信号的第一个答案。这仅仅是因为我认为最好让模型和树视图之间的通信保持完整,而不是在手动触发您仍然想要的信号时限制一些通信。后一种方法可能会产生意想不到的副作用,并且调试起来很麻烦。

【讨论】:

  • 1.很好地发现模型实际上已被修改,如您通过调整大小强制重绘时所示。 2. 我做 PySide 已经有一段时间了,从来不知道有一个“断开”方法,所以在这里学习一些新东西。 3. 你选择这种技术,而不是在 undo/redo 方法中调用 repaint(或 update)的幼稚方法有什么原因吗? (请注意,我只是尝试通过向旧代码添加更新或重绘来更新重做/撤消中的视图,但它什么也没做......我不明白为什么)。
  • 我之所以选择这种技术是因为repaint()update() 不起作用!我不知道为什么,但我从来没有成功让这些方法用QWidget 做我想做的事。我还用我刚刚发现的更多信息更新了答案。
  • 您添加的更新非常有帮助,尤其是尝试dataChanged(),这似乎是一个自然的解决方案。奇怪,这么简单的撤消操作竟然这么复杂!
  • 我现在正在尝试使用复选框来实现,因此检查操作和文本编辑都可以撤消。将成为一个单独的问题,尽管在某种程度上已经有:stackoverflow.com/questions/29640408/…
【解决方案2】:

在一个密切相关的问题上致 quote another answer

clicked 信号似乎是完全错误的跟踪方式 变化。您将如何处理通过键盘进行的更改? 那么以编程方式进行的更改呢?对于撤消 堆栈要正常工作,必须记录每个更改,并在 与它的订单完全相同。

同一篇文章继续建议创建一个自定义信号,当数据实际更改时发出旧/新数据。最终,我使用了我从 SO 中无耻窃取的三个想法。首先,three_pineapples' insight 需要 disconnect 来避免无限递归。其次,将QStandardItemModel 子类化以定义一个新的itemDataChanged 信号,该信号将以前的和新的数据都发送到一个槽。第三,子类 QStandardItem 并让它在数据更改时发出此信号:这是在 setData() 的重新实现中处理的。

这里是完整的代码:

# -*- coding: utf-8 -*-

from PySide import QtGui, QtCore
import sys

class CommandTextEdit(QtGui.QUndoCommand):
    def __init__(self, tree, item, oldText, newText, description):
        QtGui.QUndoCommand.__init__(self, description)
        self.item = item
        self.tree = tree
        self.oldText = oldText
        self.newText = newText

    def redo(self):      
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot) 
        self.item.setText(self.newText)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 

    def undo(self):
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot) 
        self.item.setText(self.oldText)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 


class CommandCheckStateChange(QtGui.QUndoCommand):
    def __init__(self, tree, item, oldCheckState, newCheckState, description):
        QtGui.QUndoCommand.__init__(self, description)
        self.item = item
        self.tree = tree
        self.oldCheckState = QtCore.Qt.Unchecked if oldCheckState == 0 else QtCore.Qt.Checked
        self.newCheckState = QtCore.Qt.Checked if oldCheckState == 0 else QtCore.Qt.Unchecked

    def redo(self): #disoconnect to avoid recursive loop b/w signal-slot
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot) 
        self.item.setCheckState(self.newCheckState)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 

    def undo(self):
        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot)
        self.item.setCheckState(self.oldCheckState)
        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) 


class StandardItemModel(QtGui.QStandardItemModel):
    itemDataChanged = QtCore.Signal(object, object, object, object)


class StandardItem(QtGui.QStandardItem):
    def setData(self, newValue, role=QtCore.Qt.UserRole + 1):
        if role == QtCore.Qt.EditRole:
            oldValue = self.data(role)
            QtGui.QStandardItem.setData(self, newValue, role)
            model = self.model()
            #only emit signal if newvalue is different from old
            if model is not None and oldValue != newValue:
                model.itemDataChanged.emit(self, oldValue, newValue, role)
            return True
        if role == QtCore.Qt.CheckStateRole:
            oldValue = self.data(role)
            QtGui.QStandardItem.setData(self, newValue, role)
            model = self.model()
            if model is not None and oldValue != newValue:
                model.itemDataChanged.emit(self, oldValue, newValue, role)
            return True
        QtGui.QStandardItem.setData(self, newValue, role)


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

    def makeConnections(self):
        self.model.itemDataChanged.connect(self.itemDataChangedSlot)
        self.quitButton.clicked.connect(self.close)
        self.undoButton.clicked.connect(self.undoStack.undo)
        self.redoButton.clicked.connect(self.undoStack.redo)

    def itemDataChangedSlot(self, item, oldValue, newValue, role):
        if role == QtCore.Qt.EditRole:
            command = CommandTextEdit(self, item, oldValue, newValue,
                "Text changed from '{0}' to '{1}'".format(oldValue, newValue))
            self.undoStack.push(command)
            return True
        if role == QtCore.Qt.CheckStateRole:
            command = CommandCheckStateChange(self, item, oldValue, newValue, 
                "CheckState changed from '{0}' to '{1}'".format(oldValue, newValue))
            self.undoStack.push(command)
            return True

    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 = StandardItemModel()
        model.setHorizontalHeaderLabels(['Titles', 'Summaries'])
        rootItem = model.invisibleRootItem()
        item0 = [StandardItem('Title0'), StandardItem('Summary0')]
        item00 = [StandardItem('Title00'), StandardItem('Summary00')]
        item01 = [StandardItem('Title01'), StandardItem('Summary01')]
        item0[0].setCheckable(True)
        item00[0].setCheckable(True)
        item01[0].setCheckable(True)
        rootItem.appendRow(item0)
        item0[0].appendRow(item00)
        item0[0].appendRow(item01)
        return model


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

if __name__ == "__main__":
    main()

总的来说,这似乎比使用clicked 更好。

【讨论】:

    猜你喜欢
    • 2015-05-11
    • 1970-01-01
    • 2015-12-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-29
    • 1970-01-01
    相关资源
    最近更新 更多