Why is my QListView empty in IconMode? - python

I have a QListView displaying data from a custom ListModel. Everything seems to be working fine in the "regular" view mode (ListMode) -- icons, labels, drag/drop, etc. As soon as I change it to IconMode nothing displays.
Here's the relevant code. I've left out the main window and any other cruft, but if it helps I'll include it.
# Model
class TheModel(QtCore.QAbstractListModel):
def __init__(self, items = [], parent = None):
QtCore.QAbstractListModel.__init__(self, parent)
self.__items = items
def appendItem(self, item):
self.__items.append(item)
# item was added to end of list, so get that index
index = len(self.__items) - 1
# data was changed, so notify
self.dataChanged.emit(index, index)
def rowCount(self, parent):
return len(self.__items)
def data(self, index, role):
image = self.__items[index.row()]
if role == QtCore.Qt.DisplayRole:
# name
return image.name
if role == QtCore.Qt.DecorationRole:
# icon
return QtGui.QIcon(image.path)
return None
# ListView
class TheListView(QtGui.QListView):
def __init__(self, parent=None):
super(Ui_DragDropListView, self).__init__(parent)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setIconSize(QtCore.QSize(48, 48))
self.setViewMode(QtGui.QListView.IconMode)
# ...

After some heavy debugging, I found that data() was never getting called. The problem was with the way I was inserting data into the model: beginInsertRows() and endInsertRows() should be called. The new method resembles the following:
def appendItem(self, item):
index = len(self.__items)
self.beginInsertRows(QtCore.QModelIndex(), index, index)
self.__items.append(item)
self.endInsertRows()
Despite the old method not using using beginInsertRows() and endInsertRows(), ListMode worked just fine. That's what threw me off: I still don't think it should have worked. Quirk?

Related

Update Object when Checkbox clicked in QTableView

I've been learning python a fair it lately and I've come across a few questions here and I'm not entirely sure how to solve them. Each item in the Table is displaying data from a class object called PlayblastJob. This is being built using Python and PySide.
When a user selects a bunch of rows in the Table and clicks 'Randomize Selected Values', the displayed data does not update until the cursor hovers over the table or i click something in the view. How can i refresh the data in all the columns and rows each time the button is clicked?
When a user clicks the 'Checkbox' how can I have that signal set the property 'active' of that rows particular Job object instance?
Code that creates ui in screenshot above:
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()
The data in a Qt item model should always be set using setData().
The obvious reason is that the default implementations of item views always call that method whenever the data is modified by the user (eg. manually editing the field), or more programmatically (checking/unchecking a checkable item, like in your case). The other reason is for more consistency with the whole model structure of Qt framework, which should not be ignored.
In any way, the most important thing is that whenever the data is changed, the dataChanged() signal must be emitted. This ensures that all views using the model are notified about the change and eventually update themselves accordingly.
So, while you could manually emit the dataChanged signal from your randomizeSelected function, I would advise you against so.
The dataChanged should only be emitted for the indexes that have actually changed. While you could theoretically emit a generic signal that has the top-left and bottom-right indexes, it's considered bad practice: the view doesn't know what data (and role) has changed and if it gets a signal saying that the whole model has changed it will have to do lots of computations; even if those computations might seem to happen instantly, if you only change even a single index text they become absolutely unnecessary. I know that yours is a very simple model, but for learning purposes it's important to keep this in mind.
On the other hand, if you want to correctly emit the signal for the changed index alone, this means that you need to manually create the model.index() correctly, making the whole structure unnecessary complex, especially if at some point you need more ways to change the data.
In any case, there's no direct and easy way to do so when dealing with checkable items, since it's up to the view to notify the model about the check state change.
Using setData() allows you to have a centralized way to actually set the data to the model and ensure that everything is correctly updated accordingly. Any other method is not only discouraged, but may lead to unexpected behavior (like yours).
Finally, abstract models only have the ItemIsEnabled and ItemIsSelectable flags, so in order to allow checking and uncheking items, you need to override the flags() method too to add the ItemIsUserCheckable flag, and then implement the relative check in the 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']))
Note: selectedRows() defaults to the first column, so I'm using index.sibling() to get the correct index of the second and fourth column at the same row.
Note2: PySide has been considered obsolete from years. You should update to PySide2 at least.

