首页 > 解决方案 > 在 QTableView 中单击复选框时更新对象

问题描述

我最近一直在公平地学习 python,在这里我遇到了一些问题,我不完全确定如何解决它们。表中的每个项目都显示来自名为 PlayblastJob 的类对象的数据。这是使用 Python 和 PySide 构建的。

  1. 当用户在表中选择一堆行并单击“随机化选定值”时,显示的数据不会更新,直到光标悬停在表上或单击视图中的某些内容。每次单击按钮时,如何刷新所有列和行中的数据?

  2. 当用户单击“复选框”时,如何让该信号设置该行特定 Job 对象实例的属性“活动”?

在此处输入图像描述

在上面的屏幕截图中创建 ui 的代码:

import os
import sys
import random
from PySide import QtCore, QtGui


class PlayblastJob(object):
    def __init__(self, **kwargs):
        super(PlayblastJob, self).__init__()

        # instance properties
        self.active = True
        self.name = ''
        self.camera = ''
        self.renderWidth = 1920
        self.renderHeight = 1080
        self.renderScale = 1.0
        self.status = ''

        # initialize attribute values
        for k, v in kwargs.items():
            if hasattr(self, k):
                setattr(self, k, v)


    def getScaledRenderSize(self):
        x = int(self.renderWidth * self.renderScale)
        y = int(self.renderHeight * self.renderScale)
        return (x,y)


class JobModel(QtCore.QAbstractTableModel):

    HEADERS = ['Name', 'Camera', 'Resolution', 'Status']

    def __init__(self):
        super(JobModel, self).__init__()
        self.items = []


    def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
        if orientation == QtCore.Qt.Horizontal:
            if role == QtCore.Qt.DisplayRole:
                return self.HEADERS[section]
        return None


    def columnCount(self, parent=QtCore.QModelIndex()):
        return len(self.HEADERS)


    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self.items)


    def appendJob(self, *items):
        self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount() + len(items) - 1)
        for item in items:
            assert isinstance(item, PlayblastJob)
            self.items.append(item)
        self.endInsertRows()


    def removeJobs(self, items):
        rowsToRemove = []
        for row, item in enumerate(self.items):
            if item in items:
                rowsToRemove.append(row)
        for row in sorted(rowsToRemove, reverse=True):
            self.beginRemoveRows(QtCore.QModelIndex(), row, row)
            self.items.pop(row)
            self.endRemoveRows()


    def clear(self):
        self.beginRemoveRows(QtCore.QModelIndex(), 0, self.rowCount())
        self.items = []
        self.endRemoveRows()


    def data(self, index, role=QtCore.Qt.DisplayRole):
        if not index.isValid():
            return

        row = index.row()
        col = index.column()

        if 0 <= row < self.rowCount():
            item = self.items[row]
            if role == QtCore.Qt.DisplayRole:
                if col == 0:
                    return item.name
                elif col == 1:
                    return item.camera
                elif col == 2:
                    width, height = item.getScaledRenderSize()
                    return '{} x {}'.format(width, height)
                elif col == 3:
                    return item.status.title()
            elif role == QtCore.Qt.ForegroundRole:
                if col == 3:
                    if item.status == 'error':
                        return QtGui.QColor(255, 82, 82)
                    elif item.status == 'success':
                        return QtGui.QColor(76, 175, 80)
                    elif item.status == 'warning':
                        return QtGui.QColor(255, 193, 7)
            elif role == QtCore.Qt.TextAlignmentRole:
                if col == 2:
                    return QtCore.Qt.AlignCenter
                if col == 3:
                    return QtCore.Qt.AlignCenter
            elif role == QtCore.Qt.CheckStateRole:
                if col == 0:
                    if item.active:
                        return QtCore.Qt.Checked
                    else:
                        return QtCore.Qt.Unchecked
            elif role == QtCore.Qt.UserRole:
                return item
        return None


