Visual position of item in QTableView - python

I am working with PyQt5/PySide2. I Have a QTableView with QSortFilterProxyModel and the data is handled by QStandardItemModel.
I am using QStandardItemModel.findItems() method to find some cells in the first table row. The result is a list of QStandardItems. Now I want to order these items by row in which they are displayed in GUI table i.e. the way that user sees them. Is there some way to archive this?. To translate proxy or model indices to to "view" indices.
I tought this could be done using QSortFilterProxyModel.mapFromSource() method but it appears the proxy indices do not have the desired order.
here is a minimal reproducible example written in PyQt5:
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from collections import deque
from random import randint
class Splash(QWidget):
def __init__(self):
super().__init__()
# create model
self.model = QStandardItemModel(self)
self.model.setHorizontalHeaderLabels(["column 1", "column 2"])
# create sort proxy
self.proxy = NumberSortModel()
self.proxy.setSourceModel(self.model)
# create view
self.table = CustomQTableView(self)
self.table.setGeometry(0, 0, 275, 575)
self.table.setModel(self.proxy)
self.table.setSortingEnabled(True)
# create buttons
button = QPushButton('Find cells containing 1', self)
button.move(300, 70)
button.clicked.connect(lambda: self.table.search_string("1"))
button1 = QPushButton('next', self)
button1.move(300, 100)
button1.clicked.connect(self.table._search_next)
button2 = QPushButton('previous', self)
button2.move(300, 130)
button2.clicked.connect(self.table._search_previous)
# fill model
for i in range(15):
self.model.appendRow([QStandardItem(str(i)),
QStandardItem(str(randint(1, 100)))])
self.show()
# takes care of the coloring of results
class _HighlightDelegate(QStyledItemDelegate):
def __init__(self, parent=None) -> None:
QStyledItemDelegate.__init__(self, parent)
self._parent = parent
def paint(self, painter: "QPainter", option: "QStyleOptionViewItem",
index: "QModelIndex"):
painter.save()
if len(self._parent.proxy_indices) > 0:
if index == self._parent.proxy_indices[0]:
painter.fillRect(option.rect, Qt.red)
elif index in self._parent.proxy_indices:
painter.fillRect(option.rect, option.palette.highlight())
else:
if (option.state & QStyle.State_Selected):
painter.fillRect(option.rect, option.palette.highlight())
elif (option.state & QStyle.State_None):
painter.fillRect(option.rect, option.palette.base())
painter.drawText(option.rect, Qt.AlignLeft, index.data(Qt.DisplayRole))
painter.restore()
class CustomQTableView(QTableView):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.real_indices = deque()
self.proxy_indices = deque()
self.horizontalHeader().sortIndicatorChanged.connect(self._re_sort)
self.setItemDelegate(_HighlightDelegate(self))
def _re_sort(self):
# pretty print indices
def ind_to_py(indices):
py_ind = list()
for i in indices:
py_ind.append((i.row(), i.column(), i.data(Qt.DisplayRole)))
return py_ind
print("real ", ind_to_py(self.real_indices))
print("proxy ", ind_to_py(self.proxy_indices))
real_ind, proxy_ind = zip(*sorted(zip(self.real_indices, self.proxy_indices),
key=lambda x: (x[1].row(),
x[1].column())))
self.real_indices = deque(real_ind)
self.proxy_indices = deque(proxy_ind)
print("sorted real ", ind_to_py(self.real_indices))
print("sorted proxy", ind_to_py(self.proxy_indices))
print("---------------------------------------------------")
self.re_draw()
#property
def _model(self):
return self.model().sourceModel()
def re_draw(self):
self.viewport().update()
# we are always searching only in first column
def search_string(self, string: str):
indices = self._model.findItems(string, Qt.MatchContains, 0)
# get QModelIndex from found data
self.real_indices = deque([i.index() for i in indices])
self.proxy_indices = [QPersistentModelIndex(self.model().mapFromSource(i))
for i in self.real_indices]
# sort indeces according to their row and column
self._re_sort()
# update the view to highlight data
self.re_draw()
def _search_next(self):
self.real_indices.rotate(-1)
self.proxy_indices.rotate(-1)
self.re_draw()
def _search_previous(self):
self.real_indices.rotate(1)
self.proxy_indices.rotate(1)
self.re_draw()
# custom implementation to sort according to numbers not strings
class NumberSortModel(QSortFilterProxyModel):
def lessThan(self, left_index: "QModelIndex",
right_index: "QModelIndex") -> bool:
left_var: str = left_index.data(Qt.EditRole)
right_var: str = right_index.data(Qt.EditRole)
try:
return float(left_var) < float(right_var)
except (ValueError, TypeError):
pass
try:
return left_var < right_var
except TypeError: # in case of NoneType
return True
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
ex = Splash()
sys.exit(app.exec_())
In short when I run the search and click next the red marked cell moves down. It moves up when previous is clicked. But when I apply sorting by clicking table header it is messes up next/previous funstions. I want the red cell to always go down irrespective of applied sorting when next is clicked same with previous.