QTreeView requests index for invalid row

Have a look at the following MWE.
It is a simple QAbstractItemModel with only a single level, storing its items in a list. I create a QTreeView to display the model, and a button to remove the 2nd item.
from PyQt5.QtCore import QModelIndex, QAbstractItemModel, Qt
from PyQt5.QtWidgets import QTreeView, QApplication, QPushButton
class Item:
def __init__(self, title):
self.title = title
class TreeModel(QAbstractItemModel):
def __init__(self, parent=None):
super().__init__(parent)
self._items = [] # typing.List[Item]
def addItem(self, item: Item):
self.beginInsertRows(QModelIndex(), len(self._items), len(self._items))
self._items.append(item)
self.endInsertRows()
def removeItem(self, item: Item):
index = self._items.index(item)
self.beginRemoveRows(QModelIndex(), index, index)
self._items.remove(item)
self.endRemoveRows()
# ----- overridden methods from QAbstractItemModel -----
# noinspection PyMethodOverriding
def data(self, index: QModelIndex, role):
item = index.internalPointer()
if role == Qt.DisplayRole:
return item.title
# noinspection PyMethodOverriding
def rowCount(self, parent=QModelIndex()):
if not parent.isValid():
return len(self._items)
return 0
# noinspection PyMethodOverriding
def columnCount(self, parent=QModelIndex()):
return 1
# noinspection PyMethodOverriding
def index(self, row: int, col: int, parent=QModelIndex()):
assert not parent.isValid()
return self.createIndex(row, 0, self._items[row])
def parent(self, index=QModelIndex()):
return QModelIndex()
def removeItem():
model.removeItem(item2)
if __name__ == '__main__':
app = QApplication([])
model = TreeModel()
button = QPushButton('Delete')
button.clicked.connect(removeItem)
button.show()
item1 = Item('Item 1')
model.addItem(item1)
item2 = Item('Item 2')
model.addItem(item2)
treeView = QTreeView()
treeView.setModel(model)
treeView.show()
app.exec()
As far as I can tell, the implementation of my model is correct (though very basic). In particular, the row, and column counts it reports are correct, and it never creates indices for data that would not be valid.
Steps to reproduce my issue:
Run the code above.
In the tree view, select Item 2.
Press the Delete button.
On my system, the application crashes in beginRemoveRows(), because the view requests a QModelIndex for row 2. Naturally, row 2 does not exist.
Any idea why the QTreeView would think there would be 3 rows, when the model explicitly reports there are only 2?
When an item is added, moved, removed, etc, what the model does is verify the QPersistentModelIndex are valid or not, so it calls the index() method of QAbstractItemModel. And in that method it is the developer's responsibility to verify if the row or column is valid, and for that the model provides the hasIndex() method that you do not use causing the error you point out, so the solution is:
def index(self, row: int, col: int, parent=QModelIndex()):
if not self.hasIndex(row, col, parent):
return QModelIndex()
assert not parent.isValid()
return self.createIndex(row, 0, self._items[row])

QTableView model crash when calling endInsertRows()

