PyQt4.9.1 view never calls model.data - pyqt4

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

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.

PYQT QTableView Delegate can not show createEditor when applied with Proxy

I'm having problem to show the Editor Widget when Delegate is applied with the Proxy situation.
-> self.table.setModel(self.proxy)
If the Delegate is applied to the View/Model structure, then there is no problem at all.
-> #self.table.setModel(self.model)
Refer to: https://www.pythonfixing.com/2021/10/fixed-adding-row-to-qtableview-with.html
See the code below:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class Delegate(QItemDelegate):
def __init__(self):
QItemDelegate.__init__(self)
self.type_items = ["1", "2", "3"]
def createEditor(self, parent, option, index):
if index.column() == 0:
comboBox = QComboBox(parent)
comboBox.addItems(self.type_items)
return comboBox
# no need to check for the other columns, as Qt automatically creates a
# QLineEdit for string values and QTimeEdit for QTime values;
return super().createEditor(parent, option, index)
class TableModel(QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def appendRowData(self, data):
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self._data.append(data)
self.endInsertRows()
def data(self, index, role=Qt.DisplayRole):
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
self._data[index.row()][index.column()] = value
self.dataChanged.emit(index, index)
return True
return False
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._data[0])
def flags(self, index):
# allow editing of the index
return super().flags(index) | Qt.ItemIsEditable
class CustomProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._filters = dict()
#property
def filters(self):
return self._filters
def setFilter(self, expresion, column):
if expresion:
self.filters[column] = expresion
elif column in self.filters:
del self.filters[column]
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
for column, expresion in self.filters.items():
text = self.sourceModel().index(source_row, column, source_parent).data()
regex = QRegExp(
expresion, Qt.CaseInsensitive, QRegExp.RegExp
)
if regex.indexIn(text) == -1:
return False
return True
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
localWidget = QWidget()
self.table = QTableView(localWidget)
data = [["1", "Hi", QTime(2, 1)], ["2", "Hello", QTime(3, 0)]]
self.model = TableModel(data)
self.proxy = CustomProxyModel() # Customized Filter
self.proxy.setSourceModel(self.model)
#self.table.setModel(self.model) # Original code, for View/Model
self.table.setModel(self.proxy) # Revised code, for View/Proxy/Model
self.table.setItemDelegate(Delegate())
self.add_row = QPushButton("Add Row", localWidget)
self.add_row.clicked.connect(self.addRow)
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
index = self.model.index(row, column)
self.table.openPersistentEditor(index) # openPersistentEditor for createEditor
layout_v = QVBoxLayout()
layout_v.addWidget(self.table)
layout_v.addWidget(self.add_row)
localWidget.setLayout(layout_v)
self.setCentralWidget(localWidget)
self.show()
def addRow(self):
row = self.model.rowCount()
new_row_data = ["3", "Howdy", QTime(9, 0)]
self.model.appendRowData(new_row_data)
for i in range(self.model.columnCount()):
index = self.model.index(row, i)
self.table.openPersistentEditor(index) # openPersistentEditor for createEditor
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Test with View/Model, Widget Editor display.
Test with View/Proxy/Model, Widget Editor not display.
Any attempt to access the view indexes must use the view's model.
Your code doesn't work because the index you are providing belongs to another model, so the editor cannot be created because the view doesn't recognize the model of the index as its own: the view uses the proxy model, while you're trying to open an editor for the source model.
While in this case the simplest solution would be to use self.proxy.index(), the proper solution is to always refer to the view's model.
Change both self.model.index(...) to self.table.model().index(...).
Yes, thanks you very much.
Revised the code as below, now the Widget Editor display accordingly.
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
#index = self.model.index(row, column) # original code, which get the wrong index from the model
index = self.proxy.index(row, column) # revised code, get the correct index from the proxy
self.table.openPersistentEditor(index) # openPersistentEditor for createEditor

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.

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

PyQt QTreeView Disable child selection when parent selected

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

Resources