PyQt QTreeView Disable child selection when parent selected - python-3.x

Does anyone know how one can disable a child from being selected once its corresponding parent is already selected and vice versa. Consider the following example:
A-1
-2
where A is a parent node in the treeview and 1 and 2 are two children under that parent. If the user selects A he/she should not be able to select either 1 or 2. In the same way if the user selects either 1 or 2 he/she should not be able to select A.
Here is the code I have used. This class defines the TreeView model:
class TreeModel(QtCore.QAbstractItemModel):
'''
This class handles the treeview model.
'''
def __init__(self, top, *args, **kwargs):
super(TreeModel, self).__init__(*args, **kwargs)
self.__top = top
def index(self, row, column, parent=QtCore.QModelIndex()):
if parent.isValid():
parent_node = parent.internalPointer()
node = parent_node.children[row]
index = self.createIndex(row, column, node)
else:
index = self.createIndex(row, column, self.__top)
return index
def parent(self, index):
if index.isValid():
node = index.internalPointer()
parent = node.parent
if parent is None:
parent_index = QtCore.QModelIndex()
else:
parent_index = self.createIndex(parent.row(), 0, parent)
else:
parent_index = QtCore.QModelIndex()
return parent_index
def rowCount(self, index=QtCore.QModelIndex()):
node = index.internalPointer()
if node is None:
count = 1
else:
count = len(node.children)
return count
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
node = index.internalPointer()
data = str(node.number)
else:
data = None
return data
def addChild(self, index, child):
self.beginInsertRows(index, self.rowCount(index), self.rowCount(index) + 1)
parent = index.internalPointer()
parent.addChild(child)
self.endInsertRows()
self.layoutChanged.emit()
This function adds nodes to the tree based on user input:
for key, value in treeViewModelDict.items():
temp = [key, value]
treeViewDictList.append(temp)
print(self.treedict)
def recurse(parent, children_data):
for child_data in children_data:
if isinstance(child_data, list):
recurse(child, child_data)
else:
child = MyData(child_data, parent=parent)
top = MyData("Tree View of Selected Meters")
for i, next in enumerate(treeViewDictList):
recurse(top, next)
self.model = TreeModel(top)
self.treeView.setModel(self.model)
self.treeView.expandAll()
Is there a pythonic way of doing this?

You need to record somewhere what the current selection is. I normally store that in the model somewhere. The selectionChanged signal on the tree view's selection model is good for this.
Then give your model a flags method (it should really have this already) which checks if the current node's parent is selected, if it isn't then you return whatever flags you want or'd with Qt.ItemIsSelectable, if it is, return flags without Qt.ItemIsSelectable

Related

Removing rows of a QSortFilterProxyModel behaves badly