class JobQueue(QtGui.QWidget):
    '''
    Description:
        Widget that manages the Jobs Queue
    '''
    def __init__(self):
        super(JobQueue, self).__init__()
        self.resize(400,600)

        # controls
        self.uiAddNewJob = QtGui.QPushButton('Add New Job')
        self.uiAddNewJob.setToolTip('Add new job')

        self.uiRemoveSelectedJobs = QtGui.QPushButton('Remove Selected')
        self.uiRemoveSelectedJobs.setToolTip('Remove selected jobs')

        self.jobModel = JobModel()
        self.uiJobTableView = QtGui.QTableView()
        self.uiJobTableView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
        self.uiJobTableView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
        self.uiJobTableView.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
        self.uiJobTableView.setModel(self.jobModel)

        self.jobSelection = self.uiJobTableView.selectionModel()

        self.uiRandomize = QtGui.QPushButton('Randomize Selected Values')
        self.uiPrintJobs = QtGui.QPushButton('Print Jobs')

        # sub layouts
        self.jobQueueToolsLayout = QtGui.QHBoxLayout()
        self.jobQueueToolsLayout.addWidget(self.uiAddNewJob)
        self.jobQueueToolsLayout.addWidget(self.uiRemoveSelectedJobs)
        self.jobQueueToolsLayout.addStretch()
        self.jobQueueToolsLayout.addWidget(self.uiRandomize)

        # layout
        self.mainLayout = QtGui.QVBoxLayout()
        self.mainLayout.addLayout(self.jobQueueToolsLayout)
        self.mainLayout.addWidget(self.uiJobTableView)
        self.mainLayout.addWidget(self.uiPrintJobs)
        self.setLayout(self.mainLayout)

        # connections
        self.uiAddNewJob.clicked.connect(self.addNewJob)
        self.uiRemoveSelectedJobs.clicked.connect(self.removeSelectedJobs)
        self.uiRandomize.clicked.connect(self.randomizeSelected)
        self.uiPrintJobs.clicked.connect(self.printJobs)


    # methods
    def addNewJob(self):
        name = random.choice(['Kevin','Melissa','Suzie','Eddie','Doug'])
        job = PlayblastJob(name=name, camera='Camera001', startFrame=50)
        self.jobModel.appendJob(job)

    def removeSelectedJobs(self):
        jobs = self.getSelectedJobs()
        self.jobModel.removeJobs(jobs)

    def getSelectedJobs(self):
        jobs = [x.data(QtCore.Qt.UserRole) for x in self.jobSelection.selectedRows()]
        return jobs

    def randomizeSelected(self):
        jobs = self.getSelectedJobs()
        for job in jobs:
            job.camera = random.choice(['Canon','Nikon','Sony','Red'])
            job.status = random.choice(['error','warning','success'])

    def printJobs(self):
        jobs = self.jobModel.items
        for job in jobs:
            print vars(job)

def main():
    app = QtGui.QApplication(sys.argv)
    window = JobQueue()
    window.show()
    app.exec_()


if __name__ == '__main__':
    main()

标签: pythonpyside

解决方案


Qt 项目模型中的数据应始终使用setData().

显而易见的原因是,项目视图的默认实现总是在用户修改数据时调用该方法(例如,手动编辑字段),或者更多地以编程方式(检查/取消选中可检查项目,如您的情况)。另一个原因是为了与 Qt 框架的整个模型结构更加一致,这一点不容忽视。

无论如何,最重要的是,每当数据发生变化时,都必须dataChanged()发出信号。这确保了所有使用模型的视图都会收到有关更改的通知,并最终相应地更新自己。

因此,虽然您可以手动dataChanged从函数中发出信号randomizeSelected,但我建议您不要这样做。应该只为实际更改
的索引发出。虽然理论上您可以发出具有左上角和右下角索引的通用信号,但这被认为是不好的做法:视图不知道哪些数据(和角色)发生了变化,以及它是否收到表明整个模型的信号已经改变了,它将不得不做很多事情dataChanged计算量;即使这些计算似乎立即发生,如果您只更改单个索引文本,它们也变得完全没有必要。我知道您的模型是一个非常简单的模型,但出于学习目的,记住这一点很重要。
另一方面,如果您想单独为更改的索引正确发出信号,这意味着您需要手动创建model.index()正确的信号,使整个结构变得不必要的复杂,特别是如果在某些时候您需要更多方法来更改数据.
在任何情况下,在处理可检查项时都没有直接且简单的方法可以做到这一点,因为由视图通知模型有关检查状态的更改。

使用setData()允许您以一种集中的方式将数据实际设置到模型中,并确保所有内容都相应地正确更新。任何其他方法不仅不鼓励,而且可能导致意外行为(如您的)。

最后,抽象模型只有ItemIsEnabledandItemIsSelectable标志,因此为了允许检查和取消选中项目,您还需要重写该flags()方法以添加ItemIsUserCheckable标志,然后在setData().

class JobModel(QtCore.QAbstractTableModel):
    # ...
    def setData(self, index, data, role=QtCore.Qt.EditRole):
        if role in (QtCore.Qt.EditRole, QtCore.Qt.DisplayRole):
            if index.column() == 0:
                self.items[index.row()].name = data
            elif index.column() == 1:
                self.items[index.row()].camera = data
            # I'm skipping the third column check, as you will probably need some 
            # custom function there, assuming it should be editable
            elif index.column() == 3:
                self.items[index.row()].status = data
            else:
                return False
            self.dataChanged.emit(index, index)
            return True
        elif role == QtCore.Qt.CheckStateRole and index.column() == 0:
            self.items[index.row()].active = bool(data)
            self.dataChanged.emit(index, index)
            return True
        return False

    def flags(self, index):
        flags = super().flags(index)
        if index.column() == 0:
            flags |= QtCore.Qt.ItemIsUserCheckable
        return flags


class JobQueue(QtGui.QWidget):
    # ...
    def randomizeSelected(self):
        for index in self.jobSelection.selectedRows():
            self.jobModel.setData(index.sibling(index.row(), 1), 
                random.choice(['Canon','Nikon','Sony','Red']))
            self.jobModel.setData(index.sibling(index.row(), 3), 
                random.choice(['error','warning','success']))

注意:selectedRows()默认为第一列,所以我index.sibling()用来在同一行获取第二列和第四列的正确索引。

2:PySide 多年来一直被认为已经过时。您至少应该更新到 PySide2。


推荐阅读