Update Object when Checkbox clicked in QTableView - python

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.

Related

Cannot select single item from already selected items in table view with pyqt5

I am playing with the model/view programming with pyqt to try to understand it.
My problem is that when I try si select in item from the already selected group of items the onSelection changed event does not trigger, and the selection behaviour becomes weird. (Not only cannot select items from previously selected butn also contiguous selections take place...).
If I comment the def data(self, _index, role=Qt.DisplayRole): method I get the behaviour I want, so I guess I am missing something with the way the data is populated in the table. But I cannot populate data in the table if this is commented (Hello :)).
I tried to handle it with the onMouseClick event and with the selection behaviour with no sucess.
The selection behaviour I want can be found also in this example:
https://wiki.python.org/moin/PyQt/Reading%20selections%20from%20a%20selection%20model
Find below my code, which might be a bit messy as I am making just some trials (sorry for that).
Any comment will be much appreciatted, many thanks.
from PyQt5.QtWidgets import QApplication, QTableView, QAbstractItemView
import sys
from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex, QItemSelection, QItemSelectionModel, QAbstractItemModel
class myTableModel(QAbstractTableModel):
def __init__(self, rows, columns, parent=None, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.rowCount = rows
self.columnCount = columns
self.table_data = [[None] * columns for _ in range(rows)]
self.unselectedItems = []
def rowCount(self, parent):
return self.rowCount
def columnCount(self, parent):
return self.columnCount
def flags(self, index):
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
def data(self, _index, role=Qt.DisplayRole):
if role == Qt.DisplayRole and _index.isValid():
row = _index.row()
column = _index.column()
item = _index.internalPointer()
if item is not None:
print(item)
value = self.table_data[row][column]
# print('value returned: ' + str(value) + ' row: ' + str(row) + ' col: ' + str(column))
return value
else:
return None
def setData(self, _index, value, role=Qt.EditRole):
if role == Qt.EditRole and _index.isValid():
# print(_index.row())
# self.arraydata[index.row()] = [value]
# print('Return from rowCount: {0}'.format(self.rowCount(index)))
row = _index.row()
column = _index.column()
self.table_data[row][column] = value
self.dataChanged.emit(_index, _index)
return True
return QAbstractTableModel.setData(self, index, value, role)
def updateSelection(self, selected, deselected):
selectedItems = selected.indexes()
for _index in selectedItems:
_text = f"({_index.row()}, {_index.column()})"
self.setData(_index, _text)
del selectedItems[:]
self.unselectedItems = deselected.indexes()
for _index in self.unselectedItems:
_text = "previous selection"
self.setData(_index, _text)
print('unselected item: ' + str(_index))
class myTableView(QTableView):
def __init__(self, rowCount, columnCount, model):
super().__init__()
self.rowCount = rowCount
self.columnCount = columnCount
self.model = model
self.setModel(model)
self.selectionModel().selectionChanged.connect(tblModel.updateSelection)
self.setSelectionMode(QAbstractItemView.ContiguousSelection)
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
selectedItems = self.selectedIndexes()
allIndexes = []
for i in range(self.rowCount):
for j in range(self.columnCount):
allIndexes.append(self.model.index(i, j))
# print('all indexes appended')
indexesToClear = [_index for _index in allIndexes if
_index not in selectedItems and _index not in self.model.unselectedItems]
for _index in indexesToClear:
valueFromIndex = str(self.model.data(_index, Qt.DisplayRole))
if valueFromIndex == "previous selection":
self.model.setData(_index, "")
# def mousePressEvent(self, event):
# if event.button() == Qt.LeftButton:
# self.selectionModel().reset()
app = QApplication(sys.argv)
tblModel = myTableModel(8, 4, app) # create table model
tblView = myTableView(8, 4, tblModel)
topLeft = tblModel.index(0, 0, QModelIndex())
bottomRight = tblModel.index(5, 2, QModelIndex())
selectionMode = tblView.selectionModel()
selection = QItemSelection(topLeft, bottomRight)
selectionMode.select(selection, QItemSelectionModel.Select)
# set selected indexes text to selection
indexes = selectionMode.selectedIndexes()
for index in indexes:
text = str(index.row()) + str(index.column())
tblModel.setData(index, text, role=Qt.EditRole)
tblView.show()
app.exec()
The behavior is erratic also because you didn't call the base class implementation of mouseReleaseEvent, which does some operations required to correctly update the selection, including deselecting the previously selected items except the current/new one (but the behavior can change according to the view's selectionMode).
Also, consider that the selectionChanged signal of the selection model only emits the changes: if an item was already selected when the selection changes, it will not be listed in the selected list of the signal argument.
In order to access the full list of selected items, you'll need to call the selectedIndexes() of the view, or its selection model.
class myTableView(QTableView):
def __init__(self, model):
super().__init__()
# no need for these
# self.rowCount = rowCount
# self.columnCount = columnCount
# NEVER overwrite existing class property names!
# self.model = model
self.setModel(model)
self.selectionModel().selectionChanged.connect(self.updateSelection)
self.setSelectionMode(QAbstractItemView.ContiguousSelection)
def updateSelection(self, selected, deselected):
selectedIndexes = self.selectedIndexes()
for row in range(model.rowCount()):
for column in range(model.columnCount()):
_index = model.index(row, column)
if _index in selectedIndexes:
_text = f"({_index.row()}, {_index.column()})"
elif _index in deselected:
_text = "previous selection"
else:
_text = ""
model.setData(_index, _text)
I also removed the rowCount and columnCount arguments for the table init, as it's redundant (and prone to errors if you change the model size): their values only depend on the model's own size, and you should access them only through it.
Finally, you should never overwrite existing class attributes; other than the self.model I commented out above, this also goes for self.rowCount and self.columnCount you used in the model (which also doesn't make much sense, as the public methods would return the methods themselves, causing recursion).

background color of cells in QTableview, PySide2

Is it possible to conditionally change the background color of items in a QTableView, using PySide2?
I've read a lot on the model view framework . I cannot figure out if it is necessary to use a Delegate or not. Recently I was able to get a column of checkboxes without a Delegate. I believe that the virtual methods setItemData(index, roles) and itemData(index) could be what I need. However, there is no QMap in PySide2. My model must need somewhere to store the extra information to be used by QtCore.Qt.BackgroundRole (that enum, btw, says "the background brush used for items rendered with the default delegate") If I don't specify a delegate, is the "default delegate" used?. Should I be using QStandardItemModel instead?
In the example code below, how would I get a particular column's background color to be red based on some thresholds (the min and max column are the thresholds?
from PySide2.QtWidgets import (QWidget, QApplication, QTableView,QVBoxLayout)
import sys
from PandasModel2 import PandasModel2
import numpy as np
import pandas as pd
class Example(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(300, 300, 700, 300)
self.setWindowTitle("QTableView")
self.initData()
self.initUI()
def initData(self):
data = pd.DataFrame(np.random.randint(1,10,size=(6,4)), columns=['Test#','MIN', 'MAX','MEASURED'])
data['Test#'] = [1,2,3,4,5,6]
#add the checkable column to the DataFrame
data['Check'] = True
self.model = PandasModel2(data)
def initUI(self):
self.tv = QTableView(self)
self.tv.setModel(self.model)
vbox = QVBoxLayout()
vbox.addWidget(self.tv)
self.setLayout(vbox)
app = QApplication([])
ex = Example()
ex.show()
sys.exit(app.exec_())
And I have a custom model using a pandas dataFrame:
import PySide2.QtCore as QtCore
class PandasModel2(QtCore.QAbstractTableModel):
"""
Class to populate a table view with a pandas dataframe.
This model is non-hierachical.
"""
def __init__(self, data, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = data
def rowCount(self, parent=None):
return self._data.shape[0]
def columnCount(self, parent=None):
return self._data.shape[1]
def data(self, index, role=QtCore.Qt.DisplayRole):
if role==QtCore.Qt.DisplayRole:
if index.column() != 4:
#don't want what determines check state to be shown as a string
if index.isValid():
if index.column() in [1,2,3]:
return '{:.3f}'.format(self._data.iloc[index.row(), index.column()])
if index.column() == 0:
return '{:.2f}'.format(self._data.iloc[index.row(), index.column()])
return str(self._data.iloc[index.row(), index.column()])
if role==QtCore.Qt.CheckStateRole:
if index.column()==4:#had to add this check to get the check boxes only in column 10
if self._data.iloc[index.row(), index.column()] == True:
return QtCore.Qt.Checked
else:
return QtCore.Qt.Unchecked
def getMinimum(self, row):
return self._data.iloc[row, self.getColumnNumber('MIN')]
def getMaximum(self, row):
return self._data.iloc[row, self.getColumnNumber('MAX')]
def getColumnNumber(self, string):
'''
Given a string that identifies a label/column,
return the location of that label/column.
This enables the config file columns to be moved around.
'''
return self._data.columns.get_loc(string)
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self._data.columns[col]
return None
def flags(self, index):
'''
The returned enums indicate which columns are editable, selectable,
checkable, etc.
The index is a QModelIndex.
'''
if index.column() == self.getColumnNumber('Check'):
#print(index.column())
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
else:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
return QtCore.Qt.ItemIsEnabled
def setData(self, index, value, role=QtCore.Qt.DisplayRole):
"""Set the value to the index position depending on Qt::ItemDataRole and data type of the column
Args:
index (QtCore.QModelIndex): Index to define column and row.
value (object): new value.
role (Qt::ItemDataRole): Use this role to specify what you want to do.
Raises:
TypeError: If the value could not be converted to a known datatype.
Returns:
True if value is changed. Calls layoutChanged after update.
False if value is not different from original value.
"""
if not index.isValid():
return False
if role == QtCore.Qt.DisplayRole: #why not edit role?
self._data.iat[index.row(),index.column()]= value
self.layoutChanged.emit()
return True
elif role == (QtCore.Qt.CheckStateRole | QtCore.Qt.DisplayRole):
#this block does get executed when toggling the check boxes,
#verified with debugger. Although the action is the same
#as the block above!
self._data.iat[index.row(),index.column()]= value
self.layoutChanged.emit()
return True
else:
return False
The delegate by default uses the BackgroundRole information if it is available so the solution is just to return a QColor, QBrush or similar.
from PySide2 import QtCore, QtGui
class PandasModel2(QtCore.QAbstractTableModel):
# ...
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return
if not (0 <= index.row() < self.rowCount() and 0 <= index.column() <= self.columnCount()):
return
value = self._data.iloc[index.row(), index.column()]
if role == QtCore.Qt.DisplayRole:
if index.column() != 4:
if index.column() in [1,2,3]:
return '{:.3f}'.format(value)
if index.column() == 0:
return '{:.2f}'.format(value)
return str(value)
elif role == QtCore.Qt.CheckStateRole:
if index.column() == 4:
return QtCore.Qt.Checked if value else QtCore.Qt.Unchecked
elif index.column() == self.getColumnNumber('MEASURED'):
if role == QtCore.Qt.BackgroundRole:
if self.getMinimum(index.row()) <= value <= self.getMaximum(index.row()):
return QtGui.QColor("red")

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 :)

Insert and remove items from a QAbstractListModel

I am trying to create a QAbstractListView for use with a QComboBox which maintains a sorted list of the items it contains. I've included some sample code below that illustrates my problem. When I update the items in the list, the currentIndex of the combo box does not update to reflect the changes to the model. I've tried using the rowsAboutToBeInserted and rowsInserted signals, but I can't see any effect (maybe I'm doing it wrong?).
My actual use case is a little more complex, but the example should be enough. The items being sorted aren't just strings, and take a little more effort to sort and have a ItemDataRole different to their DisplayRole.
The itemsAdded and itemsRemoved are my own functions, which will be connected to signals from another list which I'm trying to proxy.
To trigger the problem, press the 'Insert "c"' button. The string is inserted into the list correctly, but the selection moves from 'e' to 'd' (i.e. the selection index does not change).
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from PyQt4 import QtCore, QtGui
class Model(QtCore.QAbstractListModel):
def __init__(self, *args, **kwargs):
QtCore.QAbstractListModel.__init__(self, *args, **kwargs)
self.items = []
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid() is True:
if role == QtCore.Qt.DisplayRole:
return QtCore.QVariant(self.items[index.row()])
elif role == QtCore.Qt.ItemDataRole:
return QtCore.QVariant(self.items[index.row()])
return QtCore.QVariant()
def itemsAdded(self, items):
# insert items into their sorted position
items = sorted(items)
row = 0
while row < len(self.items) and len(items) > 0:
if items[0] < self.items[row]:
self.items[row:row] = [items.pop(0)]
row += 1
row += 1
# add remaining items to end of list
if len(items) > 0:
self.items.extend(items)
def itemsRemoved(self, items):
# remove items from list
for item in items:
for row in range(0, len(self.items)):
if self.items[row] == item:
self.items.pop(row)
break
def main():
app = QtGui.QApplication([])
w = QtGui.QWidget()
w.resize(300,300)
layout = QtGui.QVBoxLayout()
model = Model()
model.itemsAdded(['a','b','d','e'])
combobox = QtGui.QComboBox()
combobox.setModel(model)
combobox.setCurrentIndex(3)
layout.addWidget(combobox)
def insertC(self):
model.itemsAdded('c')
button = QtGui.QPushButton('Insert "c"')
button.clicked.connect(insertC)
layout.addWidget(button)
w.setLayout(layout)
w.show()
app.exec_()
if __name__ == '__main__':
main()
A complete working example below, based on Tim's answer.
The call to setCurrentIndex isn't required. The view keeps track of this automatically when insertRows/removeRows are called correctly.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from PyQt4 import QtCore, QtGui
class Model(QtCore.QAbstractListModel):
def __init__(self, *args, **kwargs):
QtCore.QAbstractListModel.__init__(self, *args, **kwargs)
self.items = []
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid() is True:
if role == QtCore.Qt.DisplayRole:
return QtCore.QVariant(self.items[index.row()])
elif role == QtCore.Qt.ItemDataRole:
return QtCore.QVariant(self.items[index.row()])
return QtCore.QVariant()
def itemsAdded(self, items):
# insert items into their sorted position
items = sorted(items)
row = 0
while row < len(self.items) and len(items) > 0:
if items[0] < self.items[row]:
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.items.insert(row, items.pop(0))
self.endInsertRows()
row += 1
row += 1
# add remaining items to end of the list
if len(items) > 0:
self.beginInsertRows(QtCore.QModelIndex(), len(self.items), len(self.items) + len(items) - 1)
self.items.extend(items)
self.endInsertRows()
def itemsRemoved(self, items):
# remove items from the list
for item in items:
for row in range(0, len(self.items)):
if self.items[row] == item:
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self.items.pop(row)
self.endRemoveRows()
break
def main():
app = QtGui.QApplication([])
w = QtGui.QWidget()
w.resize(300,200)
layout = QtGui.QVBoxLayout()
model = Model()
model.itemsAdded(['a','b','d','e'])
combobox = QtGui.QComboBox()
combobox.setModel(model)
combobox.setCurrentIndex(3)
layout.addWidget(combobox)
def insertC(self):
model.itemsAdded('c')
def removeC(self):
model.itemsRemoved('c')
buttonInsert = QtGui.QPushButton('Insert "c"')
buttonInsert.clicked.connect(insertC)
layout.addWidget(buttonInsert)
buttonRemove = QtGui.QPushButton('Remove "c"')
buttonRemove.clicked.connect(removeC)
layout.addWidget(buttonRemove)
w.setLayout(layout)
w.show()
app.exec_()
if __name__ == '__main__':
main()
I guess you need to modify the selection index yourself, i.e. something like
if row < currentIndex():
setCurrentIndex( currentIndex() + 1 );
You should however read the following passage:
Models that provide interfaces to resizable list-like data structures can provide implementations of insertRows() and removeRows(). When implementing these functions, it is important to call the appropriate functions so that all connected views are aware of any changes:
• An insertRows() implementation must call beginInsertRows() before inserting new rows into the data structure, and it must call endInsertRows() immediately afterwards.
• A removeRows() implementation must call beginRemoveRows() before the rows are removed from the data structure, and it must call endRemoveRows() immediately afterwards.

pyQt v4 rowsMoved Signal

Im pretty new to Qt and have been trying to create a UI where the user will have rows of information and each row represents a stage in a pipeline. Iam trying to achieve that a user can drag and drop the different rows and that will change the order in which the steps occur.
I have achieved the drag and drop of the rows using :
self.tableView.verticalHeader().setMovable(True)
Iam now trying to get the Signal"rowsMoved" to work but cant seem to get it to work in my custom model and delegate. If anyone knows of a way to get this working or of a way to not use this Signla and use another signal to track which row has moved and where it is now moved to. It will be a great help! :)
Thanks everyone
CODE BELOW
class pipelineModel( QAbstractTableModel ):
def __init_( self ):
super( pipelineModel, self ).__init__()
self.stages = []
# Sets up the population of information in the Model
def data( self, index, role=Qt.DisplayRole ):
if (not index.isValid() or not (0 <= index.row() < len(self.stages) ) ):
return QVariant()
column = index.column()
stage = self.stages[ index.row() ] # Retrieves the object from the list using the row count.
if role == Qt.DisplayRole: # If the role is a display role, setup the display information in each cell for the stage that has just been retrieved
if column == NAME:
return QVariant(stage.name)
if column == ID:
return QVariant(stage.id)
if column == PREV:
return QVariant(stage.prev)
if column == NEXT:
return QVariant(stage.next)
if column == TYPE:
return QVariant(stage.assetType)
if column == DEPARTMENT:
return QVariant(stage.depID)
if column == EXPORT:
return QVariant(stage.export)
if column == PREFIX:
return QVariant(stage.prefix)
if column == DELETE:
return QVariant(stage.delete)
elif role == Qt.TextAlignmentRole:
pass
elif role == Qt.TextColorRole:
pass
elif role == Qt.BackgroundColorRole:
pass
return QVariant()
# Sets up the header information for the table
def headerData( self, section, orientation, role = Qt.DisplayRole ):
if role == Qt.TextAlignmentRole:
if orientation == Qt.Horizontal:
return QVariant( int(Qt.AlignLeft|Qt.AlignVCenter))
return QVariant( int(Qt.AlignRight|Qt.AlignVCenter))
if role != Qt.DisplayRole:
return QVariant()
if orientation == Qt.Horizontal: # If Orientation is horizontal then we populate the headings
if section == ID:
return QVariant("ID")
elif section == PREV:
return QVariant("Previouse")
elif section == NEXT:
return QVariant("Next")
elif section == NAME:
return QVariant("Name")
elif section == TYPE:
return QVariant("Type")
elif section == DEPARTMENT:
return QVariant("Department")
elif section == EXPORT:
return QVariant("Export Model")
elif section == PREFIX:
return QVariant("Prefix")
elif section == DELETE:
return QVariant("Delete")
return QVariant( int( section + 1 ) ) # Creates the Numbers Down the Vertical Side
# Sets up the amount of Rows they are
def rowCount( self, index = QModelIndex() ):
count = 0
try:
count = len( self.stages )
except:
pass
return count
# Sets up the amount of columns they are
def columnCount( self, index = QModelIndex() ):
return 9
def rowsMoved( self, row, oldIndex, newIndex ):
print 'ASDASDSA'
# ---------MAIN AREA---------
class pipeline( QDialog ):
def __init__(self, parent = None):
super(pipeline, self).__init__(parent)
self.stages = self.getDBinfo() # gets the stages from the DB and return them as a list of objects
tableLabel = QLabel("Testing Table - Custom Model + Custom Delegate")
self.tableView = QTableView() # Creates a Table View (for now we are using the default one and not creating our own)
self.tableDelegate = pipelineDelegate()
self.tableModel = pipelineModel()
tableLabel.setBuddy( self.tableView )
self.tableView.setModel( self.tableModel )
# self.tableView.setItemDelegate( self.tableDelegate )
layout = QVBoxLayout()
layout.addWidget(self.tableView)
self.setLayout(layout)
self.tableView.verticalHeader().setMovable(True)
self.connect(self.tableModel, SIGNAL("rowsMoved( )"), self.MovedRow) # trying to setup an on moved signal, need to check threw the available slots
# self.connect(self.tableModel, SIGNAL(" rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex ,int)"), self.MovedRow) # trying to setup an on moved signal, need to check threw the available slots
Note: Components connected to this signal use it to adapt to changes in the model's dimensions. It can only be emitted by the QAbstractItemModel implementation, and cannot be explicitly emitted in subclass code.
What you want is to subclass the QTableView, and overload the rowMoved() SLOT
class MyTableView(QtGui.QTableView):
def __init__(self, parent=None):
super(MyTableView, self).__init__(parent)
self.rowsMoved.connect(self.movedRowsSlot)
def rowMoved(self, row, oldIndex, newIndex):
# do something with these or
super(MyTableView, self).rowMoved(row, oldIndex, newIndex)
def movedRowsSlot(self, *args):
print "Moved rows!", args
Edited To show both overloading rowMoved slot, and using rowsMoved signal

Categories