I have an application which contains a QTableView for which I would like to have the possibility to sort its contents but also to remove one or more rows. Below is an example code that implements this.
from PyQt5 import QtCore, QtWidgets
class NeXuSFilesModel(QtCore.QAbstractTableModel):
fields = ['col1','col2']
def __init__(self,parent):
super(NeXuSFilesModel,self).__init__(parent)
self._nexus_contents = [['aaa','111'],['bbb','222'],['ccc','333']]
def columnCount(self, parent=None):
return 2
def data(self,index,role):
if not index.isValid():
return QtCore.QVariant()
row = index.row()
col = index.column()
if role == QtCore.Qt.DisplayRole:
return str(self._nexus_contents[row][col])
else:
return QtCore.QVariant()
def headerData(self, index, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return NeXuSFilesModel.fields[index]
else:
return index + 1
return None
def removeRow(self, row, parent):
self.beginRemoveRows(QtCore.QModelIndex(),row,row+1)
del self._nexus_contents[row]
self.endRemoveRows()
return True
def rowCount(self, parent=None):
return len(self._nexus_contents)
class NeXuSDataTableView(QtWidgets.QTableView):
def __init__(self,parent):
super(NeXuSDataTableView,self).__init__(parent)
self.horizontalHeader().setStretchLastSection(True)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Delete:
model = self.model()
selected_indexes = self.selectionModel().selectedRows()
source_indexes_rows = sorted([model.mapToSource(index).row() for index in selected_indexes],reverse=True)
for row in source_indexes_rows:
model.sourceModel().removeRow(row,QtCore.QModelIndex())
super(NeXuSDataTableView, self).keyPressEvent(event)
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super(MainWindow,self).__init__()
self._table = NeXuSDataTableView(self)
model = NeXuSFilesModel(self)
proxy_model = QtCore.QSortFilterProxyModel()
proxy_model.setSourceModel(model)
self._table.setModel(proxy_model)
self._table.setSortingEnabled(True)
mainLayout = QtWidgets.QVBoxLayout()
mainLayout.addWidget(self._table)
self.setLayout(mainLayout)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication([])
_ = MainWindow()
app.exec_()
When I run that code, I fall into several problems for which I could not find the solution or could not understand the explanations given by the various sources I could find.
When the program starts, the data is showed initially in the wrong order. Indeed, it is displayed in descending order whereas I would like to display it in ascending order
When I remove one item, it removes actually two items !
Would you have any idea about what is wrong with my implementation ?
About both your questions:
proxy_model.sort(0, QtCore.Qt.AscendingOrder) after self._table.sortSortingEnabled(True) results in an ascending order:
self._table.setSortingEnabled(True)
proxy_model.sort(0, QtCore.Qt.AscendingOrder)
mainLayout = QtWidgets.QVBoxLayout()
mainLayout.addWidget(self._table)
self.setLayout(mainLayout)
self.show()
Using self.beginRemoveRows(QtCore.QModelIndex(),row,row) will remove only one row.

Coloring Row in QTableView instead of Cell

Backstory: Using an imported UI, I place my table onto a QTableView. I also make use of alternating row colors to better differentiate rows.
Problem: I'm looking to color the row of a table that contains a True value in one of the columns. I am able to color the cell, but have not found a way to color the entire row. I use a PandasModel class to format the tables:
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = data
def rowCount(self, parent=None):
return len(self._data.values)
def columnCount(self, parent=None):
return self._data.columns.size
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
return str(self._data.values[index.row()][index.column()])
if role == QtCore.Qt.BackgroundRole:
row = index.row()
col = index.column()
if self._data.iloc[row,col] == True:
return QtGui.QColor('yellow')
return None
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self._data.columns[col]
return None
I've look through numerous examples, and I am aware there may be multiple ways to color a table using QBrush or QColor, but so far the best I am able to do is simply color the cell that contains the True value. Splicing in code from other examples, I thought it was possible that the col = index.column() was getting in the way, as maybe it was limiting it to the cell, however, when I remove this it becomes ambiguous.
Important: I am wanting to keep the alternating row colors that I set elsewhere in the script, so please keep that in mind! I am only looking to color the specifics rows that contain any True value.
If the column of the boolean values is known, you just have to check for the value of that column at the given row of the index.
Supposing that the column index is 2:
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = data
self.boolColumn = 2
# ...
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
return str(self._data.values[index.row()][index.column()])
if role == QtCore.Qt.BackgroundRole:
row = index.row()
if self._data.iloc[row, self.boolColumn] == True:
return QtGui.QColor('yellow')
Note: return None is implicit at the end of a function block, you don't need to specify it.

PyQt5 dragging and dropping in QTableview causes selected row to disappear

Following on from a previous question here, I looked to add delete row on key press functionality to my qtableview table in PyQt5 by adding the removeRows function to my model. However, since adding this function it has disrupted my drag and drop functionality, whereby the dragged row disappears when dropping elsewhere in the qtableview table.
Is there anyway I can prevent the dragged row from disappearing?
NB: Interestingly, when selecting the vertical header 'column' the drag/drop functionality works, but I'm keen to find a solution for dragging and dropping on row selection.
Here's my code below with the added removeRows function in the model, and also the keyPressEvent in the view
from PyQt5.QtGui import QBrush
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex
class myModel(QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super().__init__(parent, *args)
self._data = data or []
self._headers = ['Type', 'result', 'count']
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._headers)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
if section < 0 or section >= len(self._headers):
return ""
else:
return self._headers[section]
else:
return ''
return None
def removeRows(self, position, rows, QModelIndex):
self.beginRemoveRows(QModelIndex, position, position + rows - 1)
for i in range(rows):
del (self._data[position])
self.endRemoveRows()
self.layoutChanged.emit()
return True
def data(self, index, role=None):
if role == Qt.TextAlignmentRole:
return Qt.AlignHCenter
if role == Qt.ForegroundRole:
return QBrush(Qt.black)
if role == Qt.BackgroundRole:
if (self.index(index.row(), 0).data().startswith('second')):
return QBrush(Qt.green)
else:
if (self.index(index.row(), 1).data()) == 'abc':
return QBrush(Qt.yellow)
if (self.index(index.row(), 1).data()) == 'def':
return QBrush(Qt.blue)
if (self.index(index.row(), 1).data()) == 'ghi':
return QBrush(Qt.magenta)
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemIsDropEnabled | Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
def supportedDropActions(self) -> bool:
return Qt.MoveAction | Qt.CopyAction
class myTableView(QTableView):
def __init__(self, parent):
super().__init__(parent)
header = self.verticalHeader()
header.setSectionsMovable(True)
header.setSectionResizeMode(QHeaderView.Fixed)
header.setFixedWidth(10)
QShortcut('F7', self, self.getLogicalRows)
QShortcut('F6', self, self.toggleVerticalHeader)
QShortcut('Alt+Up', self, lambda: self.moveRow(True))
QShortcut('Alt+Down', self, lambda: self.moveRow(False))
self.setSelectionBehavior(self.SelectRows)
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(self.InternalMove)
self.setDragDropOverwriteMode(False)
def dropEvent(self, event):
if (event.source() is not self or
(event.dropAction() != Qt.MoveAction and
self.dragDropMode() != QAbstractItemView.InternalMove)):
super().dropEvent(event)
selection = self.selectedIndexes()
from_index = selection[0].row() if selection else -1
to_index = self.indexAt(event.pos()).row()
if (0 <= from_index < self.model().rowCount() and
0 <= to_index < self.model().rowCount() and
from_index != to_index):
header = self.verticalHeader()
from_index = header.visualIndex(from_index)
to_index = header.visualIndex(to_index)
header.moveSection(from_index, to_index)
event.accept()
super().dropEvent(event)
def toggleVerticalHeader(self):
self.verticalHeader().setHidden(self.verticalHeader().isVisible())
def moveRow(self, up=True):
selection = self.selectedIndexes()
if selection:
header = self.verticalHeader()
row = header.visualIndex(selection[0].row())
if up and row > 0:
header.moveSection(row, row - 1)
elif not up and row < header.count() - 1:
header.moveSection(row, row + 1)
def getLogicalRows(self):
header = self.verticalHeader()
for vrow in range(header.count()):
lrow = header.logicalIndex(vrow)
index = self.model().index(lrow, 0)
print(index.data())
def keyPressEvent(self, event):
if event.key() == Qt.Key_Delete:
index = self.currentIndex()
try:
self.model().removeRows(index.row(), 1, index)
except IndexError:
pass
else:
super().keyPressEvent(event)
class sample_data(QMainWindow):
def __init__(self):
super().__init__()
tv = myTableView(self)
tv.setModel(myModel([
["first", 'abc', 123],
["second"],
["third", 'def', 456],
["fourth", 'ghi', 789],
]))
self.setCentralWidget(tv)
tv.setSpan(1, 0, 1, 3)
if __name__ == '__main__':
app = QApplication(['Test'])
test = sample_data()
test.setGeometry(600, 100, 350, 185)
test.show()
app.exec_()
The main "problem" is that, by default, removeRows of QAbstractItemModel doesn't do anything (and returns False).
The technical problem is a bit more subtle.
A drag operation in an item view always begins with startDrag(), which creates a QDrag object and calls its exec(). When the user drops the data, that implementation also calls a private clearOrRemove function whenever the accepted drop action is MoveAction, which eventually overwrites the data or removes the row(s).
You've used setDragDropOverwriteMode(False), so it will call removeRows. Your previous code used to work because, as said, the default implementation does nothing, but now you've reimplemented it, and it actually deletes rows in that case.
The solution is to change the drop action whenever the drop event is a move (which is a bit unintuitive, but since the operation has been already performed, that shouldn't be an issue). Using IgnoreAction will avoid the unwanted behavior, as that clearOrRemove won't be called anymore in that case:
def dropEvent(self, event):
# ...
if (0 <= from_index < self.model().rowCount() and
0 <= to_index < self.model().rowCount() and
from_index != to_index):
# ...
header.moveSection(from_index, to_index)
event.accept()
event.setDropAction(Qt.IgnoreAction)
super().dropEvent(event)
UPDATE
When the items don't fill the whole viewport, drop events can occur outside the items range, resulting in an invalid QModelIndex from indexAt() not only when dropping beyond the last row, but also the last column.
Since you're only interested in vertical movement, the solution is to get the to_index from the vertical header, and eventually set it to the last row whenever it's still invalid (-1):
def dropEvent(self, event):
if (event.source() is not self or
(event.dropAction() != Qt.MoveAction and
self.dragDropMode() != QAbstractItemView.InternalMove)):
super().dropEvent(event)
selection = self.selectedIndexes()
from_index = selection[0].row() if selection else -1
globalPos = self.viewport().mapToGlobal(event.pos())
header = self.verticalHeader()
to_index = header.logicalIndexAt(header.mapFromGlobal(globalPos).y())
if to_index < 0:
to_index = header.logicalIndex(self.model().rowCount() - 1)
if from_index != to_index:
from_index = header.visualIndex(from_index)
to_index = header.visualIndex(to_index)
header.moveSection(from_index, to_index)
event.accept()
event.setDropAction(Qt.IgnoreAction)
super().dropEvent(event)

In PyQt5 how do I properly move rows in a QTableView using Drag&Drop

I (simply) want to be able to use a QTableViews Drag&Drop mechanism to move existing rows. I found lots of sources (e.g. here, here or here) which describe some aspects of dragging, dropping, inserting etc. but I'm still struggling to make it work for my case.
Here is what the solution I'm looking for should be capable of:
work on a 'Qt-free' data structure, e.g. a list of tuples.
operate on the data structure. i.e. when the order of items gets
modified in the view it should be modified in the data structure
look and feel of standard drag&drop enabled lists:
select/move whole rows
show a drop indicator for the whole line
Further operations like deleting/editing of cells must still be possible
i.e. not be touched by the drag&drop approach
This tutorial shows a solution which is very close to what I need but it uses a QStandardItemModel rather than QAbstractTableModel which looks semi-optimal to me because I have to operate on a 'mirrored' data structure based on QStandardItem which is needed by QStandardItemModel (am I right?)
The code which represents my current progress is appended below.
Currently I see two possible approaches:
Approach 1: Implement against QAbstractTableModel and implement all needed events/slots to modify the underlying data structure:
* pro: most generic approach
* pro: no redundant data
* con: I don't know how to get informed about a finished drag&drop
operation and what index got moved where
In the code I've appended I trace all related methods I know of and print out all arguments. Here is what I get when I drag line 2 onto line 3
dropMimeData(data: ['application/x-qabstractitemmodeldatalist'], action: 2, row: -1, col: -1, parent: '(row: 2, column: 0, valid: True)')
insertRows(row=-1, count=1, parent=(row: 2, column: 0, valid: True))
setData(index=(row: 0, column: 0, valid: True), value='^line1', role=0)
setData(index=(row: 0, column: 1, valid: True), value=1, role=0)
removeRows(row=1, count=1, parent=(row: -1, column: -1, valid: False))
This output raises the following questions for me:
why do moveRow/moveRows not get called? when would they be called?
why are insertRow/removeRow not called but only insertRows/removeRows?
what does a row index of -1 mean?
what can I do with mime data provided in dropMimeData? Should I use it to copy data later?
Approach 2: Use QStandardItemModel and modify your data in parallel to the data managed by QStandardItemModel.
* pro: there's a working example
* contra: you manage a redundant data structure which has to be consistent
with another internally managed data structure.
* contra: didn't find out how to do that exactly neither
Here is my current approach using QAbstractTableModel:
from PyQt5 import QtWidgets, QtCore, QtGui
class MyModel(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super().__init__(parent, *args)
self._data = data
def columnCount(self, parent):
return 2
def rowCount(self, parent):
return len(self._data)
def headerData(self, column: int, orientation, role: QtCore.Qt.ItemDataRole):
return (('Regex', 'Category')[column]
if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal
else None)
def data(self, index, role: QtCore.Qt.ItemDataRole):
if role not in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole}:
return None
print("data(index=%s, role=%r)" % (self._index2str(index), self._role2str(role)))
return (self._data[index.row()][index.column()]
if index.isValid()
and role in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole}
and index.row() < len(self._data)
else None)
def setData(self, index: QtCore.QModelIndex, value, role: QtCore.Qt.ItemDataRole):
print("setData(index=%s, value=%r, role=%r)" % (self._index2str(index), value, role))
return super().setData(index, value, role)
def flags(self, index):
return (
super().flags(index)
| QtCore.Qt.ItemIsDropEnabled
| (QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDragEnabled)
if index.isValid() else QtCore.Qt.NoItemFlags)
def dropMimeData(self, data, action, row, col, parent: QtCore.QModelIndex):
"""Always move the entire row, and don't allow column 'shifting'"""
print("dropMimeData(data: %r, action: %r, row: %r, col: %r, parent: %r)" % (
data.formats(), action, row, col, self._index2str(parent)))
assert action == QtCore.Qt.MoveAction
return super().dropMimeData(data, action, row, 0, parent)
def supportedDragActions(self):
return QtCore.Qt.MoveAction
def supportedDropActions(self):
return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction
def removeRow(self, row: int, parent=None):
print("removeRow(row=%r):" % (row))
return super().removeRow(row, parent)
def removeRows(self, row: int, count: int, parent=None):
print("removeRows(row=%r, count=%r, parent=%s)" % (row, count, self._index2str(parent)))
return super().removeRows(row, count, parent)
def insertRow(self, index, parent=None):
print("insertRow(row=%r, count=%r):" % (row, count))
return super().insertRow(row, count, parent)
def insertRows(self, row: int, count: int, parent: QtCore.QModelIndex = None):
print("insertRows(row=%r, count=%r, parent=%s)" % (row, count, self._index2str(parent)))
return super().insertRows(row, count, parent)
#staticmethod
def _index2str(index):
return "(row: %d, column: %d, valid: %r)" % (index.row(), index.column(), index.isValid())
#staticmethod
def _role2str(role: QtCore.Qt.ItemDataRole) -> str:
return "%s (%d)" % ({
QtCore.Qt.DisplayRole: "DisplayRole",
QtCore.Qt.DecorationRole: "DecorationRole",
QtCore.Qt.EditRole: "EditRole",
QtCore.Qt.ToolTipRole: "ToolTipRole",
QtCore.Qt.StatusTipRole: "StatusTipRole",
QtCore.Qt.WhatsThisRole: "WhatsThisRole",
QtCore.Qt.SizeHintRole: "SizeHintRole",
QtCore.Qt.FontRole: "FontRole",
QtCore.Qt.TextAlignmentRole: "TextAlignmentRole",
QtCore.Qt.BackgroundRole: "BackgroundRole",
#QtCore.Qt.BackgroundColorRole:
QtCore.Qt.ForegroundRole: "ForegroundRole",
#QtCore.Qt.TextColorRole
QtCore.Qt.CheckStateRole: "CheckStateRole",
QtCore.Qt.InitialSortOrderRole: "InitialSortOrderRole",
}[role], role)
class MyTableView(QtWidgets.QTableView):
class DropmarkerStyle(QtWidgets.QProxyStyle):
def drawPrimitive(self, element, option, painter, widget=None):
"""Draw a line across the entire row rather than just the column we're hovering over.
This may not always work depending on global style - for instance I think it won't
work on OSX."""
if element == self.PE_IndicatorItemViewItemDrop and not option.rect.isNull():
option_new = QtWidgets.QStyleOption(option)
option_new.rect.setLeft(0)
if widget:
option_new.rect.setRight(widget.width())
option = option_new
super().drawPrimitive(element, option, painter, widget)
def __init__(self):
super().__init__()
self.setStyle(self.DropmarkerStyle())
# only allow rows to be selected
self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
# disallow multiple rows to be selected
self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
self.setDragEnabled(True)
self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
self.setDropIndicatorShown(True) # default
self.setAcceptDrops(False) # ?
self.viewport().setAcceptDrops(True) # ?
self.setDragDropOverwriteMode(False)
class HelloWindow(QtWidgets.QMainWindow):
def __init__(self) -> None:
super().__init__()
model = MyModel([("^line0", 0),
("^line1", 1),
("^line2", 2),
("^line3", 3)])
table_view = MyTableView()
table_view.setModel(model)
table_view.verticalHeader().hide()
table_view.setShowGrid(False)
self.setCentralWidget(table_view)
def main():
app = QtWidgets.QApplication([])
window = HelloWindow()
window.show()
app.exec_()
if __name__ == "__main__":
main()
I have no clue yet how to make QAbstractTableModel or QAbstractItemModel work as described but I finally found a way to make the QTableView handle drag & drop and just makes the model move a row.
Here's the code:
from PyQt5 import QtWidgets, QtCore
class ReorderTableModel(QtCore.QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super().__init__(parent, *args)
self._data = data
def columnCount(self, parent=None) -> int:
return 2
def rowCount(self, parent=None) -> int:
return len(self._data) + 1
def headerData(self, column: int, orientation, role: QtCore.Qt.ItemDataRole):
return (('Regex', 'Category')[column]
if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal
else None)
def data(self, index: QtCore.QModelIndex, role: QtCore.Qt.ItemDataRole):
if not index.isValid() or role not in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole}:
return None
return (self._data[index.row()][index.column()] if index.row() < len(self._data) else
"edit me" if role == QtCore.Qt.DisplayRole else "")
def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlags:
# https://doc.qt.io/qt-5/qt.html#ItemFlag-enum
if not index.isValid():
return QtCore.Qt.ItemIsDropEnabled
if index.row() < len(self._data):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
def supportedDropActions(self) -> bool:
return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction
def relocateRow(self, row_source, row_target) -> None:
row_a, row_b = max(row_source, row_target), min(row_source, row_target)
self.beginMoveRows(QtCore.QModelIndex(), row_a, row_a, QtCore.QModelIndex(), row_b)
self._data.insert(row_target, self._data.pop(row_source))
self.endMoveRows()
class ReorderTableView(QtWidgets.QTableView):
"""QTableView with the ability to make the model move a row with drag & drop"""
class DropmarkerStyle(QtWidgets.QProxyStyle):
def drawPrimitive(self, element, option, painter, widget=None):
"""Draw a line across the entire row rather than just the column we're hovering over.
This may not always work depending on global style - for instance I think it won't
work on OSX."""
if element == self.PE_IndicatorItemViewItemDrop and not option.rect.isNull():
option_new = QtWidgets.QStyleOption(option)
option_new.rect.setLeft(0)
if widget:
option_new.rect.setRight(widget.width())
option = option_new
super().drawPrimitive(element, option, painter, widget)
def __init__(self, parent):
super().__init__(parent)
self.verticalHeader().hide()
self.setSelectionBehavior(self.SelectRows)
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(self.InternalMove)
self.setDragDropOverwriteMode(False)
self.setStyle(self.DropmarkerStyle())
def dropEvent(self, event):
if (event.source() is not self or
(event.dropAction() != QtCore.Qt.MoveAction and
self.dragDropMode() != QtWidgets.QAbstractItemView.InternalMove)):
super().dropEvent(event)
selection = self.selectedIndexes()
from_index = selection[0].row() if selection else -1
to_index = self.indexAt(event.pos()).row()
if (0 <= from_index < self.model().rowCount() and
0 <= to_index < self.model().rowCount() and
from_index != to_index):
self.model().relocateRow(from_index, to_index)
event.accept()
super().dropEvent(event)
class Testing(QtWidgets.QMainWindow):
"""Demonstrate ReorderTableView"""
def __init__(self):
super().__init__()
view = ReorderTableView(self)
view.setModel(ReorderTableModel([
("a", 1),
("b", 2),
("c", 3),
("d", 4),
]))
self.setCentralWidget(view)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication([])
test = Testing()
raise SystemExit(app.exec_())
MyData class should be inherited from QStandardItemModel
revised your code to solve drag-drop and extension class function call issue.
from PyQt5 import (QtWidgets, QtCore)
from PyQt5.QtWidgets import (QApplication, QTableView)
from PyQt5.QtGui import (QStandardItem, QStandardItemModel)
class MyModel(QStandardItemModel):
def __init__(self, data, parent=None, *args):
super().__init__(parent, *args)
self._data = data
for (index, data) in enumerate(data):
first = QStandardItem('Item {}'.format(index))
first.setDropEnabled(False)
first.setEditable(False)
second = QStandardItem(data[0])
second.setDropEnabled(False)
second.setEditable(False)
self.appendRow([first, second])
def columnCount(self, parent):
return 2
def rowCount(self, parent):
return len(self._data)
def headerData(self, column: int, orientation, role: QtCore.Qt.ItemDataRole):
return (('Regex', 'Category')[column]
if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal
else None)
def data(self, index, role: QtCore.Qt.ItemDataRole):
if role not in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole}:
return None
print("data(index=%s, role=%r)" % (self._index2str(index), self._role2str(role)))
return (self._data[index.row()][index.column()]
if index.isValid() and role in {QtCore.Qt.DisplayRole, QtCore.Qt.EditRole} and index.row() < len(
self._data)
else None)
def setData(self, index: QtCore.QModelIndex, value, role: QtCore.Qt.ItemDataRole):
print("setData(index=%s, value=%r, role=%r)" % (self._index2str(index), value, role))
return super().setData(index, value, role)
def flags(self, index):
return (
super().flags(index)
| QtCore.Qt.ItemIsDropEnabled
| (QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDragEnabled)
if index.isValid() else QtCore.Qt.NoItemFlags)
def dropMimeData(self, data, action, row, col, parent: QtCore.QModelIndex):
"""Always move the entire row, and don't allow column 'shifting'"""
print("dropMimeData(data: %r, action: %r, row: %r, col: %r, parent: %r)" % (
data.formats(), action, row, col, self._index2str(parent)))
assert action == QtCore.Qt.MoveAction
return super().dropMimeData(data, action, row, 0, parent)
def supportedDragActions(self):
return QtCore.Qt.MoveAction
def supportedDropActions(self):
return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction
def removeRow(self, row: int, parent=None):
print("removeRow(row=%r):" % (row))
return super().removeRow(row, parent)
def removeRows(self, row: int, count: int, parent=None):
print("removeRows(row=%r, count=%r, parent=%s)" % (row, count, self._index2str(parent)))
return super().removeRows(row, count, parent)
def insertRow(self, index, parent=None):
print("insertRow(row=%r, count=%r):" % (row, count))
return super().insertRow(row, count, parent)
def insertRows(self, row: int, count: int, parent: QtCore.QModelIndex = None):
print("insertRows(row=%r, count=%r, parent=%s)" % (row, count, self._index2str(parent)))
return super().insertRows(row, count, parent)
#staticmethod
def _index2str(index):
return "(row: %d, column: %d, valid: %r)" % (index.row(), index.column(), index.isValid())
#staticmethod
def _role2str(role: QtCore.Qt.ItemDataRole) -> str:
return "%s (%d)" % ({
QtCore.Qt.DisplayRole: "DisplayRole",
QtCore.Qt.DecorationRole: "DecorationRole",
QtCore.Qt.EditRole: "EditRole",
QtCore.Qt.ToolTipRole: "ToolTipRole",
QtCore.Qt.StatusTipRole: "StatusTipRole",
QtCore.Qt.WhatsThisRole: "WhatsThisRole",
QtCore.Qt.SizeHintRole: "SizeHintRole",
QtCore.Qt.FontRole: "FontRole",
QtCore.Qt.TextAlignmentRole: "TextAlignmentRole",
QtCore.Qt.BackgroundRole: "BackgroundRole",
# QtCore.Qt.BackgroundColorRole:
QtCore.Qt.ForegroundRole: "ForegroundRole",
# QtCore.Qt.TextColorRole
QtCore.Qt.CheckStateRole: "CheckStateRole",
QtCore.Qt.InitialSortOrderRole: "InitialSortOrderRole",
}[role], role)
class MyTableView(QTableView):
class DropMarkerStyle(QtWidgets.QProxyStyle):
def drawPrimitive(self, element, option, painter, widget=None):
"""Draw a line across the entire row rather than just the column we're hovering over.
This may not always work depending on global style - for instance I think it won't
work on OSX."""
if element == self.PE_IndicatorItemViewItemDrop and not option.rect.isNull():
option_new = QtWidgets.QStyleOption(option)
option_new.rect.setLeft(0)
if widget:
option_new.rect.setRight(widget.width())
option = option_new
super().drawPrimitive(element, option, painter, widget)
def __init__(self):
super().__init__()
self.setStyle(self.DropMarkerStyle())
self.verticalHeader().hide()
self.setShowGrid(False)
# only allow rows to be selected
self.setSelectionBehavior(self.SelectRows)
# disallow multiple rows to be selected
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(self.InternalMove)
self.setDragDropOverwriteMode(False)
class HelloWindow(QtWidgets.QMainWindow):
def __init__(self) -> None:
super().__init__()
model = MyModel([("^line0", 0),
("^line1", 1),
("^line2", 2),
("^line3", 3)])
table_view = MyTableView()
table_view.setModel(model)
self.setCentralWidget(table_view)
def main():
app = QApplication([])
window = HelloWindow()
window.show()
app.exec_()
if __name__ == "__main__":
main()

