如何撤消对 PySide/PyQt 中 QListWidgetItem 的编辑?
How to undo an edit of a QListWidgetItem in PySide/PyQt?
短版
如何为 PySide/PyQt 中对 QListWidgetItems
所做的编辑实现撤消功能?
来自 Qt 教程的提示?
下面为 Qt 用户(c++)编写的教程可能有答案,但我不是 c++ 人,所以有点迷茫:Using Undo/Redo with Item Views
更长的版本
我正在使用 QListWidget
来了解 PyQt 的 Undo Framework (with the help of an article 主题)。当我自己执行一个命令(比如从列表中删除一个项目)时,我可以接受 undo/redo。
我还想将小部件中的 QListWidgetItems
设置为可编辑。这很简单:只需将 ItemIsEditable
标志添加到每个项目。问题是,我如何才能将此类编辑推送到撤消堆栈,然后我才能undo/redo它们?
下面是一个简单的工作示例,它显示了一个列表,允许您删除项目,以及 undo/redo 此类删除。该应用程序显示列表和撤消堆栈。需要做什么才能对该堆栈进行编辑?
简单的工作示例
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 上发布了一个电子arlier version of this question。
我会这样做:
创建自定义 QItemDelegate
并使用这两个信号:
editorEvent
closeEditor
开editorEvent
:保存当前状态
On closeEditor
:获取新状态并创建一个 QUndoCommand
,为 Redo
设置新状态,为 Undo
设置旧状态。
你提到的那个教程真的不是很有帮助。视图的undo-redo实现方式确实有很多种,我们只需要选择最简单的一种即可。如果您处理的是小列表,最简单的方法是在每次更改时保存所有数据,并在每次撤消或重做操作时从头开始恢复完整列表。
如果您仍然想要原子更改列表,您可以使用 QListWidget::itemChanged
信号跟踪用户所做的编辑。这有两个问题:
- 列表中的任何其他项目更改也会触发此信号,因此您需要将更改项目的任何代码包装到
QObject::blockSignals
调用中以阻止不需要的信号。
- 无法获取之前的文字,只能获取新的文字。解决方案是将所有列表数据保存到变量中,在更改时使用和更新它,或者在编辑之前保存已编辑项目的文本。 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:
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)
每次验证并接受项目的新文本时,将其保存为列表项数据。 Quasi-semi-pseudo-code:
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++,我很抱歉。
短版
如何为 PySide/PyQt 中对 QListWidgetItems
所做的编辑实现撤消功能?
来自 Qt 教程的提示?
下面为 Qt 用户(c++)编写的教程可能有答案,但我不是 c++ 人,所以有点迷茫:Using Undo/Redo with Item Views
更长的版本
我正在使用 QListWidget
来了解 PyQt 的 Undo Framework (with the help of an article 主题)。当我自己执行一个命令(比如从列表中删除一个项目)时,我可以接受 undo/redo。
我还想将小部件中的 QListWidgetItems
设置为可编辑。这很简单:只需将 ItemIsEditable
标志添加到每个项目。问题是,我如何才能将此类编辑推送到撤消堆栈,然后我才能undo/redo它们?
下面是一个简单的工作示例,它显示了一个列表,允许您删除项目,以及 undo/redo 此类删除。该应用程序显示列表和撤消堆栈。需要做什么才能对该堆栈进行编辑?
简单的工作示例
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 上发布了一个电子arlier version of this question。
我会这样做:
创建自定义 QItemDelegate
并使用这两个信号:
editorEvent
closeEditor
开editorEvent
:保存当前状态
On closeEditor
:获取新状态并创建一个 QUndoCommand
,为 Redo
设置新状态,为 Undo
设置旧状态。
你提到的那个教程真的不是很有帮助。视图的undo-redo实现方式确实有很多种,我们只需要选择最简单的一种即可。如果您处理的是小列表,最简单的方法是在每次更改时保存所有数据,并在每次撤消或重做操作时从头开始恢复完整列表。
如果您仍然想要原子更改列表,您可以使用 QListWidget::itemChanged
信号跟踪用户所做的编辑。这有两个问题:
- 列表中的任何其他项目更改也会触发此信号,因此您需要将更改项目的任何代码包装到
QObject::blockSignals
调用中以阻止不需要的信号。 - 无法获取之前的文字,只能获取新的文字。解决方案是将所有列表数据保存到变量中,在更改时使用和更新它,或者在编辑之前保存已编辑项目的文本。 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:
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)
每次验证并接受项目的新文本时,将其保存为列表项数据。 Quasi-semi-pseudo-code:
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++,我很抱歉。