Related
Is there any way to find if an item in a tree view is in which state in PyQt5? ie, expanded or collapsed. I need to get the value in the custom delegate class that I have created.
class SummaryDelegate(QStyledItemDelegate):
def __init__(self, treeView):
super(SummaryDelegate, self).__init__()
self.treeView = treeView
self.headerItems = self.collectDictKeys(summarySectionData)
def collectDictKeys(self, data):
collection = []
for key, value in data.items():
collection.append(key)
if isinstance(value, dict):
collection.extend(self.collectDictKeys(value))
return collection
def paint(self, painter, option, index):
dataItem = index.data()
if dataItem in self.headerItems:
pass
else:
if type(index.data()) == str:
pass
else:
pass
newRec = QRect(option.rect)
newRec.setLeft(0)
painter.fillRect(newRec, QColor(240, 245, 255))
In paint, option.state will give you the state of the item including whether it's open (i.e. expanded) or not. It also has information about other state parameters of the item such as whether it's selected or not. For find out if the item is open or not you can use something like int(option.state) & QtWidgets.QStyle.State_Open. Similarly, int(option.state) & QtWidgets.QStyle.State_Selected will tell you whether the item is selected or not.
I've seen questions similar to this one but they are aimed at QTableView. This is not using that,, this is just for a dropdown (QComboBox) with a custom QAbstractTableModel, which needs to have 2 columns.
BIG UPDATE
(Note: Legacy code has been deleted as this is a better approach on the same question, and legacy code was confusing as hell).
Okay, so trying to catch up with what #eyllanesc explained, I changed this from a QAbstractListModel to a QAbstractTableModel. The result is:
class ModelForComboboxesWithID(QAbstractTableModel):
"""Create our basic model"""
def __init__(self, program, records):
super(ModelForComboboxesWithID, self).__init__()
self._data = records
self.program = program
self.path_images = program.PATH_IMAGES
def rowCount(self, index: int = 0) -> int:
"""The length of the outer list. Structure: [row, row, row]"""
if not self._data:
return 0 # Doubt: Do we need to return this if self._data is empty?
return len(self._data)
def columnCount(self, index: int = 0) -> int:
"""The length of the sub-list inside the outer list. Meaning that Columns are inside rows
Structure: [row [column], row [column], row [column]]"""
if not self._data:
return 0 # Doubt: Do we need to return this if self._data is empty?
return len(self._data[0])
def data(self, index, role=None):
"""return the data on this index as data[row][column]"""
# 1 - Display data based on its content (this edits the text that you visually see)
if role == Qt.DisplayRole:
value = self._data[index.row()][index.column()]
return value
# 2 - Tooltip displayed when hovering on it
elif role == Qt.ToolTipRole:
return f"ID: {self._data[index.row()][1]}"
Which I set this way:
def eventFilter(self, target, event: QEvent):
if event.type() == QEvent.MouseButtonPress:
if target == self.Buscadorcombo_cliente:
records = ... # my query to the database
set_combo_records_with_ids(self.program, target, records)
target.currentIndexChanged.connect(self.test)
def set_combo_records_with_ids(program, combobox: QComboBox, records):
"""Clear combobox, set model/data and sort it"""
combobox.clear()
model = ModelForComboboxesWithID(program, records)
combobox.setModel(model)
combobox.model().sort(0, Qt.AscendingOrder)
combobox.setModelColumn(0)
The result of this works almost perfect:
On the dropdown(Combobox) it displays the name.
If you hover on an item, it displays the ID.
Now I am able to get any data of it this way.
def test(self, index):
data_id = self.Buscadorcombo_cliente.model().index(index, 1).data()
data_name = self.Buscadorcombo_cliente.model().index(index, 0).data()
print(data_id)
print(data_name)
You have to set a QTableView as a view:
from PySide2 import QtGui, QtWidgets
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
combo = QtWidgets.QComboBox()
model = QtGui.QStandardItemModel(0, 2)
for i in range(10):
items = []
for j in range(model.columnCount()):
it = QtGui.QStandardItem(f"it-{i}{j}")
items.append(it)
model.appendRow(items)
combo.setModel(model)
view = QtWidgets.QTableView(
combo, selectionBehavior=QtWidgets.QAbstractItemView.SelectRows
)
combo.setView(view)
view.verticalHeader().hide()
view.horizontalHeader().hide()
header = view.horizontalHeader()
for i in range(header.count()):
header.setSectionResizeMode(i, QtWidgets.QHeaderView.Stretch)
lay = QtWidgets.QVBoxLayout(w)
lay.addWidget(combo)
lay.addStretch()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
This might be more appropriate as a bug report, but maybe I'm just misunderstanding something.
I want to implement my own hierarchical QAbstractItemModel to show a tree of objects in a QTreeView. I've started with the editable tree model example [1] which works fine.
However, I need more metadata in my indexes as I have differend types of nodes (or rather nodes have properties) and a simple (row, column, object-pointer)-tuple is not enough. See also Storing two different types in QModelIndex for another example.
So I've created another datastructure which holds this data:
# this is just an example
class TempContainer:
def __init__(self, name, obj):
self.name = name
self.obj = obj
And I create an index like this:
# in TreeModel.index():
o = TempContainer("child", childItem)
self.tmpcache.append(o) # just to keep a reference alive
return self.createIndex(row, column, o)
Accessing objects can now be implemented as follows:
def getItem(self, index):
if index.isValid():
item = index.internalPointer()
if item:
return item.obj
return self.rootItem
But if I add this layer of metadata, selecting and editing items does not work anymore. I can still select cells in first colum of root items and edit all items in the first column. But eg. selecting sub-items or items in the second column doesn't work anymore!
I've noticed that it works if I store my TempContainer inside the corresponding TreeItem, but this is not really an option as there are a multiple containers that point to the same item in my full code.
I'm running python 3.5.3 under Linux (Fedora 24), with PyQt 5.6.2 (I've also tried 5.8.2) and Qt 5.6.2.
[1] Unfortunately, I cannot attach a ZIP of the full source, but the code is available under https://www.riverbankcomputing.com/software/pyqt/download5, in /examples/itemviews/editabletreemodel
The full source of the modified editabletreemodel.py:
(Note that I only modified a few lines and that you will still need the other files from the example to run this.)
#!/usr/bin/env python
#############################################################################
##
## Copyright (C) 2013 Riverbank Computing Limited.
## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
## All rights reserved.
##
## This file is part of the examples of PyQt.
##
## $QT_BEGIN_LICENSE:BSD$
## You may use this file under the terms of the BSD license as follows:
##
## "Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are
## met:
## * Redistributions of source code must retain the above copyright
## notice, this list of conditions and the following disclaimer.
## * Redistributions in binary form must reproduce the above copyright
## notice, this list of conditions and the following disclaimer in
## the documentation and/or other materials provided with the
## distribution.
## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
## the names of its contributors may be used to endorse or promote
## products derived from this software without specific prior written
## permission.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
## $QT_END_LICENSE$
##
#############################################################################
from PyQt5.QtCore import (QAbstractItemModel, QFile, QIODevice,
QItemSelectionModel, QModelIndex, Qt)
from PyQt5.QtWidgets import QApplication, QMainWindow
import editabletreemodel_rc
from ui_mainwindow import Ui_MainWindow
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 TempContainer:
def __init__(self, name, obj):
self.name = name
self.obj = obj
class TreeModel(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)
self.tmpcache = []
def columnCount(self, parent=QModelIndex()):
return self.rootItem.columnCount()
def data(self, index, role):
if not index.isValid():
return None
if role != Qt.DisplayRole and role != Qt.EditRole:
return None
item = self.getItem(index)
return item.data(index.column())
def flags(self, index):
if not index.isValid():
return 0
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
def getItem(self, index):
if index.isValid():
item = index.internalPointer()
if item:
return item.obj
return self.rootItem
def headerData(self, section, orientation, role=Qt.DisplayRole):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.rootItem.data(section)
return None
def index(self, row, column, parent=QModelIndex()):
if parent.isValid() and parent.column() != 0:
return QModelIndex()
parentItem = self.getItem(parent)
childItem = parentItem.child(row)
if childItem:
o = TempContainer("child", childItem)
self.tmpcache.append(o)
return self.createIndex(row, column, o)
else:
return QModelIndex()
def insertColumns(self, position, columns, parent=QModelIndex()):
self.beginInsertColumns(parent, position, position + columns - 1)
success = self.rootItem.insertColumns(position, columns)
self.endInsertColumns()
return success
def insertRows(self, position, rows, parent=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 QModelIndex()
childItem = self.getItem(index)
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QModelIndex()
o = TempContainer("parent", parentItem)
self.tmpcache.append(o)
return self.createIndex(parentItem.childNumber(), 0, o)
def removeColumns(self, position, columns, parent=QModelIndex()):
self.beginRemoveColumns(parent, position, position + columns - 1)
success = self.rootItem.removeColumns(position, columns)
self.endRemoveColumns()
if self.rootItem.columnCount() == 0:
self.removeRows(0, self.rowCount())
return success
def removeRows(self, position, rows, parent=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=QModelIndex()):
parentItem = self.getItem(parent)
return parentItem.childCount()
def setData(self, index, value, role=Qt.EditRole):
if role != 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=Qt.EditRole):
if role != Qt.EditRole or orientation != 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(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
headers = ("Title", "Description")
file = QFile(':/default.txt')
file.open(QIODevice.ReadOnly)
model = TreeModel(headers, file.readAll())
file.close()
self.view.setModel(model)
for column in range(model.columnCount()):
self.view.resizeColumnToContents(column)
self.exitAction.triggered.connect(QApplication.instance().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.updateActions()
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]", Qt.EditRole)
if model.headerData(column, Qt.Horizontal) is None:
model.setHeaderData(column, Qt.Horizontal, "[No header]",
Qt.EditRole)
self.view.selectionModel().setCurrentIndex(model.index(0, 0, index),
QItemSelectionModel.ClearAndSelect)
self.updateActions()
def insertColumn(self):
model = self.view.model()
column = self.view.selectionModel().currentIndex().column()
changed = model.insertColumn(column + 1)
if changed:
model.setHeaderData(column + 1, Qt.Horizontal, "[No header]",
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]", Qt.EditRole)
def removeColumn(self):
model = self.view.model()
column = self.view.selectionModel().currentIndex().column()
changed = model.removeColumn(column)
if 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))
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
EDIT:
I realize the example I gave is rather pointless, TempContainer serves no real purpose! It is just a minimal example that shows the problem I ran into. As to why TempContainer is required, suppose your actual TreeItems looks like this:
class TreeItem:
def __init__(self,
name:str, description:str,
data:List[TreeItem], metadata:Dict[str,str],
parent:TreeItem=None):
...
Which should be rendered in the TableView as:
itemA somedescription here
itemB this is another item
\--metadata
| \--created yesterday
| \--color green
\--data
\--itemC this is a sub-item of itemB
| \--metadata
| \--data
\--itemD and another item
Then you would need an index to e.g. 'name of the second metadata item of itemB'. You could create some sort of shadow-tree which reflects this structure but consists of normal TreeItems, but this might get cumbersome, especially with editing. My idea was to store in the index not only a pointer to a TreeItem but also annotate it with a sub-index, indicating which part of a TreeItem we're pointing at.
The indexes you are creating are almost OK, I think, but they have to fulfil a little bit more of the contract. Specifically they need to support a valid equals method. In your model index method you create a new TempContainer inside the index. That means that when the view compares that index with another one it has earlier requested for the same place in the tree, then they won't compare equal. That's because it's using the default equals on the QModelIndex class which compares row, column, and internalPointer for equality. The docs say:
All values in the model index are used when comparing with another
model index
While your TempContainer idea seems initially attractive as a way of adapting your tree to the view, this is not easy to do, as you are finding. Qt essentially expects the model to have a clear tree structure matching that in the view, and it's difficult to avoid. In cases like this I have always created a tree layer of Python objects whose references I use as the internalPointer.
I have a QTableView that dynamically loads data from a custom model that inherits QAbstractItemModel. The model implements both fetchMore and canFetchMore.
The problem is that I would like to be able to select all rows for small datasets, but if I hit ctrl-a in the view it only will select the rows that are currently loaded.
Is there some mechanism to force the QTableView to fetch more rows? Ideally I would like to show a progress bar indicating the fraction of data that has been loaded from the model. Every few seconds I would like to force the model to load a bit more of the data, but I still want to let the user interact with the data that has been loaded so far. This way when the progress bar is complete the user can press ctrl-a and be confident that all data is selected.
Edit: I have another motivating use case. I want to jump to a specific row, but if that row is not loaded my interface does nothing.
How can I force a QAbstractItemModel to fetch more (or up to a specific row) and then force the QTableView to show it?
If I don't implement fetchMore and canFetchMore, the previous functionality works, but loading the tables is very slow. When I implement those methods the opposite happens. Not having an answer to this problem is causing issues with the usability of my qt interface, so I'm opening a bounty for this question.
Here is a method I'm using to select a specific row.
def select_row_from_id(view, _id, scroll=False, collapse=True):
"""
_id is from the iders function (i.e. an ibeis rowid)
selects the row in that view if it exists
"""
with ut.Timer('[api_item_view] select_row_from_id(id=%r, scroll=%r, collapse=%r)' %
(_id, scroll, collapse)):
qtindex, row = view.get_row_and_qtindex_from_id(_id)
if row is not None:
if isinstance(view, QtWidgets.QTreeView):
if collapse:
view.collapseAll()
select_model = view.selectionModel()
select_flag = QtCore.QItemSelectionModel.ClearAndSelect
#select_flag = QtCore.QItemSelectionModel.Select
#select_flag = QtCore.QItemSelectionModel.NoUpdate
with ut.Timer('[api_item_view] selecting name. qtindex=%r' % (qtindex,)):
select_model.select(qtindex, select_flag)
with ut.Timer('[api_item_view] expanding'):
view.setExpanded(qtindex, True)
else:
# For Table Views
view.selectRow(row)
# Scroll to selection
if scroll:
with ut.Timer('scrolling'):
view.scrollTo(qtindex)
return row
return None
If the user has manually scrolled past the row in question then this function works. However, if the user has not seen the specific row this function just scrolls back to the top of the view.
It's probably too late for the answer here but maybe it would still benefit someone in future.
Below one can find a working example of a list model with canFetchMore and fetchMore methods + a view with a couple of custom methods:
Method trying to load more items from the model, if the model has something not loaded yet
Method capable of fetching the specific rows from the model if they haven't been loaded yet
The QMainWindow subclass in the example has a timer which is used to repeatedly call the first of the above mentioned methods, each time forcing the load of another batch of items from the model into the view. The loading of items in batches over small time intervals allows one to avoid blocking the UI thread completely and be able to edit the items loaded so far with little to no lag. The example contains a progress bar showing the part of items loaded so far.
The QMainWindow subclass also has a spin box which allows one to pick a particular row to show in the view. If the corresponding item has already been fetched from the model, the view simply scrolls to it. Otherwise it fetches this row's item from the model first, in a synchronous i.e. UI blocking fashion.
Here's the full code of the solution, tested with python 3.5.2 and PyQt5:
import sys
from PyQt5 import QtWidgets, QtCore
class DelayedFetchingListModel(QtCore.QAbstractListModel):
def __init__(self, batch_size=100, max_num_nodes=1000):
QtCore.QAbstractListModel.__init__(self)
self.batch_size = batch_size
self.nodes = []
for i in range(0, self.batch_size):
self.nodes.append('node ' + str(i))
self.max_num_nodes = max(self.batch_size, max_num_nodes)
def flags(self, index):
if not index.isValid():
return QtCore.Qt.ItemIsEnabled
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable;
def rowCount(self, index):
if index.isValid():
return 0
return len(self.nodes)
def data(self, index, role):
if not index.isValid():
return None
if role != QtCore.Qt.DisplayRole:
return None
row = index.row()
if row < 0 or row >= len(self.nodes):
return None
else:
return self.nodes[row]
def setData(self, index, value, role):
if not index.isValid():
return False
if role != QtCore.Qt.EditRole:
return False
row = index.row()
if row < 0 or row >= len(self.nodes):
return False
self.nodes[row] = value
self.dataChanged.emit(index, index)
return True
def headerData(self, section, orientation, role):
if section != QtCore.Qt.Horizontal:
return None
if section != 0:
return None
if role != QtCore.Qt.DisplayRole:
return None
return 'node'
def canFetchMore(self, index):
if index.isValid():
return False
return (len(self.nodes) < self.max_num_nodes)
def fetchMore(self, index):
if index.isValid():
return
current_len = len(self.nodes)
target_len = min(current_len + self.batch_size, self.max_num_nodes)
self.beginInsertRows(index, current_len, target_len - 1)
for i in range(current_len, target_len):
self.nodes.append('node ' + str(i))
self.endInsertRows()
class ListView(QtWidgets.QListView):
def __init__(self, parent=None):
QtWidgets.QListView.__init__(self, parent)
def jumpToRow(self, row):
model = self.model()
if model == None:
return False
num_rows = model.rowCount()
while(row >= num_rows):
res = fetchMoreRows(QtCore.QModelIndex())
if res == False:
return False
num_rows = model.rowCount()
index = model.index(row, 0, QtCore.QModelIndex())
self.scrollTo(index, QtCore.QAbstractItemView.PositionAtCenter)
return True
def fetchMoreRows(self, index):
model = self.model()
if model == None:
return False
if not model.canFetchMore(index):
return False
model.fetchMore(index)
return True
class MainForm(QtWidgets.QMainWindow):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
# Setup the model
self.max_num_nodes = 10000
self.batch_size = 100
self.model = DelayedFetchingListModel(batch_size=self.batch_size, max_num_nodes=self.max_num_nodes)
# Setup the view
self.view = ListView()
self.view.setModel(self.model)
# Update the currently selected row in the spinbox
self.view.selectionModel().currentChanged.connect(self.onCurrentItemChanged)
# Select the first row in the model
index = self.model.index(0, 0, QtCore.QModelIndex())
self.view.selectionModel().clearSelection()
self.view.selectionModel().select(index, QtCore.QItemSelectionModel.Select)
# Setup the spinbox
self.spinBox = QtWidgets.QSpinBox()
self.spinBox.setMinimum(0)
self.spinBox.setMaximum(self.max_num_nodes-1)
self.spinBox.setSingleStep(1)
self.spinBox.valueChanged.connect(self.onSpinBoxNewValue)
# Setup the progress bar showing the status of model data loading
self.progressBar = QtWidgets.QProgressBar()
self.progressBar.setRange(0, self.max_num_nodes)
self.progressBar.setValue(0)
self.progressBar.valueChanged.connect(self.onProgressBarValueChanged)
# Add status bar but initially hidden, will only show it if there's something to say
self.statusBar = QtWidgets.QStatusBar()
self.statusBar.hide()
# Collect all this stuff into a vertical layout
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.view)
self.layout.addWidget(self.spinBox)
self.layout.addWidget(self.progressBar)
self.layout.addWidget(self.statusBar)
self.window = QtWidgets.QWidget()
self.window.setLayout(self.layout)
self.setCentralWidget(self.window)
# Setup timer to fetch more data from the model over small time intervals
self.timer = QtCore.QBasicTimer()
self.timerPeriod = 1000
self.timer.start(self.timerPeriod, self)
def onCurrentItemChanged(self, current, previous):
if not current.isValid():
return
row = current.row()
self.spinBox.setValue(row)
def onSpinBoxNewValue(self, value):
try:
value_int = int(value)
except ValueError:
return
num_rows = self.model.rowCount(QtCore.QModelIndex())
if value_int >= num_rows:
# There is no such row within the model yet, trying to fetch more
while(True):
res = self.view.fetchMoreRows(QtCore.QModelIndex())
if res == False:
# We shouldn't really get here in this example since out
# spinbox's range is limited by exactly the number of items
# possible to fetch but generally it's a good idea to handle
# cases like this, when someone requests more rows than
# the model has
self.statusBar.show()
self.statusBar.showMessage("Can't jump to row %d, the model has only %d rows" % (value_int, self.model.rowCount(QtCore.QModelIndex())))
return
num_rows = self.model.rowCount(QtCore.QModelIndex())
if value_int < num_rows:
break;
if num_rows < self.max_num_nodes:
# If there are still items to fetch more, check if we need to update the progress bar
if self.progressBar.value() < value_int:
self.progressBar.setValue(value_int)
elif num_rows == self.max_num_nodes:
# All items are loaded, nothing to fetch more -> no need for the progress bar
self.progressBar.hide()
# Update the selection accordingly with the new row and scroll to it
index = self.model.index(value_int, 0, QtCore.QModelIndex())
selectionModel = self.view.selectionModel()
selectionModel.clearSelection()
selectionModel.select(index, QtCore.QItemSelectionModel.Select)
self.view.scrollTo(index, QtWidgets.QAbstractItemView.PositionAtCenter)
# Ensure the status bar is hidden now
self.statusBar.hide()
def timerEvent(self, event):
res = self.view.fetchMoreRows(QtCore.QModelIndex())
if res == False:
self.timer.stop()
else:
self.progressBar.setValue(self.model.rowCount(QtCore.QModelIndex()))
if not self.timer.isActive():
self.timer.start(self.timerPeriod, self)
def onProgressBarValueChanged(self, value):
if value >= self.max_num_nodes:
self.progressBar.hide()
def main():
app = QtWidgets.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
One more thing I'd like to note is that this example expects the fetchMore method to do its work synchronously. But in more sophisticated approaches fetchMore doesn't actually have to act so. If your model loads its items from, say, a database then talking with the database synchronously in the UI thread would be a bad idea. Instead fetchMore implementation could start the asynchronous sequence of signal/slot communications with some object handling the communication with the database occurring in some background thread.
a self-using model class, based on Dmitry's answer.
class EzQListModel(QAbstractListModel):
items_changed = Signal()
an_item_changed = Signal(int)
def __init__(self, batch_size=100, items_header='Items', parent=None):
super().__init__(parent)
self._batch_size = batch_size
self._current_size = 0
self._items = []
self.items_header = items_header
self.data_getter_mapping = {Qt.DisplayRole: self.get_display_data, Qt.BackgroundRole: self.get_background_data}
#property
def items_size(self):
return len(self._items)
def update_fetch_more(self):
if self.canFetchMore():
self.fetchMore()
return self
#contextlib.contextmanager
def ctx_change_items(self):
yield
self.items_changed.emit()
#contextlib.contextmanager
def ctx_change_an_item(self, index):
yield
self.an_item_changed.emit(index)
def clear_items(self):
with self.ctx_change_items():
self._items.clear()
self._current_size = 0
return self
def append_item(self, x):
with self.ctx_change_items():
self._items.append(x)
return self
def insert_item(self, index, x):
with self.ctx_change_items():
self._items.insert(index, x)
return self
def extend_items(self, items):
with self.ctx_change_items():
self._items.extend(items)
return self
def get_item(self, index):
return self._items[index]
def set_item(self, index, value):
with self.ctx_change_items():
with self.ctx_change_an_item(index):
self._items[index] = value
return self
def flags(self, index):
if not index.isValid():
return Qt.ItemIsEnabled
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
def rowCount(self, parent=QModelIndex()):
if parent.isValid():
return 0
n = self._current_size
if n <= self.items_size:
return n
else:
self._current_size = self.items_size
return self.items_size
#staticmethod
def get_none_data(index):
return None
def get_display_data(self, index: QModelIndex):
return self._items[index.row()]
#staticmethod
def get_background_data(index: QModelIndex):
palette = QApplication.palette()
return palette.alternateBase() if index.row() % 2 else palette.base()
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
if self.items_size <= index.row() < 0:
return None
return self.data_getter_mapping.get(role, self.get_none_data)(index)
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
if role != Qt.EditRole:
return False
row = index.row()
if self.items_size <= row < 0:
return False
self._items[row] = value
self.dataChanged.emit(index, index)
# print(self.setData.__name__, row, self._items[row], self.data(index))
return True
def headerData(self, section, orientation, role=None):
if orientation != Qt.Horizontal:
return None
if section != 0:
return None
if role != Qt.DisplayRole:
return None
return self.items_header
def canFetchMore(self, parent: QModelIndex = QModelIndex()):
if parent.isValid():
return False
return self._current_size < self.items_size
def fetchMore(self, parent: QModelIndex = QModelIndex()):
if parent.isValid():
return
fcls = FirstCountLastStop().set_first_and_total(self._current_size,
min(self.items_size - self._current_size, self._batch_size))
self.beginInsertRows(parent, fcls.first, fcls.last)
self.endInsertRows()
self._current_size += fcls.total
class FirstCountLastStop:
def __init__(self):
self.first = 0
self.total = 0
self.last = 0
self.stop = 1
def set_first_and_total(self, first, count):
self.first = first
self.total = count
self.stop = first + count
self.last = self.stop - 1
return self
I want to produce a simple enough application which uses a QTreeView widget to show hierarchical data from a SQLite3 (flat) table, use QDataWidgetMapper to populate some lineedit fields, allow user to edit, which in turn updates the table. Simple & basic (for most!).
I have been working on the basis that the following process would be the best way of doing this:
Connect to Dbase
Query data
Create and populate custom QAbstractItemModel from the data (manipulating it through a dict to create nodes, parents and children dynamically - for each dict entry a 'node' is generated with an associated parent)
Use QDatawidgetmapper to populate other widgets
User edits data
QAbstractItemModel (QAIM) is updated
Then have to run an UPDATE, INSERT or whatever query using new values in the QAIM model.
Refresh the QAIM and associated widgets.
I realise if I were just using a QTableView or QListView I would not need the custom model and could just write straight back into the database. The process I have outlined above seems to mean having to keep two sets of data going - i.e. the SQLite table and the custom QAIM and ensure that they are both kept up to date. This seems a bit cumbersome to me and I'm sure there must be a better way of doing it where the QTreeView is taking its data straight from the SQLite table - with the obvious need for some manipulation to convert the flat data into hierarchical data.
I am wondering, of course, whether I have completely misunderstood the relationship between QAbstractItemModel and the QSQL*Models and I am overcomplicating it through ignorance?
Thanks
What you want is a proxy model that acts as a bridge between QSql*Model and the view. For that, you need to subclass QAbstractProxyModel. You have to have a consistent way of finding parent-child relationships in proxy model and mapping them to the source model, so that might require keeping some tally in the proxy model.
When you are sub-classing QAbstractProxyModel, you need to re-define, at minimum, these methods:
rowCount
columnCount
parent
index
data
mapToSource
mapFromSource
Also, keep in mind that QAbstractProxyModel does not auto-propagate signals through. So, in order to have the view be aware of changes in source model (like insert, delete, update), you need to pass them in the proxy model (while of course, updating your mappings in the proxy model).
It will require some work, but in the end you'll have a more flexible structure. And it will eliminate all the stuff that you need to do for synchronizing database and custom QAbstractItemModel.
Edit
A custom proxy model that groups items from a flat model according to a given column:
import sys
from collections import namedtuple
import random
from PyQt4 import QtCore, QtGui
groupItem = namedtuple("groupItem",["name","children","index"])
rowItem = namedtuple("rowItem",["groupIndex","random"])
class GrouperProxyModel(QtGui.QAbstractProxyModel):
def __init__(self, parent=None):
super(GrouperProxyModel, self).__init__(parent)
self._rootItem = QtCore.QModelIndex()
self._groups = [] # list of groupItems
self._groupMap = {} # map of group names to group indexes
self._groupIndexes = [] # list of groupIndexes for locating group row
self._sourceRows = [] # map of source rows to group index
self._groupColumn = 0 # grouping column.
def setSourceModel(self, source, groupColumn=0):
super(GrouperProxyModel, self).setSourceModel(source)
# connect signals
self.sourceModel().columnsAboutToBeInserted.connect(self.columnsAboutToBeInserted.emit)
self.sourceModel().columnsInserted.connect(self.columnsInserted.emit)
self.sourceModel().columnsAboutToBeRemoved.connect(self.columnsAboutToBeRemoved.emit)
self.sourceModel().columnsRemoved.connect(self.columnsRemoved.emit)
self.sourceModel().rowsInserted.connect(self._rowsInserted)
self.sourceModel().rowsRemoved.connect(self._rowsRemoved)
self.sourceModel().dataChanged.connect(self._dataChanged)
# set grouping
self.groupBy(groupColumn)
def rowCount(self, parent):
if parent == self._rootItem:
# root level
return len(self._groups)
elif parent.internalPointer() == self._rootItem:
# children level
return len(self._groups[parent.row()].children)
else:
return 0
def columnCount(self, parent):
if self.sourceModel():
return self.sourceModel().columnCount(QtCore.QModelIndex())
else:
return 0
def index(self, row, column, parent):
if parent == self._rootItem:
# this is a group
return self.createIndex(row,column,self._rootItem)
elif parent.internalPointer() == self._rootItem:
return self.createIndex(row,column,self._groups[parent.row()].index)
else:
return QtCore.QModelIndex()
def parent(self, index):
parent = index.internalPointer()
if parent == self._rootItem:
return self._rootItem
else:
parentRow = self._getGroupRow(parent)
return self.createIndex(parentRow,0,self._rootItem)
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
parent = index.internalPointer()
if parent == self._rootItem:
return self._groups[index.row()].name
else:
parentRow = self._getGroupRow(parent)
sourceRow = self._sourceRows.index(self._groups[parentRow].children[index.row()])
sourceIndex = self.createIndex(sourceRow, index.column(), 0)
return self.sourceModel().data(sourceIndex, role)
return None
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
return self.sourceModel().headerData(section, orientation, role)
def mapToSource(self, index):
if not index.isValid():
return QtCore.QModelIndex()
parent = index.internalPointer()
if not parent.isValid():
return QtCore.QModelIndex()
elif parent == self._rootItem:
return QtCore.QModelIndex()
else:
rowItem_ = self._groups[parent.row()].children[index.row()]
sourceRow = self._sourceRows.index(rowItem_)
return self.createIndex(sourceRow, index.column(), QtCore.QModelIndex())
def mapFromSource(self, index):
rowItem_ = self._sourceRows[index.row()]
groupRow = self._getGroupRow(rowItem_.groupIndex)
itemRow = self._groups[groupRow].children.index(rowItem_)
return self.createIndex(itemRow,index.column(),self._groupIndexes[groupRow])
def _clearGroups(self):
self._groupMap = {}
self._groups = []
self._sourceRows = []
def groupBy(self,column=0):
self.beginResetModel()
self._clearGroups()
self._groupColumn = column
sourceModel = self.sourceModel()
for row in range(sourceModel.rowCount(QtCore.QModelIndex())):
groupName = sourceModel.data(self.createIndex(row,column,0),
QtCore.Qt.DisplayRole)
groupIndex = self._getGroupIndex(groupName)
rowItem_ = rowItem(groupIndex,random.random())
self._groups[groupIndex.row()].children.append(rowItem_)
self._sourceRows.append(rowItem_)
self.endResetModel()
def _getGroupIndex(self, groupName):
""" return the index for a group denoted with name.
if there is no group with given name, create and then return"""
if groupName in self._groupMap:
return self._groupMap[groupName]
else:
groupRow = len(self._groupMap)
groupIndex = self.createIndex(groupRow,0,self._rootItem)
self._groupMap[groupName] = groupIndex
self._groups.append(groupItem(groupName,[],groupIndex))
self._groupIndexes.append(groupIndex)
self.layoutChanged.emit()
return groupIndex
def _getGroupRow(self, groupIndex):
for i,x in enumerate(self._groupIndexes):
if id(groupIndex)==id(x):
return i
return 0
def _rowsInserted(self, parent, start, end):
for row in range(start, end+1):
groupName = self.sourceModel().data(self.createIndex(row,self._groupColumn,0),
QtCore.Qt.DisplayRole)
groupIndex = self._getGroupIndex(groupName)
self._getGroupRow(groupIndex)
groupItem_ = self._groups[self._getGroupRow(groupIndex)]
rowItem_ = rowItem(groupIndex,random.random())
groupItem_.children.append(rowItem_)
self._sourceRows.insert(row, rowItem_)
self.layoutChanged.emit()
def _rowsRemoved(self, parent, start, end):
for row in range(start, end+1):
rowItem_ = self._sourceRows[start]
groupIndex = rowItem_.groupIndex
groupItem_ = self._groups[self._getGroupRow(groupIndex)]
childrenRow = groupItem_.children.index(rowItem_)
groupItem_.children.pop(childrenRow)
self._sourceRows.pop(start)
if not len(groupItem_.children):
# remove the group
groupRow = self._getGroupRow(groupIndex)
groupName = self._groups[groupRow].name
self._groups.pop(groupRow)
self._groupIndexes.pop(groupRow)
del self._groupMap[groupName]
self.layoutChanged.emit()
def _dataChanged(self, topLeft, bottomRight):
topRow = topLeft.row()
bottomRow = bottomRight.row()
sourceModel = self.sourceModel()
# loop through all the changed data
for row in range(topRow,bottomRow+1):
oldGroupIndex = self._sourceRows[row].groupIndex
oldGroupItem = self._groups[self._getGroupRow(oldGroupIndex)]
newGroupName = sourceModel.data(self.createIndex(row,self._groupColumn,0),QtCore.Qt.DisplayRole)
if newGroupName != oldGroupItem.name:
# move to new group...
newGroupIndex = self._getGroupIndex(newGroupName)
newGroupItem = self._groups[self._getGroupRow(newGroupIndex)]
rowItem_ = self._sourceRows[row]
newGroupItem.children.append(rowItem_)
# delete from old group
oldGroupItem.children.remove(rowItem_)
if not len(oldGroupItem.children):
# remove the group
groupRow = self._getGroupRow(oldGroupItem.index)
groupName = oldGroupItem.name
self._groups.pop(groupRow)
self._groupIndexes.pop(groupRow)
del self._groupMap[groupName]
self.layoutChanged.emit()