如何在 PyQt/PySide/Qt 中针对项目复选框和文本处理单击事件?

3
我有一个QStandardItemModel,其中每个项目都是可勾选的。当我单击项目复选框时,一方面,当我单击其文本时,另一方面,我希望调用不同的插槽。我的最终目标是将文本编辑和复选框状态更改分别放入QUndoStack中。
在我的重新实现的clicked中,我想以不同的方式处理复选框点击和文本点击。到目前为止,在QCheckBox或QStandardItem的文档the documentation中,我找不到区分这些事件的方法。虽然QCheckBox具有可以使用的toggled信号,但我不确定如何特定地侦听文本区域上的点击。
我正在尝试避免手动设置坐标,然后在项目视图的不同区域中侦听点击。
这似乎不像调用 itemChanged 那样简单,因为它只给出了项目的 状态,而不是先前的状态。根据之前的问题,我认为您需要一些方法将先前的状态打包到撤消堆栈中,以便知道要还原什么。这就是我试图使用 clicked 实现的目标,但可能有更好的方法。
这个问题基于这个系列中的前两个问题,我试图弄清楚如何在模型中撤消操作:

1
虽然有些麻烦,但你可以创建一个 QCheckBox 并将该小部件嵌入到 QTreeWidget 中(使用 setItemWidget 方法)。你可以将一个方法连接到 QCheckBox 的 clicked 信号上,将另一个方法连接到 itemChanged 信号上。 - justengel
2个回答

4

根据ekhumoro的建议和代码片段,我构建了一个树形视图 of a QStandardItemModel,该模型在项目更改时发出自定义信号。 代码通过在setData中使用角色来区分文本与复选框的更改(对于文本,请使用Qt.EditRole,对于复选框状态更改,请使用Qt.CheckStateRole):

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

2
clicked 信号似乎完全是追踪更改的错误方式。您如何处理通过键盘进行的更改?那么对于以编程方式进行的更改呢?为了正确地使用撤消堆栈,必须记录每个更改,并且按照它被进行的确切顺序。
如果您正在使用 QStandardItemModel,则 itemChanged 信号几乎正是您想要的。但是,它的主要问题在于,虽然它发送了更改的 item,但它没有提供有关 what 更改的任何信息。因此,看起来您需要进行一些子类化并发出自定义信号来实现。
class StandardItemModel(QtGui.QStandardItemModel):
    itemDataChanged = QtCore.Signal(object, object, object)

class StandardItem(QtGui.QStandardItem):
    def setData(self, newValue, role=QtCore.Qt.UserRole + 1):
        oldValue = self.data(role)
        QtGui.QStandardItem.setData(self, newValue, role)
        model = self.model()
        if model is not None:
            model.itemDataChanged.emit(oldValue, newvValue, role)

请注意,该信号仅在将项目添加到模型后进行更改时发出。

优秀的批评,但我希望你能在这里介入并纠正我:http://stackoverflow.com/questions/29527610/how-to-undo-edit-of-qstandarditem-in-pyside-pyqt?lq=1。今晚下班后会看一下,并尝试看看是否可以在之前关于将文本编辑推送到undostack的帖子中使用您的想法... - eric
也许可以采用条件语句的方式:首先检查复选框状态是否发生了变化,如果没有,则更新文本。即使如此,我仍然需要一种方法来存储先前存在的文本,并且我接受除“clicked”之外的其他信号,我承认“clicked”可能不是最好的选择。也许只需选择项目而非点击项即可。 - eric
1
@neuronet。我不认为有问题。我回答中的示例代码只是一个快速草图,它可以很容易地适应你想要做的任何事情(我稍微调整了一下,以展示我的意思)。重要的是要认识到 data()setData() 是标准模型项目中所有数据更改的单个出口和入口点。所有其他 API 最终都依赖于它们:这是 Model Subclassing 的一个基本特征。 - ekhumoro
啊,感谢您添加了如何获取先前的值,这不仅在这个问题上有指导意义,而且帮助我更好地理解了数据/setData的逻辑。今天下班后我会认真思考这个问题。 - eric
1
我终于完成了一个简单的小例子,使用了你们许多建议...http://codereview.stackexchange.com/questions/88065/to-do-tree-application-with-undo-redo-functionality 以防有人想在那里评论...咳咳... - eric
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接