在 QTableView 中单击复选框时更新对象
Update Object when Checkbox clicked in QTableView
我最近一直在学习 python,我在这里遇到了一些问题,但我不太确定如何解决它们。 Table 中的每个项目都显示来自名为 PlayblastJob 的 class 对象的数据。这是 built 使用 Python 和 PySide。
当用户在 Table 中选择一堆行并单击 'Randomize Selected Values' 时,显示的数据不会更新,直到光标悬停在 table 上或者我单击视图中的某些内容。如何在每次单击按钮时刷新所有列和行中的数据?
当用户单击 'Checkbox' 时,我如何让该信号设置该行特定作业对象实例的 属性 'active'?
在上面的屏幕截图中创建 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()
Qt 项模型中的数据应始终 使用setData()
设置。
显而易见的原因是项目视图的默认实现总是在用户修改数据(例如手动编辑字段)时调用该方法,或者更多地以编程方式(checking/unchecking 可检查的项目,就像你的情况一样)。另一个原因是为了更符合Qt框架的整体模型结构,这一点不容忽视。
无论如何,最重要的是每当数据改变时,dataChanged()
信号必须发射。这确保 所有 使用该模型的视图都会收到有关更改的通知,并最终相应地更新自己。
因此,虽然您 可以 从 randomizeSelected
函数手动发出 dataChanged
信号,但我建议您不要这样做。
dataChanged
只应为 实际上 已更改的索引发出。虽然理论上您可以发出具有左上角和右下角索引的通用信号,但它被认为是不好的做法:视图不知道哪些数据(和角色)发生了变化,如果它收到一个信号说 whole 模型已经改变,它必须做 lots 的计算;即使这些计算似乎是立即发生的,但如果您只更改一个索引文本,它们就变得完全没有必要了。我知道您的模型非常简单,但出于学习目的,请务必牢记这一点。
另一方面,如果你想单独为改变的索引正确发出信号,这意味着你需要手动正确地创建 model.index()
,使整个结构变得不必要的复杂,特别是在某些时候你需要更多更改数据的方法。
在任何情况下,在处理可检查项目时都没有直接和简单的方法,因为要通知模型有关检查状态更改的视图。
使用 setData()
允许您以 集中式 方式实际将数据设置到模型并确保所有内容都相应地正确更新。任何其他方法不仅不被鼓励,而且可能导致意外行为(就像你的那样)。
最后,抽象模型只有 ItemIsEnabled
和 ItemIsSelectable
标志,所以为了允许检查和取消检查项目,你需要重写 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。
我最近一直在学习 python,我在这里遇到了一些问题,但我不太确定如何解决它们。 Table 中的每个项目都显示来自名为 PlayblastJob 的 class 对象的数据。这是 built 使用 Python 和 PySide。
当用户在 Table 中选择一堆行并单击 'Randomize Selected Values' 时,显示的数据不会更新,直到光标悬停在 table 上或者我单击视图中的某些内容。如何在每次单击按钮时刷新所有列和行中的数据?
当用户单击 'Checkbox' 时,我如何让该信号设置该行特定作业对象实例的 属性 'active'?
在上面的屏幕截图中创建 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()
Qt 项模型中的数据应始终 使用setData()
设置。
显而易见的原因是项目视图的默认实现总是在用户修改数据(例如手动编辑字段)时调用该方法,或者更多地以编程方式(checking/unchecking 可检查的项目,就像你的情况一样)。另一个原因是为了更符合Qt框架的整体模型结构,这一点不容忽视。
无论如何,最重要的是每当数据改变时,dataChanged()
信号必须发射。这确保 所有 使用该模型的视图都会收到有关更改的通知,并最终相应地更新自己。
因此,虽然您 可以 从 randomizeSelected
函数手动发出 dataChanged
信号,但我建议您不要这样做。
dataChanged
只应为 实际上 已更改的索引发出。虽然理论上您可以发出具有左上角和右下角索引的通用信号,但它被认为是不好的做法:视图不知道哪些数据(和角色)发生了变化,如果它收到一个信号说 whole 模型已经改变,它必须做 lots 的计算;即使这些计算似乎是立即发生的,但如果您只更改一个索引文本,它们就变得完全没有必要了。我知道您的模型非常简单,但出于学习目的,请务必牢记这一点。
另一方面,如果你想单独为改变的索引正确发出信号,这意味着你需要手动正确地创建 model.index()
,使整个结构变得不必要的复杂,特别是在某些时候你需要更多更改数据的方法。
在任何情况下,在处理可检查项目时都没有直接和简单的方法,因为要通知模型有关检查状态更改的视图。
使用 setData()
允许您以 集中式 方式实际将数据设置到模型并确保所有内容都相应地正确更新。任何其他方法不仅不被鼓励,而且可能导致意外行为(就像你的那样)。
最后,抽象模型只有 ItemIsEnabled
和 ItemIsSelectable
标志,所以为了允许检查和取消检查项目,你需要重写 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。