In GUI applications one comes across a situation where there is a need
to display a lot of items in a tabular or list format (for example
displaying large number of rows in a table). One way to increase the
GUI responsiveness is to load a few items when the screen is displayed
and defer loading of rest of the items based on user action. Qt
provides a solution to address this requirement of loading the data on
demand.
The class QAbstractItemModel class in Qt one of the
Model/View classes and is part of the Qt’s model/view framework. It
defines a standard interface that its subclasses must override to
interact with other components in the model/view framework. It
provides methods canFetchMore and fetchMore which
can be implemented in the model subclasses to achieve the goal of
loading the data on demand in the views.
To generate a tabular display in Qt it is quite common to create
an instance of QTableView class and specify a model for this class
which is an instance of QAbstractItemModel
Consider, the below sample code that displays name of a person along
with the city:
#!/usr/bin/env python
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Person(object):
"""Name of the person along with his city"""
def __init__(self,name,city):
self.name = name
self.city = city
class PersonDisplay(QMainWindow):
def __init__(self,parent=None):
super(PersonDisplay,self).__init__(parent)
self.setWindowTitle('Person City')
view = QTableView()
tableData = PersonTableModel()
view.setModel(tableData)
self.setCentralWidget(view)
tableData.addPerson(Person('Ramesh', 'Delhi'))
tableData.addPerson(Person('Suresh', 'Chennai'))
tableData.addPerson(Person('Kiran', 'Bangalore'))
class PersonTableModel(QAbstractTableModel):
"""Model class that drives the population of tabular display"""
def __init__(self):
super(PersonTableModel,self).__init__()
self.headers = ['Name','City']
self.persons = []
def rowCount(self,index=QModelIndex()):
return len(self.persons)
def addPerson(self,person):
self.beginResetModel()
self.persons.append(person)
self.endResetModel()
def columnCount(self,index=QModelIndex()):
return len(self.headers)
def data(self,index,role=Qt.DisplayRole):
col = index.column()
person = self.persons[index.row()]
if role == Qt.DisplayRole:
if col == 0:
return QVariant(person.name)
elif col == 1:
return QVariant(person.city)
return QVariant()
def headerData(self,section,orientation,role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return QVariant()
if orientation == Qt.Horizontal:
return QVariant(self.headers[section])
return QVariant(int(section + 1))
def start():
app = QApplication(sys.argv)
appWin = PersonDisplay()
appWin.show()
app.exec_()
if __name__ == "__main__":
start()
When the number of persons whose details that needs to be displayed
are fewer and when there is not a lot of processing to be done to
populate the cell values, the details are displayed without a
significant delay. But in cases where there lot of rows or where some
amount of computation is needed to display each attribute of a cell
then it makes sense to load only a few rows to the table and load
remaining rows based on the user action (like when user scrolls
down). The pagination of data loads the corresponding GUI screen
faster providing a better user experience.
To paginate the table data only the code in the model class
(PersonTableModel in our example) needs to be modified, which
demonstrates the benefits of clear separation between model and view
class that Qt provides. The methods canFetchMore and
fetchMore needs to be overridden in the child class
PersonTableModel to paginate the table data.
Below is the example code which shows only the modified table model
class, rest of the code remains same as above code example:
class PersonTableModel(QAbstractTableModel):
ROW_BATCH_COUNT = 15
def __init__(self):
super(PersonTableModel,self).__init__()
self.headers = ['Name','City']
self.persons = []
self.rowsLoaded = PersonTableModel.ROW_BATCH_COUNT
def rowCount(self,index=QModelIndex()):
if not self.persons:
return 0
if len(self.persons) <= self.rowsLoaded:
return len(self.persons)
else:
return self.rowsLoaded
def canFetchMore(self,index=QModelIndex()):
if len(self.persons) > self.rowsLoaded:
return True
else:
return False
def fetchMore(self,index=QModelIndex()):
reminder = len(self.persons) - self.rowsLoaded
itemsToFetch = min(reminder,PersonTableModel.ROW_BATCH_COUNT)
self.beginInsertRows(QModelIndex(),self.rowsLoaded,self.rowsLoaded+itemsToFetch-1)
self.rowsLoaded += itemsToFetch
self.endInsertRows()
def addPerson(self,person):
self.beginResetModel()
self.persons.append(person)
self.endResetModel()
def columnCount(self,index=QModelIndex()):
return len(self.headers)
def data(self,index,role=Qt.DisplayRole):
col = index.column()
person = self.persons[index.row()]
if role == Qt.DisplayRole:
if col == 0:
return QVariant(person.name)
elif col == 1:
return QVariant(person.city)
return QVariant()
def headerData(self,section,orientation,role=Qt.DisplayRole):
if role != Qt.DisplayRole:
return QVariant()
if orientation == Qt.Horizontal:
return QVariant(self.headers[section])
return QVariant(int(section + 1))
In the above code the variable ROW_BATCH_COUNT specifies the
number of rows for initial display of table and batch size for
subsequent view refreshes. The variable self.rowsLoaded is
initialized with ROW_BATCH_COUNT and is incremented when
there is a user action to trigger display of more number of rows in
the table.
The method rowCount is adapted so that number of rows shows
the number of rows that has been currently loaded.
The method canFetchMore returns True if the number
of rows loaded is less than the number of person instances, indicating
that there is more data that needs to be displayed.
The method fetchMore is triggered in cases where
canFetchMore returns True and in this method number
of rows to be loaded is increased by ROW_BATCH_COUNT
As shown in the example code, with a clear separation between
model,view and overriding two methods pagination can be achieved in Qt
with much ease.