The logic of the painting should not be done directly but through the roles that must be used by the delegate to do the painting.
On the other hand, the following item must be selected based on the visual position of the item, so it must be mapped using the proxy.
Considering the above, the solution is:
CurrentRole = Qt.UserRole + 1000
SelectedRole = Qt.UserRole + 1001
# takes care of the coloring of results
class _HighlightDelegate(QStyledItemDelegate):
def initStyleOption(self, option: "QStyleOptionViewItem", index: "QModelIndex"):
super().initStyleOption(option, index)
is_current = index.data(CurrentRole) or False
is_selected = index.data(SelectedRole) or False
if is_current:
option.backgroundBrush = QColor(Qt.red)
option.palette.setColor(QPalette.Normal, QPalette.Highlight, QColor(Qt.red))
elif is_selected:
option.backgroundBrush = option.palette.highlight()
class CustomQTableView(QTableView):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.selected_indexes = []
self.current_index = None
self.setItemDelegate(_HighlightDelegate(self))
#property
def _model(self):
return self.model().sourceModel()
def search_string(self, string: str):
# restore
for index in self.selected_indexes:
self._model.setData(QModelIndex(index), False, SelectedRole)
if self.current_index is not None:
self._model.setData(QModelIndex(self.current_index), False, CurrentRole)
self.current_index = None
column = 0
match_indexes = self._model.match(
self._model.index(0, column), Qt.DisplayRole, string, -1, Qt.MatchContains
)
self.selected_indexes = [
QPersistentModelIndex(index) for index in match_indexes
]
self._sort_indexes_by_view()
if self.selected_indexes:
self.current_index = self.selected_indexes[0]
for index in self.selected_indexes:
self._model.setData(QModelIndex(index), True, SelectedRole)
if self.current_index is not None:
self._model.setData(QModelIndex(self.current_index), True, CurrentRole)
def _search_next(self):
if self.current_index is not None:
self._model.setData(QModelIndex(self.current_index), False, CurrentRole)
self._sort_indexes_by_view()
pos = self.selected_indexes.index(self.current_index)
next_pos = (pos + 1) % len(self.selected_indexes)
self.current_index = self.selected_indexes[next_pos]
self._model.setData(QModelIndex(self.current_index), True, CurrentRole)
def _search_previous(self):
if self.current_index is not None:
self._model.setData(QModelIndex(self.current_index), False, CurrentRole)
self._sort_indexes_by_view()
pos = self.selected_indexes.index(self.current_index)
next_pos = (pos - 1) % len(self.selected_indexes)
self.current_index = self.selected_indexes[next_pos]
self._model.setData(QModelIndex(self.current_index), True, CurrentRole)
def _sort_indexes_by_view(self):
self.selected_indexes.sort(
key=lambda index: self.model().mapFromSource(QModelIndex(index)).row()
)

Related

Making a specified table using pyqt5 designer

