QTableView model crash when calling endInsertRows() - python

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

Related

QTreeView, QAbstractItemModel. App exits while expanding node

I need to display the hierarchical model. Child nodes must be created when user expands the node. The number of child nodes is not known in advance. Some child nodes can be created immediately after expanding parent node. And some child nodes need time to get data by sending request and only then can be created.
So I create QTreeView + QSortFilterProxyModel + Qt model (QAbstractItemModel inheritor) + data model. The code below works fine with the proxy model. But without proxy model + for nodes that created immediately I have Process finished with exit code -1073741819 (0xC0000005) while expanding any node. I'm afraid that this error will also appear in the presence of a proxy model sooner or later.
UPD 1: I added the _populate_request signal with QueuedConnection to 'separate' fetchMore call stack from adding/removing nodes from model (thanks to #musicamante - it's the same idea as a single shot timer). That helped. But this step is not obvious for me and I still open for ideas why direct calls leads to crash.
UPD 2: Added "Reload" context menu to reload children. To check old child nodes removing for crash.
import random
import sys
import weakref
from enum import Enum, auto
from typing import Optional, List
from PyQt5 import QtCore, QtTest
from PyQt5.QtWidgets import QMainWindow, QTreeView, QVBoxLayout, QApplication, QMenu
from PyQt5.QtCore import QModelIndex, Qt
class TreeNodeStatus(Enum):
NotPopulated = auto()
Populating = auto()
Populated = auto()
Error = auto()
class TreeNode(QtCore.QObject):
""" Node of objects tree; root node is essentially a data model """
status_changed = QtCore.pyqtSignal(object) # node
before_children_added = QtCore.pyqtSignal(object, int, int) # parent_node, pos, count
children_added = QtCore.pyqtSignal(object, int, int) # parent_node, pos, count
before_children_removed = QtCore.pyqtSignal(object, int, int) # parent_node, pos, count
children_removed = QtCore.pyqtSignal(object, int, int) # parent_node, pos, count
_populate_request = QtCore.pyqtSignal()
def __init__(self, name: str, parent: Optional['TreeNode']):
super().__init__()
self._name = name
self._parent_ref = weakref.ref(parent) if parent is not None else lambda: None
self._status: TreeNodeStatus = TreeNodeStatus.NotPopulated
self._children: List[TreeNode] = []
# to listen root node signals only
if parent is not None:
self.status_changed.connect(parent.status_changed)
self.before_children_added.connect(parent.before_children_added)
self.children_added.connect(parent.children_added)
self.before_children_removed.connect(parent.before_children_removed)
self.children_removed.connect(parent.children_removed)
# to imitate minimal delay between fetchMore > populate call stack and adding/removing nodes;
# for nodes that can be created immediately in populate direct calls
# fetchMore > populate > _on_children_received causes crash;
# using of this signal prevents crash
self._populate_request.connect(self._populate, Qt.ConnectionType.QueuedConnection)
# for nodes that can not be created immediately in populate;
# to imitate delay due to getting response to a request
self._timer = QtCore.QTimer()
self._timer.setSingleShot(True)
self._timer.setInterval(2 * 1000) # 2s
self._timer.timeout.connect(self._on_children_received)
def parent(self) -> Optional['TreeNode']:
return self._parent_ref()
#property
def status(self) -> TreeNodeStatus:
return self._status
def _set_status(self, status: TreeNodeStatus):
self._status = status
self.status_changed.emit(self)
def populate(self):
# # signal with QueuedConnection - works good
# self._populate_request.emit()
# direct call causes app crash for nodes that can be created immediately and if there is no proxy model
self._populate()
def _populate(self):
# loading was started for this node already, exit
if self.status == TreeNodeStatus.Populating:
return
# start loading
self._set_status(TreeNodeStatus.Populating)
# forget old children
old_children_count = len(self._children)
self.before_children_removed.emit(self, 0, old_children_count)
# disconnect signals
for child in self._children:
child.status_changed.disconnect(self.status_changed)
child.before_children_added.disconnect(self.before_children_added)
child.children_added.disconnect(self.children_added)
child.before_children_removed.disconnect(self.before_children_removed)
child.children_removed.disconnect(self.children_removed)
self._children.clear()
self.children_removed.emit(self, 0, old_children_count)
# request data about children nodes
# # timer - for nodes that can not be created immediately
# self._timer.start()
# direct call - for nodes that can be created immediately
self._on_children_received()
def children(self) -> List['TreeNode']:
return self._children
#property
def name(self) -> str:
return self._name
def _on_children_received(self):
print('!_on_children_received', self.name)
# create children nodes
new_children_count = random.randint(0, 4)
self.before_children_added.emit(self, 0, new_children_count)
self._children = [TreeNode(self.name + ' ' + str(i), self) for i in range(new_children_count)]
self.children_added.emit(self, 0, new_children_count)
# update status
self._set_status(TreeNodeStatus.Populated)
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, root_node: TreeNode):
super().__init__()
# root node == data model
self._root_node = root_node
self._root_node.status_changed.connect(self._on_node_status_changed)
self._root_node.before_children_added.connect(self._before_children_added)
self._root_node.children_added.connect(self._on_children_added)
self._root_node.before_children_removed.connect(self._before_children_removed)
self._root_node.children_removed.connect(self._on_children_removed)
def index(self, row: int, column: int, parent=QModelIndex(), *args, **kwargs) -> QModelIndex:
# discard non-existent indices: check for row/column for given parent inside
if not self.hasIndex(row, column, parent):
return QModelIndex()
# get parent node by index
if parent is None or not parent.isValid():
parent_node: TreeNode = self._root_node
else:
parent_node: TreeNode = parent.internalPointer()
# if has child with given row
if row < len(parent_node.children()):
# create index with node as internalPointer
return self.createIndex(row, column, parent_node.children()[row])
return QModelIndex()
def parent(self, index: QModelIndex = None) -> QModelIndex:
# invalid index => root node
if not index.isValid():
return QModelIndex()
node: TreeNode = index.internalPointer()
parent_node: TreeNode = node.parent()
# if parent is root node, return invalid index
if parent_node is self._root_node:
return QModelIndex()
# get row of parent node; parent_node is not root, must have it's own parent
grandparent_node = parent_node.parent()
parent_row = grandparent_node.children().index(parent_node)
# create index with node as internalPointer
return self.createIndex(parent_row, 0, parent_node)
def hasChildren(self, parent=QModelIndex(), *args, **kwargs) -> bool:
# can we expand node? if we can we have a triangle to the left of the node
parent_node = self._node_from_index(parent)
# children loaded - look at the number
if parent_node.status == TreeNodeStatus.Populated:
return len(parent_node.children()) > 0
# error - no children, can't expand
elif parent_node.status == TreeNodeStatus.Error:
return False
# not loaded/loading - assume they are
else:
return True
def canFetchMore(self, parent: QModelIndex) -> bool:
# can we get more data (child nodes) for parent?
# print('canFetchMore!', self._node_from_index(parent).name)
return self._can_fetch_more(parent)
def _can_fetch_more(self, parent: QModelIndex) -> bool:
parent_node = self._node_from_index(parent)
# children are not loaded/loading - assume they are
if parent_node.status == TreeNodeStatus.NotPopulated:
return True
# in other cases - can not get more child nodes
elif parent_node.status in [TreeNodeStatus.Populating,
TreeNodeStatus.Populated,
TreeNodeStatus.Error]:
return False
assert False
def fetchMore(self, parent: QModelIndex) -> None:
# get more data (child nodes) for parent
print('!FetchMore', self._node_from_index(parent).name)
if not self._can_fetch_more(parent):
return
parent_node = self._node_from_index(parent)
if parent_node.status != TreeNodeStatus.Populating:
parent_node.populate()
def rowCount(self, parent=QModelIndex(), *args, **kwargs):
parent_node = self._node_from_index(parent)
return len(parent_node.children())
def columnCount(self, parent=None, *args, **kwargs):
return 1
def _node_from_index(self, index: Optional[QModelIndex]) -> TreeNode:
# invalid index - root node
if index is None or not index.isValid():
return self._root_node
else:
return index.internalPointer()
def _index_from_node(self, node: TreeNode) -> Optional[QModelIndex]:
# root node - invalid index
if node is self._root_node:
return QModelIndex()
# according to the principle from index method
parent_node = node.parent()
row = parent_node.children().index(node)
return self.createIndex(row, 0, node)
def data(self, index, role=None):
node = self._node_from_index(index)
if role == Qt.DisplayRole:
return node.name
# get nodes by UserRole
elif role == Qt.UserRole:
return node
elif role == Qt.DecorationRole:
pass
def _on_node_status_changed(self, node: TreeNode):
index = self._index_from_node(node)
if index is not None:
# notify about changes - icon, tooltip
self.dataChanged.emit(index, index, [Qt.DecorationRole, Qt.ToolTipRole])
def _before_children_removed(self, parent_node: TreeNode, pos: int, count: int):
parent_index = self._index_from_node(parent_node)
if parent_index is not None:
self.beginRemoveRows(parent_index, pos, pos + count - 1)
def _on_children_removed(self, parent_node: TreeNode, pos: int, count: int):
self.endRemoveRows()
def _before_children_added(self, parent_node: TreeNode, pos: int, count: int):
parent_index = self._index_from_node(parent_node)
if parent_index is not None:
self.beginInsertRows(parent_index, pos, pos + count - 1)
print('!beginInsertRows', parent_node.name)
def _on_children_added(self, parent_node: TreeNode, pos: int, count: int):
self.endInsertRows()
print('!endInsertRows', parent_node.name)
class TreeView(QTreeView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._menu = QMenu(self)
# reload child nodes
self._reload_act = self._menu.addAction('Reload')
self._reload_act.triggered.connect(self._on_reload)
def mouseReleaseEvent(self, event):
""" Call context menu on right click button release """
super().mouseReleaseEvent(event)
if event.button() == Qt.MouseButton.RightButton:
index = self.indexAt(event.pos())
# above nodes only
if index.isValid():
self._menu.popup(self.viewport().mapToGlobal(event.pos()))
def _on_reload(self):
index = self.currentIndex()
node = index.data(role=Qt.UserRole)
if node.status != TreeNodeStatus.Populating:
node.populate()
class ClientWindow(QMainWindow):
def __init__(self):
super().__init__()
self._setup_ui()
root_node = TreeNode('root', None)
model = TreeModel(root_node)
# proxy = QtCore.QSortFilterProxyModel()
# proxy.setSourceModel(model)
# FixMe crash on expanding any node if we put source model here
self._view.setModel(model)
def _setup_ui(self):
self._view = TreeView()
self._view.setSortingEnabled(True)
central_wdg = self._view
central_vlt = QVBoxLayout()
central_wdg.setLayout(central_vlt)
self.setCentralWidget(central_wdg)
if __name__ == '__main__':
app = QApplication([])
main_window = ClientWindow()
main_window.show()
sys.exit(app.exec())

Exporting filter results QTableView (Python)

I have a Qtableview with a filter option using QLineEdit. It works ok except:
When i choose an index the filter doesn't look in the index.
Row 0 isn't visible
What i would like is to be able to export the filter results to another dataframe.
This is my code , i'm new at this son feel free to correct ;-)
import timeit
import pandas as pd
from PyQt5 import QtGui
from PyQt5.QtCore import Qt, QSortFilterProxyModel
class PandasTableModel(QtGui.QStandardItemModel):
def __init__(self, data, parent=None):
QtGui.QStandardItemModel.__init__(self, parent)
self._data = data
for col in data.columns:
data_col = [QtGui.QStandardItem("{}".format(x)) for x in data[col].values]
self.appendColumn(data_col)
return
def rowCount(self, parent=None):
return len(self._data.values)
def columnCount(self, parent=None):
return self._data.columns.size
def headerData(self, x, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._data.columns[x]
if orientation == Qt.Vertical and role == Qt.DisplayRole:
return self._data.index[x]
return None
class Datahandler():
def __init__(self):
pass
def Load_Master_Data(self,tableMasterData,inputFilter):
print('Start Load_Master_Data')
start = timeit.default_timer()
Datafile = pd.read_pickle('modules/ssh/downloads/productsmaster.pkl')
df = pd.DataFrame(Datafile)
# df= df.set_index('Master_key')
self.inputfilter = inputFilter
self.tableMasterData = tableMasterData
self.df = df
self.model = PandasTableModel(df)
self.proxy_model = QSortFilterProxyModel()
self.proxy_model.setFilterKeyColumn(-1) # Search all columns.
self.proxy_model.setSourceModel(self.model)
self.proxy_model.sort(0, Qt.AscendingOrder)
self.proxy_model.setFilterCaseSensitivity(False)
self.tableMasterData.setModel(self.proxy_model)
inputFilter.textChanged.connect(self.proxy_model.setFilterFixedString)
end = timeit.default_timer()
print("Process Time: ",end-start)
Cheers , Johnson

QTreeView requests index for invalid row

Have a look at the following MWE.
It is a simple QAbstractItemModel with only a single level, storing its items in a list. I create a QTreeView to display the model, and a button to remove the 2nd item.
from PyQt5.QtCore import QModelIndex, QAbstractItemModel, Qt
from PyQt5.QtWidgets import QTreeView, QApplication, QPushButton
class Item:
def __init__(self, title):
self.title = title
class TreeModel(QAbstractItemModel):
def __init__(self, parent=None):
super().__init__(parent)
self._items = [] # typing.List[Item]
def addItem(self, item: Item):
self.beginInsertRows(QModelIndex(), len(self._items), len(self._items))
self._items.append(item)
self.endInsertRows()
def removeItem(self, item: Item):
index = self._items.index(item)
self.beginRemoveRows(QModelIndex(), index, index)
self._items.remove(item)
self.endRemoveRows()
# ----- overridden methods from QAbstractItemModel -----
# noinspection PyMethodOverriding
def data(self, index: QModelIndex, role):
item = index.internalPointer()
if role == Qt.DisplayRole:
return item.title
# noinspection PyMethodOverriding
def rowCount(self, parent=QModelIndex()):
if not parent.isValid():
return len(self._items)
return 0
# noinspection PyMethodOverriding
def columnCount(self, parent=QModelIndex()):
return 1
# noinspection PyMethodOverriding
def index(self, row: int, col: int, parent=QModelIndex()):
assert not parent.isValid()
return self.createIndex(row, 0, self._items[row])
def parent(self, index=QModelIndex()):
return QModelIndex()
def removeItem():
model.removeItem(item2)
if __name__ == '__main__':
app = QApplication([])
model = TreeModel()
button = QPushButton('Delete')
button.clicked.connect(removeItem)
button.show()
item1 = Item('Item 1')
model.addItem(item1)
item2 = Item('Item 2')
model.addItem(item2)
treeView = QTreeView()
treeView.setModel(model)
treeView.show()
app.exec()
As far as I can tell, the implementation of my model is correct (though very basic). In particular, the row, and column counts it reports are correct, and it never creates indices for data that would not be valid.
Steps to reproduce my issue:
Run the code above.
In the tree view, select Item 2.
Press the Delete button.
On my system, the application crashes in beginRemoveRows(), because the view requests a QModelIndex for row 2. Naturally, row 2 does not exist.
Any idea why the QTreeView would think there would be 3 rows, when the model explicitly reports there are only 2?
When an item is added, moved, removed, etc, what the model does is verify the QPersistentModelIndex are valid or not, so it calls the index() method of QAbstractItemModel. And in that method it is the developer's responsibility to verify if the row or column is valid, and for that the model provides the hasIndex() method that you do not use causing the error you point out, so the solution is:
def index(self, row: int, col: int, parent=QModelIndex()):
if not self.hasIndex(row, col, parent):
return QModelIndex()
assert not parent.isValid()
return self.createIndex(row, 0, self._items[row])

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

Wrong rowCount when TableView is empty in PySIde

I am using PySide, with a tableview and a model as shown below:
# -*- coding: utf-8 -*-
from PySide import QtCore, QtGui
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, items, headers, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.items = items
self.headers = headers
def rowCount(self, parent):
return len(self.items)
def columnCount(self, parent):
return len(self.items[0])
def data(self, index, role):
if not index.isValid():
return None
elif role != QtCore.Qt.DisplayRole:
return None
return self.items[index.row()][index.column()]
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.headers[section]
return None
This is how I set the model, importing from models.py:
table_model = models.TableModel(data, headers)
self.tableView.setModel(table_model)
I get the tableview total rows:
table_model = tableview.model()
total_rows = table_model.rowCount(tableview)
If there is no data to display in the tableview, I set data = [[]].
If I try to get the rowCount, I get 1 instead of O.
I realize it probably counts the nested list set as data.
So, what should I do to get the right rou count?
Use data = [] when there's no data, and then implement columnCount like this:
def columnCount(self, parent):
return len(self.items[0]) if self.items else 0

Categories