PyQt4.9.1 view never calls model.data

I'm trying to figure out how to work with the models and views in PyQt4.9.1 and I've run into a bit of a problem.
Here's the code that matters:
class TestModel(QtGui.QStandardItemModel):
def __init__(self,parent=None):
QtGui.QStandardItemModel.__init__(self,parent)
self.initData()
self.headings = ['name','value','']
def initData(self):
self.rows = [['name {0}'.format(i), i] for i in range(20)]
def data(self, index, value, role):
print ("foo")
if not index.isValid():
return
if (role == QtCore.Qt.DisplayRole):
row = index.row()
col = index.column()
if (col == 3):
return "BUTTON GOES HERE"
return self.rows[row][col]
def headerData(self,section,orientation,role):
if (role == QtCore.Qt.DisplayRole):
if (orientation == QtCore.Qt.Horizontal):
return self.headings[section]
def rowCount(self,parent):
return len(self.rows)
def columnCount(self,parent):
return 3
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.m = TestModel()
self.initUi()
def initUi(self):
layout = QtGui.QVBoxLayout()
widget = QtGui.QTableView()
widget.setModel(self.m)
layout.addWidget(widget)
self.setLayout(layout)
self.show()
Here's what happens when I launch my application's MainWindow: There are no error messages, the table is drawn with the right number of rows and columns and the correct headings, but the table is empty. You might notice that the model's draw method starts off with a print statement. That statement is never reached. Is there something I'm missing? I cant find any tutorials at all for PyQt4.9.1.
data() doesn't have any value parameter, but removing it doesn't solve the problem.
The virtual method index(row, column, parent), which is called whenever a QModelIndex needs to be created, always returns an invalid index for QStandardItemModel, unless a QStandardItem was explicitly created for the requested index. The view probably doesn't try to display cells when the index is invalid, so data() is never called.
If you kept inheriting from QStandardItemModel, you would need to reimplement index() to create valid indexes, but since you are using your own structure to store the data instead of using QStandardItem, you could simply inherit from QtCore.QAbstractTableModel:
class TestModel(QtCore.QAbstractTableModel):
def __init__(self,parent=None):
super(TestModel, self).__init__(parent)
self.initData()
self.headings = ['name','value','']
def initData(self):
self.rows = [['name {0}'.format(i), i] for i in range(20)]
def data(self, index, role):
if index.parent().isValid():
return None
if (role == QtCore.Qt.DisplayRole):
row = index.row()
col = index.column()
# 3rd column index is 2 not 3
if (col == 2):
return "BUTTON GOES HERE"
# that return should also be "inside" the role DisplayRole
return self.rows[row][col]
return None
def headerData(self,section,orientation,role):
if (role == QtCore.Qt.DisplayRole):
if (orientation == QtCore.Qt.Horizontal):
return self.headings[section]
Also, you should only return a non-zero row/column count for top-level items (the one without parent), if you are not representing a tree model :
def rowCount(self,parent):
if not parent.isValid():
return len(self.rows)
return 0
def columnCount(self,parent):
if not parent.isValid():
return 3
return 0

Resources