I want to make a specific table like shown in the picture below using pyqt designer and I coulnd't make a good result.
I want to make this table in a window and contains the same elements and same dimensions.
I tried to use layouts using LineEdits and Qlabels but I couldnt make it too .
Thank you.
Premise: your question didn't show lots of research efforts, and from what was said it's quite clear that you're still a bit inexperienced; this will probably make this answer very complicated, but that's because what you asked is not simple.
While achieving what it is asked is not impossible, it is not easy.
Also, you cannot do it directly in designer.
The main problem is that Qt's item views use QHeaderView, which uses a monodimensional structure; adding another "dimension" layer makes things much more difficult.
So, the first aspect you need to consider is that the table widget needs to have a new, custom QHeaderView set for the horizontal header, so you'll obviously need to subclass QHeaderView; but in order to make things work you'll also need to subclass QTableWidget too.
Due to the "monodimensionality" of the header (which only uses a single coordinate for its data), you need to "flatten" the structure and create an abstraction layer in order to access it.
In order to achieve that, I created a Structure class, with functions that allow access to it as some sort of tree-model:
class Section(object):
def __init__(self, label='', children=None, isRoot=False):
self.label = label
self._children = []
if children:
self._children = []
for child in children:
child.parent = self
self._children.append(child)
self._isRoot = isRoot
self.parent = None
def children(self):
return self._children
def isRoot(self):
return self._isRoot
def iterate(self):
# an iterator that cycles through *all* items recursively
if not self._isRoot:
yield self
items = []
for child in self._children:
items.extend([i for i in child.iterate()])
for item in items:
yield item
def sectionForColumn(self, column):
# get the first (child) item for the given column
if not self._isRoot:
return self.root().sectionForColumn(column)
for child in self.iterate():
if not child._children:
if child.column() == column:
return child
def root(self):
if self._isRoot:
return self
return self.parent.root()
def level(self):
# while levels should start from -1 (root), we're using levels starting
# from 0 (which is root); this is done for simplicity and performance
if self._isRoot:
return 0
parent = self.parent
level = 0
while parent:
level += 1
parent = parent.parent
return level
def column(self):
# root column should be -1; see comment on level()
if self._isRoot:
return 0
parentColIndex = self.parent._children.index(self)
column = self.parent.column()
for c in self.parent._children[:parentColIndex]:
column += c.columnCount()
return column
def columnCount(self):
# return the column (child) count for this section
if not self._children:
return 1
columns = 0
for child in self._children:
columns += child.columnCount()
return columns
def subLevels(self):
if not self._children:
return 0
levels = 0
for child in self._children:
levels = max(levels, child.subLevels())
return 1 + levels
class Structure(Section):
# a "root" class created just for commodity
def __init__(self, label='', children=None):
super().__init__(label, children, isRoot=True)
With this class, you can create your own header structure like this:
structure = Structure('Root item', (
Section('First parent, two sub levels', (
Section('First child, no children'),
Section('Second child, two children', (
Section('First subchild'),
Section('Second subchild')
)
)
)),
# column index = 3
Section('Second parent', (
Section('First child'),
Section('Second child')
)),
# column index = 5
Section('Third parent, no children'),
# ...
))
And here are the QHeaderView and QTableWidget subclasses, with a minimal reproducible code:
class AdvancedHeader(QtWidgets.QHeaderView):
_resizing = False
_resizeToColumnLock = False
def __init__(self, view, structure=None):
super().__init__(QtCore.Qt.Horizontal, view)
self.structure = structure or Structure()
self.sectionResized.connect(self.updateSections)
self.sectionHandleDoubleClicked.connect(self.emitHandleDoubleClicked)
def setStructure(self, structure):
if structure == self.structure:
return
self.structure = structure
self.updateGeometries()
def updateSections(self, index=0):
# ensure that the parent section is always updated
if not self.structure.children():
return
section = self.structure.sectionForColumn(index)
while not section.parent.isRoot():
section = section.parent
leftColumn = section.column()
left = self.sectionPosition(leftColumn)
width = sum(self.sectionSize(leftColumn + c) for c in range(section.columnCount()))
self.viewport().update(left - self.offset(), 0, width, self.height())
def sectionRect(self, section):
if not self.structure.children():
return
column = section.column()
left = 0
for c in range(column):
left += self.sectionSize(c)
bottom = self.height()
rowHeight = bottom / self.structure.subLevels()
if section.parent.isRoot():
top = 0
else:
top = (section.level() - 1) * rowHeight
width = sum(self.sectionSize(column + c) for c in range(section.columnCount()))
if section.children():
height = rowHeight
else:
root = section.root()
rowCount = root.subLevels()
parent = section.parent
while parent.parent:
rowCount -= 1
parent = parent.parent
height = rowHeight * rowCount
return QtCore.QRect(left, top, width, height)
def paintSubSection(self, painter, section, level, rowHeight):
sectionRect = self.sectionRect(section).adjusted(0, 0, -1, -1)
painter.drawRect(sectionRect)
painter.save()
font = painter.font()
selection = self.selectionModel()
column = section.column()
sectionColumns = set([column + c for c in range(section.columnCount())])
selectedColumns = set([i.column() for i in selection.selectedColumns()])
if ((section.children() and selectedColumns & sectionColumns == sectionColumns) or
(not section.children() and column in selectedColumns)):
font.setBold(True)
painter.setFont(font)
painter.drawText(sectionRect, QtCore.Qt.AlignCenter, section.label)
painter.restore()
for child in section.children():
self.paintSubSection(painter, child, child.level(), rowHeight)
def sectionHandleAt(self, pos):
x = pos.x() + self.offset()
visual = self.visualIndexAt(x)
if visual < 0:
return visual
for section in self.structure.iterate():
rect = self.sectionRect(section)
if pos in rect:
break
else:
return -1
grip = self.style().pixelMetric(QtWidgets.QStyle.PM_HeaderGripMargin, None, self)
if x < rect.x() + grip:
return section.column() - 1
elif x > rect.x() + rect.width() - grip:
return section.column() + section.columnCount() - 1
return -1
logical = self.logicalIndex(visual)
position = self.sectionViewportPosition(logical)
atLeft = x < (position + grip)
atRight = x > (position + self.sectionSize(logical) - grip)
if self.orientation() == QtCore.Qt.Horizontal and self.isRightToLeft():
atLeft, atRight = atRight, atLeft
if atLeft:
while visual >= 0:
visual -= 1
logical = self.logicalIndex(visual)
if not self.isSectionHidden(logical):
break
else:
logical = -1
elif not atRight:
logical = -1
return logical
def emitHandleDoubleClicked(self, index):
if self._resizeToColumnLock:
# avoid recursion
return
pos = self.viewport().mapFromGlobal(QtGui.QCursor.pos())
handle = self.sectionHandleAt(pos)
if handle != index:
return
self._resizeToColumnLock = True
for section in self.structure.iterate():
if index in range(section.column(), section.column() + section.columnCount()):
rect = self.sectionRect(section)
if rect.y() <= pos.y() <= rect.y() + rect.height():
sectCol = section.column()
for col in range(sectCol, sectCol + section.columnCount()):
if col == index:
continue
self.sectionHandleDoubleClicked.emit(col)
break
self._resizeToColumnLock = False
# -------- base class reimplementations -------- #
def sizeHint(self):
hint = super().sizeHint()
hint.setHeight(hint.height() * self.structure.subLevels())
return hint
def mousePressEvent(self, event):
super().mousePressEvent(event)
if event.button() != QtCore.Qt.LeftButton:
return
handle = self.sectionHandleAt(event.pos())
if handle >= 0:
self._resizing = True
else:
# if the clicked section has children, select all of its columns
cols = []
for section in self.structure.iterate():
sectionRect = self.sectionRect(section)
if event.pos() in sectionRect:
firstColumn = section.column()
columnCount = section.columnCount()
for column in range(firstColumn, firstColumn + columnCount):
cols.append(column)
break
self.sectionPressed.emit(cols[0])
for col in cols[1:]:
self.sectionEntered.emit(col)
def mouseMoveEvent(self, event):
super().mouseMoveEvent(event)
handle = self.sectionHandleAt(event.pos())
if not event.buttons():
if handle < 0:
self.unsetCursor()
elif handle < 0 and not self._resizing:
# update sections when click/dragging (required if highlight is enabled)
pos = event.pos()
pos.setX(pos.x() + self.offset())
for section in self.structure.iterate():
if pos in self.sectionRect(section):
self.updateSections(section.column())
break
# unset the cursor, in case it was set for a section handle
self.unsetCursor()
def mouseReleaseEvent(self, event):
self._resizing = False
super().mouseReleaseEvent(event)
def paintEvent(self, event):
qp = QtGui.QPainter(self.viewport())
qp.setRenderHints(qp.Antialiasing)
qp.translate(.5, .5)
height = self.height()
rowHeight = height / self.structure.subLevels()
qp.translate(-self.horizontalOffset(), 0)
column = 0
for parent in self.structure.children():
self.paintSubSection(qp, parent, 0, rowHeight)
column += 1
class CustomHeaderTableWidget(QtWidgets.QTableWidget):
structure = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
customHeader = AdvancedHeader(self)
self.setHorizontalHeader(customHeader)
customHeader.setSectionsClickable(True)
customHeader.setHighlightSections(True)
self.cornerHeader = QtWidgets.QLabel(self)
self.cornerHeader.setAlignment(QtCore.Qt.AlignCenter)
self.cornerHeader.setStyleSheet('border: 1px solid black;')
self.cornerHeader.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
self.verticalHeader().setMinimumWidth(
self.cornerHeader.minimumSizeHint().width() + self.fontMetrics().width(' '))
self._cornerButton = self.findChild(QtWidgets.QAbstractButton)
self.setStructure(kwargs.get('structure') or Section('ROOT', isRoot=True))
self.selectionModel().selectionChanged.connect(self.selectionModelSelChanged)
def setStructure(self, structure):
if structure == self.structure:
return
self.structure = structure
if not structure:
super().setColumnCount(0)
self.cornerHeader.setText('')
else:
super().setColumnCount(structure.columnCount())
self.cornerHeader.setText(structure.label)
self.horizontalHeader().setStructure(structure)
self.updateGeometries()
def selectionModelSelChanged(self):
# update the corner widget
selected = len(self.selectionModel().selectedIndexes())
count = self.model().rowCount() * self.model().columnCount()
font = self.cornerHeader.font()
font.setBold(selected == count)
self.cornerHeader.setFont(font)
def updateGeometries(self):
super().updateGeometries()
vHeader = self.verticalHeader()
if not vHeader.isVisible():
return
style = self.verticalHeader().style()
opt = QtWidgets.QStyleOptionHeader()
opt.initFrom(vHeader)
margin = style.pixelMetric(style.PM_HeaderMargin, opt, vHeader)
width = self.cornerHeader.minimumSizeHint().width() + margin * 2
vHeader.setMinimumWidth(width)
self.cornerHeader.setGeometry(self._cornerButton.geometry())
def setColumnCount(self, count):
# ignore column count, as we're using setStructure() instead
pass
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
structure = Structure('UNITE', (
Section('Hrs de marche', (
Section('Expl'),
Section('Indi', (
Section('Prev'),
Section('Accid')
))
)),
Section('Dem', (
Section('TST'),
Section('Epl')
)),
Section('Decle'),
Section('a'),
Section('Consom'),
Section('Huile'),
))
tableWidget = CustomHeaderTableWidget()
tableWidget.setStructure(structure)
tableWidget.setRowCount(2)
tableWidget.setVerticalHeaderLabels(
['Row {}'.format(r + 1) for r in range(tableWidget.rowCount())])
tableWidget.show()
sys.exit(app.exec())
Some considerations, since the above example is not perfect:
sections are not movable (if you try to set setSectionsMovable and try to drag a section, it would probably crash at some point);
while I tried to avoid resizing of a "parent" section (the resize cursor is not shown), it is still possible to resize a child section from the parent rectangle;
changing the horizontal structure of the model might give unexpected results (I only implemented basic operations);
Structure is a standard python object subclass, and it's completely unlinked from the QTableWidget;
considering the above, using functions like horizontalHeaderItem, setHorizontalHeaderItem or setHorizontalHeaderLabels might not work as expected;
Now, how to use it in designer? You need to use a promoted widget.
Add the QTableWidget, right click on it and select Promote to..., ensure that "QTableWidget" is selected in the "Base class name" combo, type "CustomHeaderTableWidget" in the "Promoted class name" field and then the file name that contains the subclass in the "Header file" field (note that it's treated like a python module name, so it has to be without the .py file extension); click "Add", click "Promote" and save it.
Consider that, from there, you must still provide the custom Structure, and if you added any row and column in Designer it must reflect the structure column count.
Finally, since the matter is interesting, I might return on it in the future and update the code, eventually.
In the meantime, I strongly suggest you to carefully study the code, explore all the reimplementations of QHeaderView (see what's below base class reimplementations comment) and what the original methods actually do by reading the QHeaderView documentation.

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

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

Link rows from two QTableView for visual comparaison

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

Adding checkBox as vertical header in QtableView

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.

Categories