如何在PySide/PyQt中撤消对QStandardItem的编辑?
以作为指导,我尝试创建一个如何在PySide/PyQt中撤消对QStandardItem的编辑?,qt,pyqt,pyqt4,pyside,Qt,Pyqt,Pyqt4,Pyside,以作为指导,我尝试创建一个QStandardItemModel,在其中可以撤消对项目的编辑 从下面可以看出,我几乎完全复制了这个示例,但是做了一些小的调整,因为currentItemChanged不适用于QStandardItemModel。为了解决这个问题,我使用了单击的信号来修复项目前面的文本 奇怪的是,正确的描述显示在undostack中,但当我单击undo按钮时,它实际上并没有撤消任何内容 请注意,目前的问题表面上与之相同。另一个版本接受的答案与其说是答案,不如说是暗示。这是我试图在这里
QStandardItemModel
,在其中可以撤消对项目的编辑
从下面可以看出,我几乎完全复制了这个示例,但是做了一些小的调整,因为currentItemChanged
不适用于QStandardItemModel
。为了解决这个问题,我使用了单击的
信号来修复项目前面的文本
奇怪的是,正确的描述显示在undostack中,但当我单击undo
按钮时,它实际上并没有撤消任何内容
请注意,目前的问题表面上与之相同。另一个版本接受的答案与其说是答案,不如说是暗示。这是我试图在这里实现的一个提示,但它还没有起作用。国际海事组织称,由于这个问题更为具体和详细,因此不应将其视为重复问题
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()
出现此问题的原因似乎是
blockSignals()
阻止treeview被告知重新绘制。我认为这是因为当模型中的数据被修改时,模型会向树视图发出一个信号,当您调用model.blockSignals(True)
时,它显然被阻止了。如果在单击“撤消/重做”后手动调整窗口大小(显然,只有在有要撤消/重做的内容时才有效),您会看到实际上已经应用了“撤消/重做”,只是最初没有显示它
为了解决这个问题,我修改了代码,这样我们就不用阻塞信号,而是断开相关信号并重新连接。这允许模型和treeview在撤消/重做过程中继续正确通信
请参阅下面的代码
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
的原始实现。这将强制treeview更新,而不会导致调用UndoableTree.itemChanged()
插槽
注意,treeview连接到模型信号,而treeview依次连接到UndoableTree.itemChanged()
插槽
我还尝试发出了dataChanged()
信号,但最终调用了仍然连接的UndoableTree.itemChanged()
插槽,这导致了无限递归。我认为调用model.blockSignals()
的目标是这个is signal,所以不显式调用它是有意义的
因此,最后,虽然这些附加方法中的一种确实有效,但我仍然会使用我的第一个答案,即显式断开信号。这仅仅是因为我认为最好保持模型和treeview之间的通信完好无损,而不是在手动触发您仍然需要的信号时限制一些通信。后一种方法可能会产生意外的副作用,并且是一种调试的痛苦。对于一个密切相关的问题:
点击信号似乎是完全错误的跟踪方式
变化。您将如何处理通过键盘进行的更改?
那么以编程方式进行的更改呢?撤销
堆栈要正常工作,必须记录每个更改,并且
与订单完全相同
同一篇文章还建议创建一个自定义信号,当数据发生实际更改时,该信号会发出新旧数据。最终,我使用了三个我无耻地从SO那里偷来的想法。首先,需要断开连接以避免无限递归。其次,子类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()
总的来说,这似乎比使用单击的
要好
我并不想对此感到太烦人,但您链接到的SSCCE的定义明确指出示例代码应该是自包含的。告诉人们转到另一个问题,复制并重命名另一个类,并将其包含在这个问题的代码中,并不能完全使其自包含!实际上我现在更糊涂了。CommandItemEdit
类的代码不包含在其他问题中。有一个CommandEdit
类的代码,但它采用不同数量的实例化参数。你到底在用什么代码?@three_Pinepples噢,我本来想在发帖前把它放回去的,但不知怎么的,我忘了。修好了。很好,我们发现模型实际上已被修改,如通过调整大小强制重画时所示。2.我做PySide已经有一段时间了,从来都不知道有一个“断开连接”的方法,所以这里有一些新的东西需要学习。3.您选择这种技术而不是仅仅在undo/redo方法中调用repaint(或update)的幼稚方法,有什么原因吗?(注意,我只是尝试在重做/撤消中通过向旧代码添加更新或重新绘制来更新视图,但没有任何效果……我不明白为什么)。我选择这种技术是因为repain()
和update()
不起作用!我不知道为什么,但我从未成功地用QWidget
让这些方法实现我想要的功能。我还用刚刚发现的更多信息更新了答案。您添加的更新非常有用,特别是尝试dataChanged()
,这似乎是一个自然的解决方案。奇怪,这么简单的撤销操作竟如此复杂!我现在尝试使用复选框来实现,这样复选操作和文本编辑都可以撤消。将成为一个单独的问题,尽管在某种程度上已经: