Related
I have a working drag and drop example below for reordering rows of the same column length for a qtableview using PyQt5 (with help from this StackOverflow question here). However I am looking to perform the same operation on a qtableview table where one or two rows have merged cells spanning the total number of columns (like the second row in the picture below).
How would be the best way to go about this? Should I remove the merge (clearSpans) at the point of drag/drop and then do a remerge based on the cell value (though when I tried this it did not work), or is there a way to drag/drop reorder with the cell merging intact?
Here's the code which works for row data of equal columns, but fails when a row is merged
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]
return None
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
def relocateRow(self, row_source, row_target) -> None:
row_a, row_b = max(row_source, row_target), min(row_source, row_target)
self.beginMoveRows(QModelIndex(), row_a, row_a, QModelIndex(), row_b)
self._data.insert(row_target, self._data.pop(row_source))
self.endMoveRows()
class myTableView(QTableView):
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)
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()
#self.clearSpans()
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 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)
self.show()
if __name__ == '__main__':
app = QApplication([])
test = sample_data()
raise SystemExit(app.exec_())
The sections of the vertical header can be made movable, so there's no need to implement this functionality yourself. It obviously means the vertical header will be visible, but that can be mitigated by making the sections blank, which will result in a relatively narrow header:
Note that moving sections around (rather than rows) is purely visual - the underlying model is never modified. That shouldn't really matter in practice, though, since the header provides methods to translate from logical to visual indices. And it does bring some additional benefits - for example, it's very easy to return to a previous state (i.e. by using the header's saveState and restoreState methods).
Below is a working demo based on your example. The rows can be re-ordered by dragging and dropping the section headers, or by pressing Alt+Up / Alt+Down when a row is selected. The vertical header can be toggled by pressing F6. The logical rows can be printed by pressing F7.
UPDATE:
I also added support for moving sections around by dragging and dropping the rows themselves.
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 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())
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_()
I'm trying to create a model that can be used for both a QTableView and QTreeView. As an example, my data is something like:
ID
Location
Name
101
201
Apple
201
None
Kitchen
102
201
Banana
301
None
Cellar
302
301
Potatoes
202
302
Nail
So every entry has a location which is itself an entry in the model. For the QTableView, I'd like to simply display all entries under each other as shown above, while for the QTreeView I'd like something like
201: Kitchen
101: Apple
102: Banana
301: Cellar
302: Potatoes
202: Nail
My problem however is that I can't figure out how to implement QAbstractProxyModel.maptoSource() or mapfromSource() as I lose information about the parent in the QTableView. Reading https://www.qtcentre.org/threads/26163-Map-table-to-tree-through-model-view-possible it seems that perhaps this is not possible at all. However the QAbstractProxyModel explicitly says that's it's meant for showing data in both views. Can anyone point me in the right direction or knows whether it's possible to implement a model like this? Especially in Python, I can't find any examples unfortunately.
I really like the idea of just using an unindented TreeView as a sort of TableView. Unfortunately I'm still having trouble creating the model. Currently, only the top entries are being shown.
class MyModel(qtg.QStandardItemModel):
def __init__(
self,
engine
):
self.engine = engine
self.hierarchy_key = 'location_id'
self.column_names = ['id', 'location_id', 'name', 'quantity']
super().__init__(0, len(self.fields))
self.setHorizontalHeaderLabels(self.column_names)
self.root = self.invisibleRootItem()
self.build_model()
def build_model(self):
def add_children_to_tree(entries, parent_item):
for entry in entries:
items = []
for col in self.column_names:
text = getattr(entry, col)
item = qtg.QStandardItem(text)
items.append(qtg.QStandardItem(text))
parent_item.appendRow(items)
item = items[1] #the location_id item
parent_item.setChild(item.index().row(), item.index().column(), item)
with session_scope(self.engine) as session:
child_entries = (
session.query(self.entry_type)
.filter(
getattr(getattr(self.entry_type, self.hierarchy_key), "is_")(
entry.id
)
)
.all()
)
if child_entries:
add_children_to_tree(child_entries, item)
self.removeRows(0, self.rowCount())
with session_scope(self.engine) as session:
root_entries = session.query(self.entry_type).filter(getattr(getattr(self.entry_type, self.hierarchy_key), "is_")(None)).all()
if not isinstance(root_entries, list):
root_entries = [root_entries]
add_children_to_tree(root_entries, self.root)
The idea is that the session query results in a list of entries. Each entry is a record in the database with the attributes "id", "location_id", etc. Each attribute thus is an Item and the list of items creates a row in the model. I can't figure out how one makes the row of items a child of another row in the way it's shown here:
I assume the setChild() function needs to be called differently?
As there is a distinct lack of examples for python, I'll post my modified version of the simpletreemodel here, which is what ended up working for me. By then using a QTreeView instead of a QTableView as suggested, I got the table to behave as I wanted it too. Overall, this creates MyItem which is an item containing the entire row of information and I then use recursion to add children to parents if their value for the hierarchy_key (location_id) is equal to the id of the parent.
class MyItem(object):
def __init__(self, data, parent=None):
self.parentItem = parent
self.itemData = data
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def columnCount(self):
return len(self.itemData)
def data(self, column=None):
try:
if column == None:
return [self.itemData[i] for i in range(self.columnCount())]
return self.itemData[column]
except IndexError:
return None
def parent(self):
return self.parentItem
def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0
class MyModel(QtCore.QAbstractItemModel):
def __init__(self, entry_type, engine, hierarchy_key, description_key, parent=None):
super(ORMModel, self).__init__(parent)
self.entry_type = entry_type
if isinstance(self.entry_type, str):
self.entry_type = getattr(ds, self.entry_type)
self.engine = engine
self.hierarchy_key = hierarchy_key
self.column_names = ['id', 'location_id', 'name', 'quantity']
self.rootItem = MyItem(self.column_names)
self.setHeaderData(0, Qt.Horizontal, self.rootItem)
self.initiateModel()
def root(self):
return self.rootItem
def columnCount(self, parent):
if parent.isValid():
return parent.internalPointer().columnCount()
else:
return self.rootItem.columnCount()
def data(self, index, role):
if not index.isValid():
return None
item = index.internalPointer()
if role == Qt.DisplayRole:
return item.data(index.column())
return None
def flags(self, index):
if not index.isValid():
return QtCore.Qt.NoItemFlags
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.rootItem.data(section)
return None
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return parentItem.childCount()
def initiateModel(self):
def add_children_to_tree(entries, parent_item):
for entry in entries:
row = []
for field in self.fields.keys():
val = getattr(entry, field)
if isinstance(val, list):
text = "; ".join(map(str, val))
else:
text = str(val)
row.append(text)
item = ORMItem(row, parent_item)
parent_item.appendChild(item)
with session_scope(self.engine) as session:
child_entries = (
session.query(self.entry_type)
.filter(
getattr(
getattr(self.entry_type, self.hierarchy_key), "is_"
)(entry.id)
)
.all()
)
if child_entries:
add_children_to_tree(child_entries, item)
with session_scope(self.engine) as session:
root_entries = (
session.query(self.entry_type)
.filter(
getattr(getattr(self.entry_type, self.hierarchy_key), "is_")(None)
)
.all()
)
if not isinstance(root_entries, list):
root_entries = [root_entries]
add_children_to_tree(root_entries, self.rootItem)
I'am using PySide2 and want to search a QListView for a value and have that row selected. Like you can with .findText(string_to_search_for) on a QComboBox.
How can i search for a value in a Qlistview and have the index returned?
some additional info:
The model of my QListView is implementation of QAbstractTableModel i've written.
The model is filled with data from a database, in the first column the id and 2nd column the name of the Database item. The QListView is only showing the 2nd column. This is my code for the QTableModel.
from PySide2 import QtGui,QtCore
class TwoColumnTableModel(QtCore.QAbstractTableModel):
def __init__(self, row_data=[], column_data=[], parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.row_data = row_data
self.column_data = column_data
def rowCount(self, parent):
return len(self.row_data)
def columnCount(self, parent):
return len(self.column_data)
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
value = self.row_data[row][column]
self.dataChanged.emit(row, column, [])
return value
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
if section < len(self.column_data):
return self.column_data[section]
else:
return "TEMP COL"
def insertRows(self, position, rows, data=[], parent=QtCore.QModelIndex()):
self.beginInsertRows(parent, position, position + rows - 1)
for i in range(len(data)):
columns = []
row_column1 = data[i][0]
row_column2 = data[i][1]
columns.insert(0, row_column1)
columns.insert(1, row_column2)
self.row_data.insert(position, columns)
self.endInsertRows()
return True
def removeRows(self, position, rows, parent=QtCore.QModelIndex()):
self.beginRemoveRows()
for i in range(rows):
value = self.row_data[position]
self.row_data.remove(value)
self.endRemoveRows()
return True
I ended up creating the following function in QTableModel class:
def find_index_of_value(self, search_string, search_column):
for index in range(self.rowCount(None)):
model_index = self.index(index, 1)
index_value = self.data(model_index, search_column)
if index_value == search_string:
return model_index
The "search_string" being the string i'm looking for and the "search_column" being the column of the model where i want to search for that string. With the return index i can use the setCurrentIndex(index) on my QListView and that's it.
I am trying to develop a GUI to managed scientific results. For this, I would like to present result from two datasets. The user would have a visual reprentation to help him to compare those results in two QTableView.
Image : comparer Result,
I want to link lines from the two table, so they always be present face to face.
When order change in one table, the other will follow and adapt its order to still have the linked lines face to face.
Eventually i would like to add empty row face to a line that haven't a relative line in the other table.
I was think to use a QSortFilterProxyModel but I am not sure how to use it.
Edit
My Question seem to be not clear. I Formulate here. I find myself a solution so here a example of what I was looking.
On this example line I link line according to the name (bla, bli blo, blu). We see on the same line, tables present face to face line of result "bla" and "bli", because there are in both left model and right.
There is no "blu" in the right table. so i add a empty line.
Idem in the left table with "blo"
In this example item are sorted with "configuration" of the right table. Left table have to follow the order choose by the right table.
Here my code without the solution
class Result(object):
def __init__(self, headerOrder, infoResult):
for n, headerName in enumerate(headerOrder):
self.__setattr__(headerName, infoResult[n])
self.diff = self.reference_redshift - self.estimate
ModelClass
class Result_model(QtCore.QAbstractTableModel):
def __init__(self, header, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.__datas = []
self.__headers = header
def rowCount(self, parent=None):
return len(self.__datas)
def columnCount(self, parent=None):
return len(self.__headers)
def data(self, index, role):
if role == QtCore.Qt.ToolTipRole:
row = index.row()
column = index.column()
return "{}: {}".format(self.__headers[column], getattr(self.__datas[row], self.__headers[column]))
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
value = getattr(self.__datas[row], self.__headers[column])
return value
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
if section < len(self.__headers):
return self.__headers[section]
else:
return "not implemented"
else:
return section
def supportedDragActions(self):
return QtCore.Qt.CopyAction
def supportedDropActions(self):
return Qt.CopyAction | Qt.MoveAction
def getResult(self, index):
row = index.row()
return self.__datas[row]
def sort(self, Ncol, order):
"""Sort table by given column number.
"""
self.emit(QtCore.SIGNAL("layoutAboutToBeChanged()"))
attribut = self.__headers[Ncol]
self.__datas = sorted(
self.__datas, key=lambda x: getattr(x, attribut), reverse=(order == QtCore.Qt.DescendingOrder))
self.emit(QtCore.SIGNAL("layoutChanged()"))
def addResults(self, results):
self.beginInsertRows(QtCore.QModelIndex(), len(
self.__datas), len(self.__datas) + len(results))
for res in results:
self.__datas.append(res)
self.endInsertRows()
TableView only Drag
class TableResult(QtGui.QTableView):
def __init__(self, parent=None):
QtGui.QTableView.__init__(self, parent)
self.setDragEnabled(True)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.header, self.aid_index = [["aid", "estimate", "reference_redshift", "diff", "amazed_executable_id", "amazed_configuration_id",
"astronomical_object_name", "star_forming_rate", "magnitude", "log_f_halpha", "emission_velocity_dispersion", "res_dir"], 0]
self.tag_info_result = ["aid", "estimate", "reference_redshift", "amazed_executable_id", "amazed_configuration_id",
"astronomical_object_name", "star_forming_rate", "magnitude", "log_f_halpha", "emission_velocity_dispersion", "res_dir"]
self.setItemDelegateForColumn(self.aid_index, ButtonDelegate(self))
self.parent = parent
def startDrag(self, dropAction):
if(self.parent is not None and hasattr(self.parent, "selection")):
# create mime data object
mime = QtCore.QMimeData()
# start drag
drag = QtGui.QDrag(self)
drag.setMimeData(mime)
drag.start(QtCore.Qt.CopyAction | QtCore.Qt.CopyAction)
else:
print("Drag impossible")
def mouseMoveEvent(self, event):
self.startDrag(event)
TableView with drop
class Selection_receiver(TableResult):
"Add the drop possibility from TableResult"
def __init__(self, setResultFunction, parent=None):
TableResult.__init__(self, parent)
self.setAcceptDrops(True)
self.setResultFunction = setResultFunction
def dragEnterEvent(self, event):
if (isinstance(event.source(), TableResult)):
event.accept()
event.acceptProposedAction()
else:
event.ignore()
def dropEvent(self, event):
print("dropEvent")
if (isinstance(event.source(), TableResult)):
event.acceptProposedAction()
model_result = event.source().parent.resModel
self.setResultFunction(model_result)
else:
event.ignore()
The Widget presenting the two Tables
class Comparater_result_widget(QtGui.QWidget):
"""
Present two table for easy comparaison.
"""
def __init__(self,parent=None):
super(self.__class__, self).__init__(parent)
self.setWindowTitle("Result Comparer")
main_layout = QtGui.QVBoxLayout()
receiverSplitter = QtGui.QSplitter()
receiverSplitter.setOrientation(QtCore.Qt.Horizontal)
self.left_receiver = Selection_receiver(self.setLeftResult)
receiverSplitter.addWidget(self.left_receiver)
self.right_receiver = Selection_receiver( self.setRightResult)
receiverSplitter.addWidget(self.right_receiver)
main_layout.addWidget(receiverSplitter)
self.left_receiver.horizontalScrollBar().valueChanged.connect(
self.right_receiver.horizontalScrollBar().setValue)
self.right_receiver.horizontalScrollBar().valueChanged.connect(
self.left_receiver.horizontalScrollBar().setValue)
self.left_receiver.verticalScrollBar().valueChanged.connect(
self.right_receiver.verticalScrollBar().setValue)
self.right_receiver.verticalScrollBar().valueChanged.connect(
self.left_receiver.verticalScrollBar().setValue)
self.right_results = None
self.left_results = None
self.setLayout(main_layout)
def setLeftResult(self, model_result):
print("setLeftResult []".format(model_result))
self.left_results = model_result
self.add_model_result(self.left_receiver, model_result)
def setRightResult(self, model_result):
print("setRightResult {}".format(model_result))
self.right_results = model_result
self.add_model_result(self.right_receiver, model_result)
def add_model_result(self, receiver, model_result):
receiver.setModel(model_result)
if(self.right_results is not None and self.left_results is not None):
self.link_result()
def link_result(self):
# parse the two model and link results if the have equal on one
# particular attribut
pass
def OnLeftChangeOrder(self):
# somthing like right_proxy.reorder(left_order)
pass
def OnRightChangeOrder(self):
# something link left_proxy.reorder(right_order)
pass
Here is my solution i created.
I use Two Proxy:
my own mapping proxy model ("MixRowProxyModel")
a almost normal QSortFilterProxyModel ("MySortProxyModel")
Source model is the model of MixRowProxyModel.
MixRowProxyModel is the model of MySortProxyModel
Table can be in two state, master or slave. If I sort the right table at one column, the right become master and the left is slave.
When a table is master, its MySortProxyModel is active, MixRowProxyModel is inactive
When a table is slave, its MySortProxyModel is inactive, MixRowProxyModel is active
When the two source model are set. I create a map between rows of the sources models. This map will not change when table are sorted.
When a table become slave i construct the mapping for its MixRowProxyModel (see method change in Comparater_result_widget). For that i use the initial mapping i created between modelsource.
class MixRowProxyModel(QtGui.QAbstractProxyModel):
def __init__(self, controller, side, parent=None):
QtGui.QAbstractProxyModel.__init__(self, parent=parent)
self.controller = controller
self.nbEmpty = 0
self.mapProxyToSource = None
self.isMaster = True
self.side = side
def setMap(self, mapping):
self.isMaster = False
self.mapProxyToSource = mapping
self.mapSourceToProxy = {v: k for k,
v in self.mapProxyToSource.items()}
def mapFromSource(self, sourceIndex):
#print("MixRowProxyModel Proxy Index model {}".format(sourceIndex.model()))
if(not sourceIndex.isValid()):
return self.index(-1, -1)
if(self.isMaster):
return self.index(sourceIndex.row(), sourceIndex.column(), parent=QtCore.QModelIndex)
else:
row = sourceIndex.row()
if(row in self.mapSourceToProxy):
return self.index(self.mapSourceToProxy[row], sourceIndex.column())
else:
print("Invalid sourceIndex {}".format(row))
return self.index(-1, -1)
def mapToSource(self, proxyIndex):
if(not proxyIndex.isValid()):
return self.sourceModel().index(-1, -1)
if(self.isMaster):
return self.sourceModel().index(proxyIndex.row(), proxyIndex.column())
else:
row = proxyIndex.row()
if(row in self.mapProxyToSource):
return self.sourceModel().index(self.mapProxyToSource[row], proxyIndex.column())
else:
# print("Invalid proxyIndex {}".format(row))
return self.sourceModel().index(-1, -1)
def rowCount(self, parent=None):
return self.sourceModel().rowCount() + self.nbEmpty
def columnCount(self, parent=None):
return self.sourceModel().columnCount()
def addEmptyRow(self):
print("addEmptyRow {}".format(self.side))
self.beginInsertRows(QtCore.QModelIndex(),
self.rowCount(), self.rowCount())
self.nbEmpty += 1
self.endInsertRows()
return -1
def parent(self, index):
return QtCore.QModelIndex()
def index(self, row, column, parent=QtCore.QModelIndex):
if(row >= self.rowCount() or row < 0 or column < 0 or column >= self.columnCount()):
return QtCore.QModelIndex()
return self.createIndex(row, column, parent)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self.sourceModel().headerData(section, orientation, role)
else:
return section
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def data(self, index, role=QtCore.Qt.DisplayRole):
if(not index.isValid()):
return None
if(self.isMaster):
if(index.row() < self.sourceModel().rowCount()):
return self.sourceModel().data(index, role)
else:
return None
else:
if(not index.row() in self.mapProxyToSource):
return None
return self.sourceModel().data(self.sourceModel().index(self.mapProxyToSource[index.row()], index.column()), role)
the second ProxyModel
class MySortProxyModel(QtGui.QSortFilterProxyModel):
sortedSignal = QtCore.pyqtSignal(QtCore.Qt.SortOrder)
def sort(self, column, order):
self.sourceModel().isMaster = True
#print("Sort order : {} sort Role {}".format(order, self.sortRole()))
self.setSortRole(QtCore.Qt.DisplayRole)
super().sort(column, order)
self.sortedSignal.emit(order)
the Parent Widget controlling the two tables
class Comparater_result_widget(QtGui.QWidget):
def __init__(self, controller, parent=None):
super(self.__class__, self).__init__(parent)
self.controller = controller
self.setWindowTitle("Result Comparer")
main_layout = QtGui.QVBoxLayout()
receiverSplitter = QtGui.QSplitter()
receiverSplitter.setOrientation(QtCore.Qt.Horizontal)
self.left_receiver = Selection_receiver(
self.controller, self.setLeftResult)
receiverSplitter.addWidget(self.left_receiver)
self.right_receiver = Selection_receiver(
self.controller, self.setRightResult)
receiverSplitter.addWidget(self.right_receiver)
main_layout.addWidget(receiverSplitter)
self.left_receiver.horizontalScrollBar().valueChanged.connect(
self.right_receiver.horizontalScrollBar().setValue)
self.right_receiver.horizontalScrollBar().valueChanged.connect(
self.left_receiver.horizontalScrollBar().setValue)
self.left_receiver.verticalScrollBar().valueChanged.connect(
self.right_receiver.verticalScrollBar().setValue)
self.right_receiver.verticalScrollBar().valueChanged.connect(
self.left_receiver.verticalScrollBar().setValue)
self.right_source_model = None
self.right_proxy_model = None
self.right_proxy_model2 = None
self.left_source_model = None
self.left_proxy_model = None
self.left_proxy_model2 = None
self.setLayout(main_layout)
def setLeftResult(self, model_result):
self.left_source_model = model_result
self.left_proxy_model = MixRowProxyModel(
self.controller, "left", parent=self)
# self.left_proxy_model.sortedSignal.connect(self.onLeftChange)
self.left_proxy_model.setSourceModel(self.left_source_model)
self.left_proxy_model2 = MySortProxyModel(parent=self)
self.left_proxy_model2.sortedSignal.connect(self.onLeftChange)
self.left_proxy_model2.setSourceModel(self.left_proxy_model)
self.add_model_result(self.left_receiver, self.left_proxy_model2)
def setRightResult(self, model_result):
self.right_source_model = model_result
self.right_proxy_model = MixRowProxyModel(
self.controller, "right", parent=self)
self.right_proxy_model.setSourceModel(self.right_source_model)
# self.right_proxy_model.sortedSignal.connect(self.onRightChange)
self.right_proxy_model2 = MySortProxyModel(parent=self)
self.right_proxy_model2.sortedSignal.connect(self.onRightChange)
self.right_proxy_model2.setSourceModel(self.right_proxy_model)
self.add_model_result(self.right_receiver, self.right_proxy_model2)
def add_model_result(self, receiver, model_result):
receiver.setModel(model_result)
if(self.right_source_model is not None and self.left_source_model is not None):
self.link_result()
def link_result(self):
name_to_row = {}
for numSourceLeftRow in range(self.left_source_model.rowCount()):
res = self.left_source_model.getResultNumRow(numSourceLeftRow)
name = res.astronomical_object_name
if(name in name_to_row):
name_to_row[name][0].append(numSourceLeftRow)
else:
name_to_row[name] = ([numSourceLeftRow], [])
for numSourceRightRow in range(self.right_source_model.rowCount()):
res = self.right_source_model.getResultNumRow(numSourceRightRow)
name = res.astronomical_object_name
if(name in name_to_row):
name_to_row[name][1].append(numSourceRightRow)
else:
name_to_row[name] = ([], [numSourceRightRow])
self.mapLeftToRight = {} # key = leftRow; value = rightRow
self.mapRightToLeft = {} # key = rightRow; value = leftRow
for list_leftRow, list_rightRow in name_to_row.values():
if(len(list_rightRow) > 1):
print(
"Error more that index at right for same astronomical name {}".list_rightRow)
if(len(list_leftRow) > 1):
print(
"Error more that index at left for same astronomical name {}".list_leftRow)
if(len(list_leftRow) == 0):
leftRow = self.left_proxy_model.addEmptyRow()
else:
leftRow = list_leftRow[0]
if(len(list_rightRow) == 0):
rightRow = self.right_proxy_model.addEmptyRow()
else:
rightRow = list_rightRow[0]
self.mapLeftToRight[leftRow] = rightRow
self.mapRightToLeft[rightRow] = leftRow
self.left_receiver.rowCountChanged(
self.left_source_model.rowCount(), self.left_proxy_model.rowCount())
self.right_receiver.rowCountChanged(
self.right_source_model.rowCount(), self.right_proxy_model.rowCount())
print("Link Done : LtoR : {}; RtoL {}".format(
self.mapLeftToRight, self.mapRightToLeft))
def onRightChange(self, order):
print("RightChange")
self.change(self.left_source_model, self.left_proxy_model, self.left_proxy_model2, self.right_source_model, self.right_proxy_model,
self.right_proxy_model2, self.mapLeftToRight, self.left_receiver, order)
def onLeftChange(self, order):
print("LeftChange")
self.change(self.right_source_model, self.right_proxy_model, self.right_proxy_model2, self.left_source_model, self.left_proxy_model,
self.left_proxy_model2, self.mapRightToLeft, self.right_receiver, order)
def change(self, slave_source_model, slave_proxy_model,
slave_proxy_model2, master_source_model, master_proxy_model, master_proxy_model2,
map_slave_to_master, slave_receiver, order):
if(slave_source_model is not None):
slaveMapping = dict() # in slave table key = indexProxy , value = index Source
if(order == QtCore.Qt.AscendingOrder):
unlinkIndex = 0
else:
unlinkIndex = master_source_model.rowCount()
for slaveSourceRow in range(slave_source_model.rowCount()):
# this line is link to one in master, so we keep the same
# proxy number
master_source_row = map_slave_to_master[slaveSourceRow]
if(master_source_row != -1):
master_source_index = master_source_model.index(
master_source_row, 0)
master_proxy = master_proxy_model.mapFromSource(
master_source_index)
master_proxy2 = master_proxy_model2.mapFromSource(
master_proxy)
slaveProxyRow = master_proxy2.row() # same as master
else:
slaveProxyRow = unlinkIndex # we put it at the end or begining depending on order
unlinkIndex += 1
slaveMapping[slaveProxyRow] = slaveSourceRow
slave_proxy_model.layoutAboutToBeChanged.emit()
slave_proxy_model.setMap(slaveMapping)
slave_proxy_model2.setSortRole(
QtCore.Qt.InitialSortOrderRole) # proxy 2 is reinitialise
slave_proxy_model.layoutChanged.emit()
I'm trying to modify the excellent example of pyqt4 called "Editabletreemodel" but I have a problem I can't manage: after I add a new row in the model, how can I save or update the data I've inserted to a text file?
Or more in general, how is it possible to save data from the model/view into a file?
Thank you for your help.
# This is only needed for Python v2 but is harmless for Python v3.
import sip
sip.setapi('QVariant', 2)
from PyQt4 import QtCore, QtGui
import editabletreemodel
from ui_mainwindow import Ui_MainWindow
import sys, os, time
import paramiko
import threading
class TreeItem(object):
def __init__(self, data, parent=None):
self.parentItem = parent
self.itemData = data
self.childItems = []
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def childNumber(self):
if self.parentItem != None:
return self.parentItem.childItems.index(self)
return 0
def columnCount(self):
return len(self.itemData)
def data(self, column):
return self.itemData[column]
def insertChildren(self, position, count, columns):
if position < 0 or position > len(self.childItems):
return False
for row in range(count):
data = [None for v in range(columns)]
item = TreeItem(data, self)
self.childItems.insert(position, item)
return True
def insertColumns(self, position, columns):
if position < 0 or position > len(self.itemData):
return False
for column in range(columns):
self.itemData.insert(position, None)
for child in self.childItems:
child.insertColumns(position, columns)
return True
def parent(self):
return self.parentItem
def removeChildren(self, position, count):
if position < 0 or position + count > len(self.childItems):
return False
for row in range(count):
self.childItems.pop(position)
return True
def removeColumns(self, position, columns):
if position < 0 or position + columns > len(self.itemData):
return False
for column in range(columns):
self.itemData.pop(position)
for child in self.childItems:
child.removeColumns(position, columns)
return True
def setData(self, column, value):
if column < 0 or column >= len(self.itemData):
return False
self.itemData[column] = value
return True
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, headers, data, parent=None):
super(TreeModel, self).__init__(parent)
rootData = [header for header in headers]
self.rootItem = TreeItem(rootData)
self.setupModelData(data.split("\n"), self.rootItem)
def columnCount(self, parent=QtCore.QModelIndex()):
return self.rootItem.columnCount()
def data(self, index, role):
if not index.isValid():
return None
if role != QtCore.Qt.DisplayRole and role != QtCore.Qt.EditRole:
return None
item = self.getItem(index)
return item.data(index.column())
def flags(self, index):
if not index.isValid():
return 0
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def getItem(self, index):
if index.isValid():
item = index.internalPointer()
if item:
return item
return self.rootItem
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.rootItem.data(section)
return None
def index(self, row, column, parent=QtCore.QModelIndex()):
if parent.isValid() and parent.column() != 0:
return QtCore.QModelIndex()
parentItem = self.getItem(parent)
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def insertColumns(self, position, columns, parent=QtCore.QModelIndex()):
self.beginInsertColumns(parent, position, position + columns - 1)
success = self.rootItem.insertColumns(position, columns)
self.endInsertColumns()
return success
def insertRows(self, position, rows, parent=QtCore.QModelIndex()):
parentItem = self.getItem(parent)
self.beginInsertRows(parent, position, position + rows - 1)
success = parentItem.insertChildren(position, rows,
self.rootItem.columnCount())
self.endInsertRows()
return success
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = self.getItem(index)
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.childNumber(), 0, parentItem)
def removeColumns(self, position, columns, parent=QtCore.QModelIndex()):
self.beginRemoveColumns(parent, position, position + columns - 1)
success = self.rootItem.removeColumns(position, columns)
self.endRemoveColumns()
if self.rootItem.columnCount() == 0:
self.removeRows(0, rowCount())
return success
def removeRows(self, position, rows, parent=QtCore.QModelIndex()):
parentItem = self.getItem(parent)
self.beginRemoveRows(parent, position, position + rows - 1)
success = parentItem.removeChildren(position, rows)
self.endRemoveRows()
return success
def rowCount(self, parent=QtCore.QModelIndex()):
parentItem = self.getItem(parent)
return parentItem.childCount()
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role != QtCore.Qt.EditRole:
return False
item = self.getItem(index)
result = item.setData(index.column(), value)
if result:
self.dataChanged.emit(index, index)
return result
def setHeaderData(self, section, orientation, value, role=QtCore.Qt.EditRole):
if role != QtCore.Qt.EditRole or orientation != QtCore.Qt.Horizontal:
return False
result = self.rootItem.setData(section, value)
if result:
self.headerDataChanged.emit(orientation, section, section)
return result
def setupModelData(self, lines, parent):
parents = [parent]
indentations = [0]
number = 0
while number < len(lines):
position = 0
while position < len(lines[number]):
if lines[number][position] != " ":
break
position += 1
lineData = lines[number][position:].trimmed()
if lineData:
# Read the column data from the rest of the line.
columnData = [s for s in lineData.split('\t') if s]
if position > indentations[-1]:
# The last child of the current parent is now the new
# parent unless the current parent has no children.
if parents[-1].childCount() > 0:
parents.append(parents[-1].child(parents[-1].childCount() - 1))
indentations.append(position)
else:
while position < indentations[-1] and len(parents) > 0:
parents.pop()
indentations.pop()
# Append a new item to the current parent's list of children.
parent = parents[-1]
parent.insertChildren(parent.childCount(), 1,
self.rootItem.columnCount())
for column in range(len(columnData)):
parent.child(parent.childCount() -1).setData(column, columnData[column])
number += 1
class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
headers = ("Sendor Name", "Address", "Comments")
file = QtCore.QFile('./default.txt')
file.open(QtCore.QIODevice.ReadOnly)
model = TreeModel(headers, file.readAll())
file.close()
print model.invisibleRootItem()
self.view.setModel(model)
for column in range(model.columnCount(QtCore.QModelIndex())):
self.view.resizeColumnToContents(column)
self.exitAction.triggered.connect(QtGui.qApp.quit)
self.view.selectionModel().selectionChanged.connect(self.updateActions)
self.actionsMenu.aboutToShow.connect(self.updateActions)
self.insertRowAction.triggered.connect(self.insertRow)
self.insertColumnAction.triggered.connect(self.insertColumn)
self.removeRowAction.triggered.connect(self.removeRow)
self.removeColumnAction.triggered.connect(self.removeColumn)
self.insertChildAction.triggered.connect(self.insertChild)
self.callSensorsButton.clicked.connect(self.call_sensors)
self.updateActions()
self.view.expandAll()
self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.view.customContextMenuRequested.connect(self.openMenu)
self.connection = connection_thread()
self.connect(self.connection, QtCore.SIGNAL("started()"), self.start_progressBar)
self.connect(self.connection, QtCore.SIGNAL("finished()"), self.stop_progressBar)
self.connect(self.connection, QtCore.SIGNAL("terminated()"), self.stop_progressBar)
self.SaveListButton.clicked.connect(self.save_sensor_list)
self.pushButton.clicked.connect(self.prova)
def save_sensor_list(self):
index = self.view.selectionModel().currentIndex()
model = self.view.model()
print model.rootItem
for i in range(0, model.rootItem.rowCount()):
print model.child(i)
def prova(self):
index = self.view.selectionModel().currentIndex()
model = self.view.model()
print model.data(index,0)
def openMenu(self, position):
indexes = self.view.selectedIndexes()
model = self.view.model()
if len(indexes) > 0:
level = 0
index = indexes[0]
while index.parent().isValid():
index = index.parent()
level += 1
menu = QtGui.QMenu()
if level == 0:
menu.addAction(self.tr("Call all %ss" % (model.data(index,0))))
menu.addSeparator()
menu.addAction(self.tr("Add new sensor family"),self.insertRow)
menu.addAction(self.tr("Add new sensor"),self.insertChild)
elif level == 1:
menu.addAction(self.tr("Call this sensor"))
menu.addSeparator()
menu.addAction(self.tr("Add new sensor"),self.insertRow)
elif level == 2:
menu.addAction(self.tr("Edit object"))
menu.exec_(self.view.viewport().mapToGlobal(position))
def insertChild(self):
index = self.view.selectionModel().currentIndex()
model = self.view.model()
if model.columnCount(index) == 0:
if not model.insertColumn(0, index):
return
if not model.insertRow(0, index):
return
for column in range(model.columnCount(index)):
child = model.index(0, column, index)
model.setData(child, "[No data]", QtCore.Qt.EditRole)
if not model.headerData(column, QtCore.Qt.Horizontal).isValid():
model.setHeaderData(column, QtCore.Qt.Horizontal,
"[No header]", QtCore.Qt.EditRole)
self.view.selectionModel().setCurrentIndex(model.index(0, 0, index),
QtGui.QItemSelectionModel.ClearAndSelect)
self.updateActions()
def insertColumn(self, parent=QtCore.QModelIndex()):
model = self.view.model()
column = self.view.selectionModel().currentIndex().column()
# Insert a column in the parent item.
changed = model.insertColumn(column + 1, parent)
if changed:
model.setHeaderData(column + 1, QtCore.Qt.Horizontal,
"[No header]", QtCore.Qt.EditRole)
self.updateActions()
return changed
def insertRow(self):
index = self.view.selectionModel().currentIndex()
model = self.view.model()
if not model.insertRow(index.row()+1, index.parent()):
return
self.updateActions()
for column in range(model.columnCount(index.parent())):
child = model.index(index.row()+1, column, index.parent())
model.setData(child, "[No data]", QtCore.Qt.EditRole)
def removeColumn(self, parent=QtCore.QModelIndex()):
model = self.view.model()
column = self.view.selectionModel().currentIndex().column()
# Insert columns in each child of the parent item.
changed = model.removeColumn(column, parent)
if not parent.isValid() and changed:
self.updateActions()
return changed
def removeRow(self):
index = self.view.selectionModel().currentIndex()
model = self.view.model()
if (model.removeRow(index.row(), index.parent())):
self.updateActions()
def updateActions(self):
hasSelection = not self.view.selectionModel().selection().isEmpty()
self.removeRowAction.setEnabled(hasSelection)
self.removeColumnAction.setEnabled(hasSelection)
hasCurrent = self.view.selectionModel().currentIndex().isValid()
self.insertRowAction.setEnabled(hasCurrent)
self.insertColumnAction.setEnabled(hasCurrent)
if hasCurrent:
self.view.closePersistentEditor(self.view.selectionModel().currentIndex())
row = self.view.selectionModel().currentIndex().row()
column = self.view.selectionModel().currentIndex().column()
if self.view.selectionModel().currentIndex().parent().isValid():
self.statusBar().showMessage("Position: (%d,%d)" % (row, column))
else:
self.statusBar().showMessage("Position: (%d,%d) in top level" % (row, column))
def start_progressBar(self):
self.progressBar.setRange(0,0)
self.progressBar.setValue(0)
def stop_progressBar(self):
self.progressBar.setRange(0,1)
self.progressBar.setValue(1)
def call_sensors(self):
self.textEdit.insertPlainText("Connecting to Fox...\n")
self.connection.start_thread(self.textEdit)
class connection_thread(QtCore.QThread):
def __init__(self, parent = None):
QtCore.QThread.__init__(self, parent)
def start_thread(self,textEdit):
self.textEdit = textEdit
self.start()
def run(self):
print "Dentro il thread"
time.sleep(10)
try:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('192.168.0.90', username='root', password='netusg20')
self.textEdit.insertPlainText("Calling sensor list...\n")
app.processEvents()
stdin, stdout, stderr = ssh.exec_command('python g20.py c')
self.textEdit.insertPlainText(stdout.read())
self.textEdit.insertPlainText(stderr.read())
self.textEdit.insertPlainText("Connection closed\n")
ssh.close()
app.processEvents()
except:
self.textEdit.insertPlainText(str(sys.exc_info()[1]))
ssh.close()
app.processEvents()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
I guess the easies way would be to iterate though model items and save save to file via QDataStream. QDataStream supports reading\writing QVariant's and you can get\set the model item's data as QVariant. Below is a small example, I'm using QStandardItemModel for simplicity:
import sys
from PyQt4 import QtGui, QtCore
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
self.setMinimumSize(400, 400)
# create model
self.model = QtGui.QStandardItemModel()
for k in range(0, 4):
parentItem = self.model.invisibleRootItem()
for i in range(0, 4):
item = QtGui.QStandardItem(QtCore.QString("item %0 %1").arg(k).arg(i))
parentItem.appendRow(item)
parentItem = item
# create treeview
self.view = QtGui.QTreeView(self)
self.view.setModel(self.model)
self.view.setMinimumSize(300, 400)
self.saveButton = QtGui.QPushButton("save", self)
self.saveButton.move(300, 1)
self.saveButton.clicked.connect(self.on_save_button_clicked)
self.layout = QtGui.QVBoxLayout(self.centralWidget())
self.layout.addWidget(self.view)
self.layout.addWidget(self.saveButton)
def on_save_button_clicked(self):
# create text file
file = QtCore.QFile("save.txt")
file.open(QtCore.QIODevice.WriteOnly)
# open data stream
out = QtCore.QDataStream(file)
# recursively write model item into the datastream
self.save_item(self.model.invisibleRootItem(), out)
def save_item(self, item, out):
for i in range(0, item.rowCount()):
child = item.child(i)
child.write(out)
self.save_item(child, out)
def main():
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
hope this helps, regards