Related
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.
I'm using Pyside2, Python 3.8
I have a QTableView, with a QSortFilterProxyModel Model. I managed to sort my rows on a single column. What I want to achieve is sort myTableView on the column 3 (contains String data), then on column 2 (contains Bool data) and then on Column 4 (contains integer data). See the below picture for an example
I've been trying about different why to do this, It seems that the hack my be in the lessThan() method, but It's very confusing to me.
Can someone give me a hint on how should I proceed?
Here's some samples of my code, If it helps any one.
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
ProxyModel = ProxyModel()
TableModel = TableModel()
ProxyModel.setSourceModel(TableModel)
self.MyTableView.setModel(ProxyModel)
class ProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self,parent=None):
super(ProxyModel, self).__init__()
self._filter = "Aucun"
def filterAcceptsRow(self, sourceRow, sourceParent):
if self._filter == "Aucun": return True
sourceModel = self.sourceModel()
id = sourceModel.index(sourceRow, self.filterKeyColumn(), sourceParent)
if sourceModel.data(id) == self._filter:
return True
return False
def lessThan(self, left, right):
print(left.row(), ' vs ',right.row())
if left.column() == 3:
leftData = int(self.sourceModel().data(left))
rightData = int(self.sourceModel().data(right))
if left.column() == 2:
leftData = str(self.sourceModel().data(left))
rightData = str(self.sourceModel().data(right))
return leftData < rightData
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, mlist=None):
super(TableModel, self).__init__()
self._items = [] if mlist == None else mlist
self._header = []
def rowCount(self, parent = QtCore.QModelIndex):
return len(self._items)
def columnCount(self, parent = QtCore.QModelIndex):
return len(self._header)
def data(self, index, role = QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self._items[index.row()][index.column()]
return None
def setData(self, index, value, role = QtCore.Qt.EditRole):
if value is not None and role == QtCore.Qt.EditRole:
self._items[index.row()-1][index.column()] = value
self.dataChanged.emit(index, index)
return True
return False
def addRow(self, rowObject):
row = self.rowCount()
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self._items.append(rowObject)
self.endInsertRows()
self.layoutChanged.emit()
QSortFilterProxyModel calls lessThan only once for each row pair with indexes set to index(left_row, sort_column) and index(right_row, sort_column), so implementation must take this into account. Ignore column part and access columns you interested in.
def lessThan(self, left, right):
row1 = left.row()
row2 = right.row()
model = left.model()
for col in [3,2,4]:
a = model.data(model.index(row1, col))
b = model.data(model.index(row2, col))
if a < b:
return True
elif a > b:
return False
return True
I am trying to develop a GUI to managed scientific results. For this, I would like to present result from two datasets. The user would have a visual reprentation to help him to compare those results in two QTableView.
Image : comparer Result,
I want to link lines from the two table, so they always be present face to face.
When order change in one table, the other will follow and adapt its order to still have the linked lines face to face.
Eventually i would like to add empty row face to a line that haven't a relative line in the other table.
I was think to use a QSortFilterProxyModel but I am not sure how to use it.
Edit
My Question seem to be not clear. I Formulate here. I find myself a solution so here a example of what I was looking.
On this example line I link line according to the name (bla, bli blo, blu). We see on the same line, tables present face to face line of result "bla" and "bli", because there are in both left model and right.
There is no "blu" in the right table. so i add a empty line.
Idem in the left table with "blo"
In this example item are sorted with "configuration" of the right table. Left table have to follow the order choose by the right table.
Here my code without the solution
class Result(object):
def __init__(self, headerOrder, infoResult):
for n, headerName in enumerate(headerOrder):
self.__setattr__(headerName, infoResult[n])
self.diff = self.reference_redshift - self.estimate
ModelClass
class Result_model(QtCore.QAbstractTableModel):
def __init__(self, header, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.__datas = []
self.__headers = header
def rowCount(self, parent=None):
return len(self.__datas)
def columnCount(self, parent=None):
return len(self.__headers)
def data(self, index, role):
if role == QtCore.Qt.ToolTipRole:
row = index.row()
column = index.column()
return "{}: {}".format(self.__headers[column], getattr(self.__datas[row], self.__headers[column]))
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
value = getattr(self.__datas[row], self.__headers[column])
return value
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
if section < len(self.__headers):
return self.__headers[section]
else:
return "not implemented"
else:
return section
def supportedDragActions(self):
return QtCore.Qt.CopyAction
def supportedDropActions(self):
return Qt.CopyAction | Qt.MoveAction
def getResult(self, index):
row = index.row()
return self.__datas[row]
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
self.emit(QtCore.SIGNAL("layoutAboutToBeChanged()"))
attribut = self.__headers[Ncol]
self.__datas = sorted(
self.__datas, key=lambda x: getattr(x, attribut), reverse=(order == QtCore.Qt.DescendingOrder))
self.emit(QtCore.SIGNAL("layoutChanged()"))
def addResults(self, results):
self.beginInsertRows(QtCore.QModelIndex(), len(
self.__datas), len(self.__datas) + len(results))
for res in results:
self.__datas.append(res)
self.endInsertRows()
TableView only Drag
class TableResult(QtGui.QTableView):
def __init__(self, parent=None):
QtGui.QTableView.__init__(self, parent)
self.setDragEnabled(True)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.header, self.aid_index = [["aid", "estimate", "reference_redshift", "diff", "amazed_executable_id", "amazed_configuration_id",
"astronomical_object_name", "star_forming_rate", "magnitude", "log_f_halpha", "emission_velocity_dispersion", "res_dir"], 0]
self.tag_info_result = ["aid", "estimate", "reference_redshift", "amazed_executable_id", "amazed_configuration_id",
"astronomical_object_name", "star_forming_rate", "magnitude", "log_f_halpha", "emission_velocity_dispersion", "res_dir"]
self.setItemDelegateForColumn(self.aid_index, ButtonDelegate(self))
self.parent = parent
def startDrag(self, dropAction):
if(self.parent is not None and hasattr(self.parent, "selection")):
# create mime data object
mime = QtCore.QMimeData()
# start drag
drag = QtGui.QDrag(self)
drag.setMimeData(mime)
drag.start(QtCore.Qt.CopyAction | QtCore.Qt.CopyAction)
else:
print("Drag impossible")
def mouseMoveEvent(self, event):
self.startDrag(event)
TableView with drop
class Selection_receiver(TableResult):
"Add the drop possibility from TableResult"
def __init__(self, setResultFunction, parent=None):
TableResult.__init__(self, parent)
self.setAcceptDrops(True)
self.setResultFunction = setResultFunction
def dragEnterEvent(self, event):
if (isinstance(event.source(), TableResult)):
event.accept()
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event):
print("dropEvent")
if (isinstance(event.source(), TableResult)):
event.acceptProposedAction()
model_result = event.source().parent.resModel
self.setResultFunction(model_result)
else:
event.ignore()
The Widget presenting the two Tables
class Comparater_result_widget(QtGui.QWidget):
"""
Present two table for easy comparaison.
"""
def __init__(self,parent=None):
super(self.__class__, self).__init__(parent)
self.setWindowTitle("Result Comparer")
main_layout = QtGui.QVBoxLayout()
receiverSplitter = QtGui.QSplitter()
receiverSplitter.setOrientation(QtCore.Qt.Horizontal)
self.left_receiver = Selection_receiver(self.setLeftResult)
receiverSplitter.addWidget(self.left_receiver)
self.right_receiver = Selection_receiver( self.setRightResult)
receiverSplitter.addWidget(self.right_receiver)
main_layout.addWidget(receiverSplitter)
self.left_receiver.horizontalScrollBar().valueChanged.connect(
self.right_receiver.horizontalScrollBar().setValue)
self.right_receiver.horizontalScrollBar().valueChanged.connect(
self.left_receiver.horizontalScrollBar().setValue)
self.left_receiver.verticalScrollBar().valueChanged.connect(
self.right_receiver.verticalScrollBar().setValue)
self.right_receiver.verticalScrollBar().valueChanged.connect(
self.left_receiver.verticalScrollBar().setValue)
self.right_results = None
self.left_results = None
self.setLayout(main_layout)
def setLeftResult(self, model_result):
print("setLeftResult []".format(model_result))
self.left_results = model_result
self.add_model_result(self.left_receiver, model_result)
def setRightResult(self, model_result):
print("setRightResult {}".format(model_result))
self.right_results = model_result
self.add_model_result(self.right_receiver, model_result)
def add_model_result(self, receiver, model_result):
receiver.setModel(model_result)
if(self.right_results is not None and self.left_results is not None):
self.link_result()
def link_result(self):
# parse the two model and link results if the have equal on one
# particular attribut
pass
def OnLeftChangeOrder(self):
# somthing like right_proxy.reorder(left_order)
pass
def OnRightChangeOrder(self):
# something link left_proxy.reorder(right_order)
pass
Here is my solution i created.
I use Two Proxy:
my own mapping proxy model ("MixRowProxyModel")
a almost normal QSortFilterProxyModel ("MySortProxyModel")
Source model is the model of MixRowProxyModel.
MixRowProxyModel is the model of MySortProxyModel
Table can be in two state, master or slave. If I sort the right table at one column, the right become master and the left is slave.
When a table is master, its MySortProxyModel is active, MixRowProxyModel is inactive
When a table is slave, its MySortProxyModel is inactive, MixRowProxyModel is active
When the two source model are set. I create a map between rows of the sources models. This map will not change when table are sorted.
When a table become slave i construct the mapping for its MixRowProxyModel (see method change in Comparater_result_widget). For that i use the initial mapping i created between modelsource.
class MixRowProxyModel(QtGui.QAbstractProxyModel):
def __init__(self, controller, side, parent=None):
QtGui.QAbstractProxyModel.__init__(self, parent=parent)
self.controller = controller
self.nbEmpty = 0
self.mapProxyToSource = None
self.isMaster = True
self.side = side
def setMap(self, mapping):
self.isMaster = False
self.mapProxyToSource = mapping
self.mapSourceToProxy = {v: k for k,
v in self.mapProxyToSource.items()}
def mapFromSource(self, sourceIndex):
#print("MixRowProxyModel Proxy Index model {}".format(sourceIndex.model()))
if(not sourceIndex.isValid()):
return self.index(-1, -1)
if(self.isMaster):
return self.index(sourceIndex.row(), sourceIndex.column(), parent=QtCore.QModelIndex)
else:
row = sourceIndex.row()
if(row in self.mapSourceToProxy):
return self.index(self.mapSourceToProxy[row], sourceIndex.column())
else:
print("Invalid sourceIndex {}".format(row))
return self.index(-1, -1)
def mapToSource(self, proxyIndex):
if(not proxyIndex.isValid()):
return self.sourceModel().index(-1, -1)
if(self.isMaster):
return self.sourceModel().index(proxyIndex.row(), proxyIndex.column())
else:
row = proxyIndex.row()
if(row in self.mapProxyToSource):
return self.sourceModel().index(self.mapProxyToSource[row], proxyIndex.column())
else:
# print("Invalid proxyIndex {}".format(row))
return self.sourceModel().index(-1, -1)
def rowCount(self, parent=None):
return self.sourceModel().rowCount() + self.nbEmpty
def columnCount(self, parent=None):
return self.sourceModel().columnCount()
def addEmptyRow(self):
print("addEmptyRow {}".format(self.side))
self.beginInsertRows(QtCore.QModelIndex(),
self.rowCount(), self.rowCount())
self.nbEmpty += 1
self.endInsertRows()
return -1
def parent(self, index):
return QtCore.QModelIndex()
def index(self, row, column, parent=QtCore.QModelIndex):
if(row >= self.rowCount() or row < 0 or column < 0 or column >= self.columnCount()):
return QtCore.QModelIndex()
return self.createIndex(row, column, parent)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self.sourceModel().headerData(section, orientation, role)
else:
return section
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def data(self, index, role=QtCore.Qt.DisplayRole):
if(not index.isValid()):
return None
if(self.isMaster):
if(index.row() < self.sourceModel().rowCount()):
return self.sourceModel().data(index, role)
else:
return None
else:
if(not index.row() in self.mapProxyToSource):
return None
return self.sourceModel().data(self.sourceModel().index(self.mapProxyToSource[index.row()], index.column()), role)
the second ProxyModel
class MySortProxyModel(QtGui.QSortFilterProxyModel):
sortedSignal = QtCore.pyqtSignal(QtCore.Qt.SortOrder)
def sort(self, column, order):
self.sourceModel().isMaster = True
#print("Sort order : {} sort Role {}".format(order, self.sortRole()))
self.setSortRole(QtCore.Qt.DisplayRole)
super().sort(column, order)
self.sortedSignal.emit(order)
the Parent Widget controlling the two tables
class Comparater_result_widget(QtGui.QWidget):
def __init__(self, controller, parent=None):
super(self.__class__, self).__init__(parent)
self.controller = controller
self.setWindowTitle("Result Comparer")
main_layout = QtGui.QVBoxLayout()
receiverSplitter = QtGui.QSplitter()
receiverSplitter.setOrientation(QtCore.Qt.Horizontal)
self.left_receiver = Selection_receiver(
self.controller, self.setLeftResult)
receiverSplitter.addWidget(self.left_receiver)
self.right_receiver = Selection_receiver(
self.controller, self.setRightResult)
receiverSplitter.addWidget(self.right_receiver)
main_layout.addWidget(receiverSplitter)
self.left_receiver.horizontalScrollBar().valueChanged.connect(
self.right_receiver.horizontalScrollBar().setValue)
self.right_receiver.horizontalScrollBar().valueChanged.connect(
self.left_receiver.horizontalScrollBar().setValue)
self.left_receiver.verticalScrollBar().valueChanged.connect(
self.right_receiver.verticalScrollBar().setValue)
self.right_receiver.verticalScrollBar().valueChanged.connect(
self.left_receiver.verticalScrollBar().setValue)
self.right_source_model = None
self.right_proxy_model = None
self.right_proxy_model2 = None
self.left_source_model = None
self.left_proxy_model = None
self.left_proxy_model2 = None
self.setLayout(main_layout)
def setLeftResult(self, model_result):
self.left_source_model = model_result
self.left_proxy_model = MixRowProxyModel(
self.controller, "left", parent=self)
# self.left_proxy_model.sortedSignal.connect(self.onLeftChange)
self.left_proxy_model.setSourceModel(self.left_source_model)
self.left_proxy_model2 = MySortProxyModel(parent=self)
self.left_proxy_model2.sortedSignal.connect(self.onLeftChange)
self.left_proxy_model2.setSourceModel(self.left_proxy_model)
self.add_model_result(self.left_receiver, self.left_proxy_model2)
def setRightResult(self, model_result):
self.right_source_model = model_result
self.right_proxy_model = MixRowProxyModel(
self.controller, "right", parent=self)
self.right_proxy_model.setSourceModel(self.right_source_model)
# self.right_proxy_model.sortedSignal.connect(self.onRightChange)
self.right_proxy_model2 = MySortProxyModel(parent=self)
self.right_proxy_model2.sortedSignal.connect(self.onRightChange)
self.right_proxy_model2.setSourceModel(self.right_proxy_model)
self.add_model_result(self.right_receiver, self.right_proxy_model2)
def add_model_result(self, receiver, model_result):
receiver.setModel(model_result)
if(self.right_source_model is not None and self.left_source_model is not None):
self.link_result()
def link_result(self):
name_to_row = {}
for numSourceLeftRow in range(self.left_source_model.rowCount()):
res = self.left_source_model.getResultNumRow(numSourceLeftRow)
name = res.astronomical_object_name
if(name in name_to_row):
name_to_row[name][0].append(numSourceLeftRow)
else:
name_to_row[name] = ([numSourceLeftRow], [])
for numSourceRightRow in range(self.right_source_model.rowCount()):
res = self.right_source_model.getResultNumRow(numSourceRightRow)
name = res.astronomical_object_name
if(name in name_to_row):
name_to_row[name][1].append(numSourceRightRow)
else:
name_to_row[name] = ([], [numSourceRightRow])
self.mapLeftToRight = {} # key = leftRow; value = rightRow
self.mapRightToLeft = {} # key = rightRow; value = leftRow
for list_leftRow, list_rightRow in name_to_row.values():
if(len(list_rightRow) > 1):
print(
"Error more that index at right for same astronomical name {}".list_rightRow)
if(len(list_leftRow) > 1):
print(
"Error more that index at left for same astronomical name {}".list_leftRow)
if(len(list_leftRow) == 0):
leftRow = self.left_proxy_model.addEmptyRow()
else:
leftRow = list_leftRow[0]
if(len(list_rightRow) == 0):
rightRow = self.right_proxy_model.addEmptyRow()
else:
rightRow = list_rightRow[0]
self.mapLeftToRight[leftRow] = rightRow
self.mapRightToLeft[rightRow] = leftRow
self.left_receiver.rowCountChanged(
self.left_source_model.rowCount(), self.left_proxy_model.rowCount())
self.right_receiver.rowCountChanged(
self.right_source_model.rowCount(), self.right_proxy_model.rowCount())
print("Link Done : LtoR : {}; RtoL {}".format(
self.mapLeftToRight, self.mapRightToLeft))
def onRightChange(self, order):
print("RightChange")
self.change(self.left_source_model, self.left_proxy_model, self.left_proxy_model2, self.right_source_model, self.right_proxy_model,
self.right_proxy_model2, self.mapLeftToRight, self.left_receiver, order)
def onLeftChange(self, order):
print("LeftChange")
self.change(self.right_source_model, self.right_proxy_model, self.right_proxy_model2, self.left_source_model, self.left_proxy_model,
self.left_proxy_model2, self.mapRightToLeft, self.right_receiver, order)
def change(self, slave_source_model, slave_proxy_model,
slave_proxy_model2, master_source_model, master_proxy_model, master_proxy_model2,
map_slave_to_master, slave_receiver, order):
if(slave_source_model is not None):
slaveMapping = dict() # in slave table key = indexProxy , value = index Source
if(order == QtCore.Qt.AscendingOrder):
unlinkIndex = 0
else:
unlinkIndex = master_source_model.rowCount()
for slaveSourceRow in range(slave_source_model.rowCount()):
# this line is link to one in master, so we keep the same
# proxy number
master_source_row = map_slave_to_master[slaveSourceRow]
if(master_source_row != -1):
master_source_index = master_source_model.index(
master_source_row, 0)
master_proxy = master_proxy_model.mapFromSource(
master_source_index)
master_proxy2 = master_proxy_model2.mapFromSource(
master_proxy)
slaveProxyRow = master_proxy2.row() # same as master
else:
slaveProxyRow = unlinkIndex # we put it at the end or begining depending on order
unlinkIndex += 1
slaveMapping[slaveProxyRow] = slaveSourceRow
slave_proxy_model.layoutAboutToBeChanged.emit()
slave_proxy_model.setMap(slaveMapping)
slave_proxy_model2.setSortRole(
QtCore.Qt.InitialSortOrderRole) # proxy 2 is reinitialise
slave_proxy_model.layoutChanged.emit()
I am trying to have a QTableView of checkboxes, so I can use them for row selections... I have managed to do that, now I want the header Itself to be checkbox so I can check/Uncheck All or any row . I have been looking for days, but couldn't get to do it.
I tried to use setHeaderData to the model, but couldn't do it.
Any help would be appreciated.
I wasn't particularly happy with the C++ version that #tmoreau ported to Python as it didn't:
handle more than one column
handle custom header heights (for example multi-line header text)
use a tri-state checkbox
work with sorting
So I fixed all of those issues, and created an example with a QStandardItemModel which I generally would advocate over trying to create your own model based on QAbstractTableModel.
There are probably still some imperfections, so I welcome suggestions for how to improve it!
import sys
from PyQt4 import QtCore, QtGui
# A Header supporting checkboxes to the left of the text of a subset of columns
# The subset of columns is specified by a list of column_indices at
# instantiation time
class CheckBoxHeader(QtGui.QHeaderView):
clicked=QtCore.pyqtSignal(int, bool)
_x_offset = 3
_y_offset = 0 # This value is calculated later, based on the height of the paint rect
_width = 20
_height = 20
def __init__(self, column_indices, orientation = QtCore.Qt.Horizontal, parent = None):
super(CheckBoxHeader, self).__init__(orientation, parent)
self.setResizeMode(QtGui.QHeaderView.Stretch)
self.setClickable(True)
if isinstance(column_indices, list) or isinstance(column_indices, tuple):
self.column_indices = column_indices
elif isinstance(column_indices, (int, long)):
self.column_indices = [column_indices]
else:
raise RuntimeError('column_indices must be a list, tuple or integer')
self.isChecked = {}
for column in self.column_indices:
self.isChecked[column] = 0
def paintSection(self, painter, rect, logicalIndex):
painter.save()
super(CheckBoxHeader, self).paintSection(painter, rect, logicalIndex)
painter.restore()
#
self._y_offset = int((rect.height()-self._width)/2.)
if logicalIndex in self.column_indices:
option = QtGui.QStyleOptionButton()
option.rect = QtCore.QRect(rect.x() + self._x_offset, rect.y() + self._y_offset, self._width, self._height)
option.state = QtGui.QStyle.State_Enabled | QtGui.QStyle.State_Active
if self.isChecked[logicalIndex] == 2:
option.state |= QtGui.QStyle.State_NoChange
elif self.isChecked[logicalIndex]:
option.state |= QtGui.QStyle.State_On
else:
option.state |= QtGui.QStyle.State_Off
self.style().drawControl(QtGui.QStyle.CE_CheckBox,option,painter)
def updateCheckState(self, index, state):
self.isChecked[index] = state
self.viewport().update()
def mousePressEvent(self, event):
index = self.logicalIndexAt(event.pos())
if 0 <= index < self.count():
x = self.sectionPosition(index)
if x + self._x_offset < event.pos().x() < x + self._x_offset + self._width and self._y_offset < event.pos().y() < self._y_offset + self._height:
if self.isChecked[index] == 1:
self.isChecked[index] = 0
else:
self.isChecked[index] = 1
self.clicked.emit(index, self.isChecked[index])
self.viewport().update()
else:
super(CheckBoxHeader, self).mousePressEvent(event)
else:
super(CheckBoxHeader, self).mousePressEvent(event)
if __name__=='__main__':
def updateModel(index, state):
for i in range(model.rowCount()):
item = model.item(i, index)
item.setCheckState(QtCore.Qt.Checked if state else QtCore.Qt.Unchecked)
def modelChanged():
for i in range(model.columnCount()):
checked = 0
unchecked = 0
for j in range(model.rowCount()):
if model.item(j,i).checkState() == QtCore.Qt.Checked:
checked += 1
elif model.item(j,i).checkState() == QtCore.Qt.Unchecked:
unchecked += 1
if checked and unchecked:
header.updateCheckState(i, 2)
elif checked:
header.updateCheckState(i, 1)
else:
header.updateCheckState(i, 0)
app = QtGui.QApplication(sys.argv)
tableView = QtGui.QTableView()
model = QtGui.QStandardItemModel()
model.itemChanged.connect(modelChanged)
model.setHorizontalHeaderLabels(['Title 1\nA Second Line','Title 2'])
header = CheckBoxHeader([0,1], parent = tableView)
header.clicked.connect(updateModel)
# populate the models with some items
for i in range(3):
item1 = QtGui.QStandardItem('Item %d'%i)
item1.setCheckable(True)
item2 = QtGui.QStandardItem('Another Checkbox %d'%i)
item2.setCheckable(True)
model.appendRow([item1, item2])
tableView.setModel(model)
tableView.setHorizontalHeader(header)
tableView.setSortingEnabled(True)
tableView.show()
sys.exit(app.exec_())
I had the same issue, and find a solution here, in C++. There is no easy solution, you have to create your own header.
Here's my full code with PyQt4. It seems to work with Python2 and Python3.
I also implemented the select all / select none functionality.
import sys
import signal
#import QT
from PyQt4 import QtCore,QtGui
#---------------------------------------------------------------------------------------------------------
# Custom checkbox header
#---------------------------------------------------------------------------------------------------------
#Draw a CheckBox to the left of the first column
#Emit clicked when checked/unchecked
class CheckBoxHeader(QtGui.QHeaderView):
clicked=QtCore.pyqtSignal(bool)
def __init__(self,orientation=QtCore.Qt.Horizontal,parent=None):
super(CheckBoxHeader,self).__init__(orientation,parent)
self.setResizeMode(QtGui.QHeaderView.Stretch)
self.isChecked=False
def paintSection(self,painter,rect,logicalIndex):
painter.save()
super(CheckBoxHeader,self).paintSection(painter,rect,logicalIndex)
painter.restore()
if logicalIndex==0:
option=QtGui.QStyleOptionButton()
option.rect= QtCore.QRect(3,1,20,20) #may have to be adapt
option.state=QtGui.QStyle.State_Enabled | QtGui.QStyle.State_Active
if self.isChecked:
option.state|=QtGui.QStyle.State_On
else:
option.state|=QtGui.QStyle.State_Off
self.style().drawControl(QtGui.QStyle.CE_CheckBox,option,painter)
def mousePressEvent(self,event):
if self.isChecked:
self.isChecked=False
else:
self.isChecked=True
self.clicked.emit(self.isChecked)
self.viewport().update()
#---------------------------------------------------------------------------------------------------------
# Table Model, with checkBoxed on the left
#---------------------------------------------------------------------------------------------------------
#On row in the table
class RowObject(object):
def __init__(self):
self.col0="column 0"
self.col1="column 1"
class Model(QtCore.QAbstractTableModel):
def __init__(self,parent=None):
super(Model,self).__init__(parent)
#Model= list of object
self.myList=[RowObject(),RowObject()]
#Keep track of which object are checked
self.checkList=[]
def rowCount(self,QModelIndex):
return len(self.myList)
def columnCount(self,QModelIndex):
return 2
def addOneRow(self,rowObject):
frow=len(self.myList)
self.beginInsertRows(QtCore.QModelIndex(),row,row)
self.myList.append(rowObject)
self.endInsertRows()
def data(self,index,role):
row=index.row()
col=index.column()
if role==QtCore.Qt.DisplayRole:
if col==0:
return self.myList[row].col0
if col==1:
return self.myList[row].col1
elif role==QtCore.Qt.CheckStateRole:
if col==0:
if self.myList[row] in self.checkList:
return QtCore.Qt.Checked
else:
return QtCore.Qt.Unchecked
def setData(self,index,value,role):
row=index.row()
col=index.column()
if role==QtCore.Qt.CheckStateRole and col==0:
rowObject=self.myList[row]
if rowObject in self.checkList:
self.checkList.remove(rowObject)
else:
self.checkList.append(rowObject)
index=self.index(row,col+1)
self.dataChanged.emit(index,index)
return True
def flags(self,index):
if index.column()==0:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable
return QtCore.Qt.ItemIsEnabled
def headerData(self,section,orientation,role):
if role==QtCore.Qt.DisplayRole:
if orientation==QtCore.Qt.Horizontal:
if section==0:
return "Title 1"
elif section==1:
return "Title 2"
def headerClick(self,isCheck):
self.beginResetModel()
if isCheck:
self.checkList=self.myList[:]
else:
self.checkList=[]
self.endResetModel()
if __name__=='__main__':
app=QtGui.QApplication(sys.argv)
#to be able to close with ctrl+c
signal.signal(signal.SIGINT, signal.SIG_DFL)
tableView=QtGui.QTableView()
model=Model(parent=tableView)
header=CheckBoxHeader(parent=tableView)
header.clicked.connect(model.headerClick)
tableView.setModel(model)
tableView.setHorizontalHeader(header)
tableView.show()
sys.exit(app.exec_())
NB: You could store the rows in self.checkList. In my case, I often have to delete rows in random position so it was not sufficient.
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