I have been trying to update a QTableViewModel when inserting a new object that represents a row. I did follow the advise of several question in SO, but I cannot get an example to work.
After debugging, I found that the call to self.endInsertRows() produces the crash.
This is a minimal example:
import sys
from PyQt5.QtWidgets import *
from PyQt5 import QtCore, QtGui, QtWidgets
class Wire:
def __init__(self, name, x, y, gmr, r):
self.name = name
self.x = x
self.y = y
self.r = r
self.gmr = gmr
class WiresCollection(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.header = ['Name', 'R (Ohm/km)', 'GMR (m)']
self.index_prop = {0: 'name', 1: 'r', 2: 'gmr'}
self.wires = list()
def add(self, wire: Wire):
"""
Add wire
:param wire:
:return:
"""
row = len(self.wires)
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.wires.append(wire)
self.endInsertRows()
def delete(self, index):
"""
Delete wire
:param index:
:return:
"""
row = len(self.wires)
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self.wires.pop(index)
self.endRemoveRows()
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.wires)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self.header)
def parent(self, index=None):
return QtCore.QModelIndex()
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
val = getattr(self.wires[index.row()], self.index_prop(index.column()))
return str(val)
return None
def headerData(self, p_int, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self.header[p_int]
def setData(self, index, value, role=QtCore.Qt.DisplayRole):
"""
Set data by simple editor (whatever text)
:param index:
:param value:
:param role:
"""
wire = self.wires[index.row()]
attr = self.index_prop[index.column()]
setattr(wire, attr, value)
class TowerBuilderGUI(QtWidgets.QDialog):
def __init__(self, parent=None):
"""
Constructor
Args:
parent:
"""
QtWidgets.QDialog.__init__(self, parent)
self.setWindowTitle('Tower builder')
# GUI objects
self.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
self.layout = QVBoxLayout(self)
self.wires_tableView = QTableView()
self.add_wire_pushButton = QPushButton()
self.add_wire_pushButton.setText('Add')
self.delete_wire_pushButton = QPushButton()
self.delete_wire_pushButton.setText('Delete')
self.layout.addWidget(self.wires_tableView)
self.layout.addWidget(self.add_wire_pushButton)
self.layout.addWidget(self.delete_wire_pushButton)
self.setLayout(self.layout)
# Model
self.wire_collection = WiresCollection(self)
# set models
self.wires_tableView.setModel(self.wire_collection)
# button clicks
self.add_wire_pushButton.clicked.connect(self.add_wire_to_collection)
self.delete_wire_pushButton.clicked.connect(self.delete_wire_from_collection)
def msg(self, text, title="Warning"):
"""
Message box
:param text: Text to display
:param title: Name of the window
"""
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText(text)
# msg.setInformativeText("This is additional information")
msg.setWindowTitle(title)
# msg.setDetailedText("The details are as follows:")
msg.setStandardButtons(QMessageBox.Ok)
retval = msg.exec_()
def add_wire_to_collection(self):
"""
Add new wire to collection
:return:
"""
name = 'Wire_' + str(len(self.wire_collection.wires) + 1)
wire = Wire(name, x=0, y=0, gmr=0, r=0.01)
self.wire_collection.add(wire)
def delete_wire_from_collection(self):
"""
Delete wire from the collection
:return:
"""
idx = self.ui.wires_tableView.currentIndex()
sel_idx = idx.row()
if sel_idx > -1:
self.wire_collection.delete(sel_idx)
else:
self.msg('Select a wire in the wires collection')
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = TowerBuilderGUI()
window.show()
sys.exit(app.exec_())
As indicated in the comments you have 2 errors:
The first is when you press add because when you add a new item you have to refresh the view and that's why it's called data() method and it's where the error is shown in self.index_prop(index.column()), index_pro is a dictionary so you should use [] instead of ().
val = getattr(self.wires[index.row()], self.index_prop[index.column()])
Another error is generated by the line idx = self.ui.wires_tableView.currentIndex(), the ui does not exist and it is not necessary, sure it is a remnant of a previous code, to access wires_tableView as this is a member of the class not it is necessary to use an intermediary, you must access directly with self: idx = self.wires_tableView.currentIndex()
The above are typos and probably mark it so that the question is closed there is another error that is not, and that is the reason for my answer.
In the line self.beginRemoveRows(...) you must pass the row that you are going to remove but you are passing the row that does not exist:
row = len(self.wires)
self.beginRemoveRows(QtCore.QModelIndex(), row, row) # <---- row does not exist in the table
The solution is simple, change it by index:
def delete(self, index):
"""
Delete wire
:param index:
:return:
"""
self.beginRemoveRows(QtCore.QModelIndex(), index, index)
self.wires.pop(index)
self.endRemoveRows()

How to refresh QTableView when it is driven by model

The code below creates QTableView driven by self.myModel (QAbstractTableModel).
'Show All' self.checkBox is linked to self.myModel.cbChanged() method.
Question: How to modify this code so 'QTableView' gets refreshed as soon as checkbox is checked?
The goal: when the checkbox is checked we want the odd numbered items to be displayed. And the even numbered items to be hidden.
When the checkbox is off (unchecked) we want the even numbered items to be displayed. All the odd numbered items are hidden.
import sys, os
from PyQt4 import QtCore, QtGui
app=QtGui.QApplication(sys.argv)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items=['One','Two','Three','Four','Five','Six','Seven']
self.cb_status=True
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def data(self, index, role):
if not index.isValid() or not (0<=index.row()<len(self.items)):
return QtCore.QVariant()
item=str(self.items[index.row()])
if role==QtCore.Qt.DisplayRole and self.cb_status:
return item
else:
return QtCore.QVariant()
def cbChanged(self, arg=None):
self.cb_status=arg
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
mainLayout=QtGui.QHBoxLayout()
self.setLayout(mainLayout)
self.viewA=QtGui.QTableView()
self.viewA.horizontalHeader().setResizeMode(QtGui.QHeaderView.Stretch)
self.myModel=TableModel()
self.viewA.setModel(self.myModel)
self.checkBox=QtGui.QCheckBox("Show All")
self.checkBox.stateChanged.connect(self.myModel.cbChanged)
self.checkBox.setChecked(self.myModel.cb_status)
mainLayout.addWidget(self.viewA)
mainLayout.addWidget(self.checkBox)
self.show()
view=Window()
sys.exit(app.exec_())
For this purpose, you can use the QSortFilterProxyModel class. This way, we don't tamper with the actual source model's structure or it's data. We just map the main source to this proxy model, which the view uses to display filtered/sorted data. We can affect the data in the proxy model as we please, without the risk of tampering the source model.
Here is your source code modified to use this:
import sys, os
from PyQt4 import QtCore, QtGui
app=QtGui.QApplication(sys.argv)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items=['One','Two','Three','Four','Five','Six','Seven']
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def data(self, index, role):
if not index.isValid() or not (0<=index.row()<len(self.items)):
return QtCore.QVariant()
item=str(self.items[index.row()])
if role==QtCore.Qt.DisplayRole:
return item
else:
return QtCore.QVariant()
class MySortFilterProxyModel(QtGui.QSortFilterProxyModel):
def __init__(self):
super(MySortFilterProxyModel, self).__init__()
self.cb_status=True
def cbChanged(self, arg=None):
self.cb_status=arg
print self.cb_status
self.invalidateFilter()
def filterAcceptsRow(self, sourceRow, sourceParent):
print_when_odd_flag = self.cb_status
is_odd = True
index = self.sourceModel().index(sourceRow, 0, sourceParent)
print "My Row Data: %s" % self.sourceModel().data(index, role=QtCore.Qt.DisplayRole)
if (sourceRow + 1) % 2 == 0:
is_odd = False
if print_when_odd_flag:
if is_odd:
return True
else:
return False
else:
if not is_odd:
return True
else:
return False
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
mainLayout=QtGui.QHBoxLayout()
self.setLayout(mainLayout)
self.viewA=QtGui.QTableView()
self.viewA.horizontalHeader().setResizeMode(QtGui.QHeaderView.Stretch)
self.myModel=TableModel()
self.sortModel = MySortFilterProxyModel()
self.sortModel.setSourceModel(self.myModel)
self.viewA.setModel(self.sortModel)
self.checkBox=QtGui.QCheckBox("Show All")
self.checkBox.stateChanged.connect(self.sortModel.cbChanged)
self.checkBox.setChecked(self.sortModel.cb_status)
mainLayout.addWidget(self.viewA)
mainLayout.addWidget(self.checkBox)
self.show()
view=Window()
sys.exit(app.exec_())
As you can see, I have removed all connection from UI and the main source model. The main source model does not care about whether the checkbox is set or not. This keeps it decoupled. It's cleaner. The proxy model has been given this responsibility now. The filterAcceptsRow() does the main heavy lifting of displaying the right row based on whether the index of the row shown is odd or even based on the checkbox status.
I have added a few print statements to it, just in case you want to alter the logic based on the data and not the index.
Check out the docs on QSortFilterProxyModel and some examples here.
Hope this was useful.
QTableView instance has viewport which has update method through that we can update the table content
Use the update method of viewport to update the content of a tableview
viewport().update()
try using :
self.viewA.viewport().update()

abstractTableModel to display two sets of interdependent data in PySide

I'm building a python/PySide tool for cinematography. I created an object which represents a shot. It has properties for start time, end time, and a list of references to actor objects. The actor object has simple properties (name, build, age, etc) and can be shared between shots.
I want to display this in two table views in PySide. One table view lists the shots (and properties in columns), while the other displays actors referenced in the selected shots. If no shots are selected, the second table view is empty. If multiple shots are selected, all referenced actors are displayed in the actor table view.
I created an abstractTableModel for my shot data and everything is working correctly for the shot data in its corresponding table view. However I'm not sure how to even approach the table view for the actors. Should I use another abstractTableModel for the actors? I can't seem to figure out how to feed/connect the data to the second table view for actors contained in selected shots using a abstractTableModel.
I think part of my issue is that I only want to display the actor information once, regardless of whether multiple selected shots reference the same actor. After multiple failed attempts, I'm thinking that I need to redirect and parse the selected shot information (their actor list property) into a custom property of the main window to contain a list of all referenced actor indices and make an abstractTableModel which uses this to fetch the actual actor properties for display. I'm not fully confident this will work, not to mention my gut tells me this is a messy approach, so I've come here for advice.
Is this the correct approach? If not, what's the 'correct' way of setting this up in PySide/python.
Please keep in mind this is my first foray in data models and PySide.
Here's the shot model.
class ShotTableModel(QtCore.QAbstractTableModel):
def __init__(self, data=[], parent=None, *args):
super(ShotTableModel, self).__init__(parent)
self._data = data
def rowCount(self, parent):
return len(self._data.shots)
def columnCount(self, parent):
return len(self._data._headers_shotList)
def getItemFromIndex(self, index):
if index.isValid():
item = self._data.shots[index.row()]
if item:
return item
return None
def flags(self, index):
if index.isValid():
item = self.getItemFromIndex(index)
return item.qt_flags(index.column())
def data(self, index, role):
if not index.isValid():
return None
item = self.getItemFromIndex(index)
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return item.qt_data(index.column())
if role == QtCore.Qt.CheckStateRole:
if index.column() is 0:
return item.qt_checked
# if role == QtCore.Qt.BackgroundColorRole:
# return QtGui.QBrush()
# if role == QtCore.Qt.FontRole:
# return QtGui.QFont()
if role == QtCore.Qt.DecorationRole:
if index.column() == 0:
resource = item.qt_resource()
return QtGui.QIcon(QtGui.QPixmap(resource))
if role == QtCore.Qt.ToolTipRole:
return item.qt_toolTip()
return None
def setData(self, index, value, role = QtCore.Qt.EditRole):
if index.isValid():
item = self.getItemFromIndex(index)
if role == QtCore.Qt.EditRole:
item.qt_setData(index.column(), value)
self.dataChanged.emit(index, index)
return value
if role == QtCore.Qt.CheckStateRole:
if index.column() is 0:
item.qt_checked = value
return True
return value
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self._data._headers_shotList[section]
def insertRows(self, position, rows, parent = QtCore.QModelIndex()):
self.beginInsertRows(parent, position, position + rows - 1)
for row in range(rows):
newShotName = self._data.getUniqueName(self._data.shots, 'New_Shot')
newShot = Shot(newShotName)
self._data.shots.insert(position, newShot)
self.endInsertRows()
return True
def removeRows(self, position, rows, parent = QtCore.QModelIndex()):
self.beginRemoveRows(parent, position, position + rows - 1)
for row in range(rows):
self._data.shots.pop(position)
self.endRemoveRows()
return True
Here's the data block containing the shot and actor instances. This is what I pass to the shot model.
class ShotManagerData(BaseObject):
def __init__(self, name='', shots=[], actors=[]):
super(ShotManagerData, self).__init__(name)
self._shots = shots # Shot(name="New_Shot", start=0, end=100, actors=[])
self._actors = actors # Actor(name="New_Actor", size="Average", age=0)
self.selectedShotsActors = [] #decided to move to this data block
self._headers_shotList = ['Name', 'Start Frame', 'End Frame']
self._headers_actorList = ['Name', 'Type', 'RootNode', 'File']
def save(self, file=None):
mEmbed.save('ShotManagerData', self)
#classmethod
def load(cls, file=None):
return(mEmbed.load('ShotManagerData'))
#staticmethod
def getUniqueName(dataList, baseName='New_Item'):
name = baseName
increment = 0
list_of_names = [data.name if issubclass(data.__class__, BaseObject) else str(data) for data in dataList]
while name in list_of_names:
increment += 1
name = baseName + '_{0:02d}'.format(increment)
return name
#property
def actors(self):
return self._actors
#property
def shots(self):
return self._shots
def actorsOfShots(self, shots):
actorsOfShots = []
for shot in shots:
for actor in shot.actors:
if actor not in actorsOfShots:
actorsOfShots.append(actor)
return actorsOfShots
def shotsOfActors(self, actors):
shotsOfActors = []
for actor in actors:
for shot in self.shots:
if actor in shot.actors and actor not in shotsOfActors:
shotsOfActors.append(shot)
return shotsOfActors
Finally the main tool.
class ShotManager(form, base):
def __init__(self, parent=None):
super(ShotManager, self).__init__(parent)
self.setupUi(self)
#=======================================================================
# Properties
#=======================================================================
self.data = ShotManagerData() #do any loading if necessary here
self.actorsInSelectedShots = []
#test data
actor1 = Actor('Actor1')
actor2 = Actor('Actor2')
actor3 = Actor('Actor3')
shot1 = Shot('Shot1', [actor1, actor2])
shot2 = Shot('Shot2', [actor2, actor3])
shot3 = Shot('Shot3', [actor1])
self.data.actors.append(actor1)
self.data.actors.append(actor2)
self.data.actors.append(actor3)
self.data.shots.append(shot1)
self.data.shots.append(shot2)
self.data.shots.append(shot3)
#=======================================================================
# Models
#=======================================================================
self._model_shotList = ShotTableModel(self.data)
self._proxyModel_shotList = QtGui.QSortFilterProxyModel()
self._proxyModel_shotList.setSourceModel(self._model_shotList)
self.shotList.setModel(self._proxyModel_shotList) #this is the QTableView
self._selModel_shotList = self.shotList.selectionModel()
self.shotList.setSortingEnabled(True)
self._proxyModel_shotList.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._model_actorList = SelectedShotsActorTableModel(self.data)
self._proxyModel_actorList = QtGui.QSortFilterProxyModel()
self._proxyModel_actorList.setSourceModel(self._model_actorList)
self.actorList.setModel(self._proxyModel_actorList)
self._selModel_actorList = self.actorList.selectionModel()
#=======================================================================
# Events
#=======================================================================
self.addShot.clicked.connect(self.addShot_clicked)
self.delShot.clicked.connect(self.delShot_clicked)
self._selModel_shotList.selectionChanged.connect(self.shotList_selectionChanged)
#===========================================================================
# General Functions
#===========================================================================
def getSelectedRows(self, widget):
selModel = widget.selectionModel()
proxyModel = widget.model()
model = proxyModel.sourceModel()
rows = [proxyModel.mapToSource(index).row() for index in selModel.selectedRows()]
rows.sort()
return rows
def getSelectedItems(self, widget):
selModel = widget.selectionModel()
proxyModel = widget.model()
model = proxyModel.sourceModel()
indices = [proxyModel.mapToSource(index) for index in selModel.selectedRows()]
items = [model.getItemFromIndex(index) for index in indices]
return items
#===========================================================================
# Event Functions
#===========================================================================
def addShot_clicked(self):
position = len(self.data.shots)
self._proxyModel_shotList.insertRows(position,1)
def delShot_clicked(self):
rows = self.getSelectedRows(self.shotList)
for row in reversed(rows):
self._proxyModel_shotList.removeRows(row, 1)
def shotList_selectionChanged(self, selected, deselected):
selectedShots = self.getSelectedItems(self.shotList)
print 'SelectedShots: {}'.format(selectedShots)
self.data.selectedShotsActors = self.data.actorsOfShots(selectedShots)
print 'ActorsOfShots: {}'.format(self.data.selectedShotsActors)
self._proxyModel_actorList.setData() # this line reports missing variables
This is the selectedShotActors model:
class SelectedShotsActorTableModel(QtCore.QAbstractTableModel):
def __init__(self, data=[], headers=[], parent=None, *args):
super(SelectedShotsActorTableModel, self).__init__(parent)
self._data = data
def rowCount(self, parent):
return len(self._data.selectedShotsActors)
def columnCount(self, parent):
return len(self._data._headers_actorList)
def getItemFromIndex(self, index):
if index.isValid():
item = self._data.selectedShotsActors[index.row()]
if item:
return item
return None
def flags(self, index):
if index.isValid():
item = self.getItemFromIndex(index)
return item.qt_flags(index.column())
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
item = self.getItemFromIndex(index)
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return item.qt_data(index.column())
if role == QtCore.Qt.CheckStateRole:
if index.column() is 0:
return item.qt_checked
#
# if role == QtCore.Qt.BackgroundColorRole:
# return QtGui.QBrush()
#
# if role == QtCore.Qt.FontRole:
# return QtGui.QFont()
if role == QtCore.Qt.DecorationRole:
if index.column() == 0:
resource = item.qt_resource()
return QtGui.QIcon(QtGui.QPixmap(resource))
if role == QtCore.Qt.ToolTipRole:
return item.qt_toolTip()
def setData(self, index, value, role = QtCore.Qt.EditRole):
if index.isValid():
item = self.getItemFromIndex(index)
if role == QtCore.Qt.EditRole:
item.qt_setData(index.column(), value)
self.dataChanged.emit(index, index)
return value
if role == QtCore.Qt.CheckStateRole:
if index.column() is 0:
item.qt_checked = value
return True
return value
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self._data._headers_actorList[section]
I would suggest that you use a proxy model. Proxy models do not hold data, they link to an existing model and provide the opportunity to sort, filter or restructure that data as necessary.
Specifically, you can create a QSortFilterProxyModel object with the QTableView's QItemSelectionModel as the source. You can then create a custom filter that constructs a list of actors based on the selected shots. The advantage of this approach is that the proxy model and the view will automatically update as the selection changes. I think this approach adheres to the intent of MVC better than adding code to MainWindow.
See the Custom Sort/Filter Model example for more information on how to do this.
http://qt-project.org/doc/qt-5/qtwidgets-itemviews-customsortfiltermodel-example.html
If you need some background information (I am new to Qt MVC too; I feel your pain) here are some other useful links:
Model-View Programming: Proxy models
http://qt-project.org/doc/qt-5/model-view-programming.html#proxy-models
QSortFilterProxyModel
http://qt-project.org/doc/qt-5/qsortfilterproxymodel.html
My suggestion would be to update the actor model on a selectionChanged() signal from the selectionModel of your shot QTableView.
Each time the signal is emitted (when the shot selection is changed), you need to reset the actor model and then iterate over the selected model indexes within your selection, and get a reference to the shot object for each selected row of your model. For each shot object you can get a list of actors. Then you need to check if the actor is in the actor model, and if it is not, add it in.
Now this is a little inefficient because you are resetting the actor model each time the selection of the shot model is changed. You do have information from the selectionChanged() signal about which rows were deselected, so you could instead remove entries from your actor model when rows of the shot model re deselected, but only if no other selected shot row contains a given actor from your deselected shot.
There are a few options for where this code could live in your project, but I would probably but it where you already have access to the views and models for both actors and shots. This is probably in your subclass of QMainWindow, but without seeing code it is hard to tell!
Hope that helps :)

Categories