Group / Aggregate QAbstractItemModel
Hi Guys,
I've been looking around for a while to see if I can get my QAbstractItemModel to group and/or sum given a defined column in the model. I my case I have a model with three columns from a ledger system - account number, account name, amount. I may have say ten rows in the model, but if those ten rows contains only entries from two accounts I want to show only two rows with the account number, account name and the sum of the amount. Basically exactly like a Group By statement in SQL, but in memory.
I've had a look to the QSortFilterProxyModel to put on top of the model I already have but I can't get it to group/sum - which means I haven't found an example yet that shows this.
I will be truely greatfull if someone sould share his/hers experience with grouping rows in QAbstractItemModel.
Thanks
Re: Group / Aggregate QAbstractItemModel
I can only see two ways of doing this :
- using a tree model and grouping items upon insertion into a parent with same name/account and updating the amount of the parent item
- using QAbstractProxyModel and using an internal intermediate data structure to do the actual group tracking/summing
Re: Group / Aggregate QAbstractItemModel
Hi fullmetalcoder
Thanks for your reply. Your second way looks appealing can you elaborate in this, please? I guess you'd used the QAbstractItemModel as source for the proxy but how will you get it to group?
Re: Group / Aggregate QAbstractItemModel
Re: Group / Aggregate QAbstractItemModel
OK, here is my attempt at a proxy model that does aggregation. Some notes:
- It assumes the data is unsorted. With sorted data a more efficient implementation could be created.
- It is static. It doesn't update with changes to the source model. This could be fixed by connecting to the appropriate slots.
- The groupby is done as a string. If only grouping by integers, it could be made more efficient.
- It only works on flat models.
- The setup is done in the constructor. It takes a function pointer to an aggregate function (Total and Average are supplied), the groupby column and the aggregate column.
So here is the code:
Header:
Code:
#include <QDebug>
#include <QAbstractProxyModel>
#include <QMap>
#include <QVariantList>
#include <QStringList>
class AggregateItem
{
public:
QVariantList items;
int sourceRow;
};
{
Q_OBJECT
public:
Aggregator
(QVariant (*aggFunction
)(const QVariantList
&),
int colGroup,
int colAgg,
int roleGroup = Qt::DisplayRole,
int roleAgg = Qt::DisplayRole,
virtual int columnCount
(const QModelIndex
& parent
= QModelIndex()) const ;
virtual int rowCount(const QModelIndex& parent) const ;
virtual QModelIndex parent
(const QModelIndex
& index
) const ;
virtual QModelIndex mapFromSource
(const QModelIndex
&) const ;
virtual QModelIndex mapToSource
(const QModelIndex
&) const ;
virtual Qt
::ItemFlags flags
( const QModelIndex & index
) const;
void transformData();
private:
QMap<
QString, AggregateItem > map;
int m_colGroup, m_colAgg, m_roleGroup, m_roleAgg;
QVariant (*m_aggFunction
)(const QVariantList
&);
};
Source:
Code:
#include <QtGui/QApplication>
#include <QStandardItemModel>
#include <QTreeView>
#include <QObject>
#include "aggregator.h"
Aggregator
::Aggregator(QVariant (*aggFunction
)(const QVariantList
&),
int colGroup, int colAgg,
int roleGroup,
int roleAgg,
QObject *parent
) : m_colGroup(colGroup), m_colAgg(colAgg),
m_roleGroup(roleGroup), m_roleAgg(roleAgg),
m_aggFunction(aggFunction)
{ }
QVariant Total
(const QVariantList
& vars
) {
int total = 0;
{
total += var.toInt();
}
}
QVariant Average
(const QVariantList
& vars
) {
int total = 0;
{
total += var.toInt();
}
}
void Aggregator::transformData()
{
int rowCount = m->rowCount();
for (int i = 0; i < rowCount; i++)
{
QString group
= m
->data
(m
->index
(i, m_colGroup
), m_roleGroup
).
toString();
QVariant agg
= m
->data
(m
->index
(i, m_colAgg
), m_roleAgg
);
if (!map.contains(group))
{
AggregateItem item;
item.sourceRow = i;
map.insert(group, item);
rows.push_back(group);
}
map[group].items.push_back(agg);
}
{
map[str].result = m_aggFunction(map.value(str).items);
map[str].items.clear(); /* No longer needed. */
}
}
{
if (index.column() == columnCount() - 1)
{
if ((index.row() < rows.count() && index.row() >= 0) &&
(role == Qt::DisplayRole || role == Qt::EditRole))
return map.value(rows.at(index.row())).result;
else
}
}
Qt
::ItemFlags Aggregator
::flags ( const QModelIndex & index
) const{
if (index.column() == columnCount() - 1)
return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
else
}
int Aggregator::columnCount(const QModelIndex& parent) const
{
}
int Aggregator::rowCount(const QModelIndex& parent) const
{
return rows.count();
}
QModelIndex Aggregator
::index(int row,
int column,
const QModelIndex
& parent
) const {
return createIndex(row, column, map[rows.at(row)].sourceRow);
}
QModelIndex Aggregator
::parent(const QModelIndex
& index
) const {
}
QModelIndex Aggregator
::mapFromSource(const QModelIndex
& index
) const {
if (index.isValid())
{
QString groupby
= sourceModel
()->data
(sourceModel
()->index
(index.
row(), m_colGroup
)).
toString();
for (int i = 0; i < rows.count(); i++)
{
if (rows.at(i) == groupby)
return createIndex(i, index.column(), map[groupby].sourceRow);
}
}
}
QModelIndex Aggregator
::mapToSource(const QModelIndex
& index
) const {
if (index.
isValid() && index.
column() != columnCount
(QModelIndex()) - 1) return sourceModel
()->index
(index.
internalId(), index.
column(),
QModelIndex()) ;
else
}
bool Aggregator
::hasChildren(const QModelIndex &parent
) const {
return !parent.isValid();
}
and usage:
Code:
int main(int argc, char *argv[])
{
for (int row = 0; row < 4; ++row) {
model.setItem(row, 0, item);
model.setItem(row, 1, item);
for (int column = 2; column < 4; ++column) {
model.setItem(row, column, item);
}
}
for (int row = 4; row < 8; ++row) {
model.setItem(row, 0, item);
model.setItem(row, 1, item);
for (int column = 2; column < 4; ++column) {
model.setItem(row, column, item);
}
}
Aggregator * ag = new Aggregator(Total, 0, 1);
ag->setSourceModel(&model);
ag->transformData();
tv->setModel(ag);
tv->show();
return app.exec();
}
Re: Group / Aggregate QAbstractItemModel
Hi numbat
This looks exiting. I'm getting an error though when trying to compile. It's in the declaration of ag (see above line 39 in main.cpp) and the error description is: 'Total' was not declared in this scope. Are you passing the right argument to the constructor?
Re: Group / Aggregate QAbstractItemModel
Sorry, add the following two lines to the end of the header file:
Code:
QVariant Average
(const QVariantList
& vars
);
QVariant Total
(const QVariantList
& vars
);
Re: Group / Aggregate QAbstractItemModel
I'm still getting the same error after pasting the two function prototypes to the class. The error message is the same as before: 'Total' was not declared in this scope. I tried to work around it but I must admit that I don't understand the following argument for the constructor: QVariant (*aggFunction)(const QVariantList&) and you have it later in the class under the private section as QVariant (*m_aggFunction)(const QVariantList&). Does this mean casting QVariantList Items as pointer to m_aggFunction or what? I don't really follow?!
1 Attachment(s)
Re: Group / Aggregate QAbstractItemModel
It works for me. I'll attach a zip.
Quote:
I tried to work around it but I must admit that I don't understand the following argument for the constructor: QVariant (*aggFunction)(const QVariantList&) and you have it later in the class under the private section as QVariant (*m_aggFunction)(const QVariantList&).
That is a function pointer. Specifically, it is a pointer to a function that takes a const QVariantList& as argument and returns a QVariant. I use function pointers so that the programmer can provide another function as an argument to the constructor and this function will be used as the aggregate function. This probably signals my C heritage, in C++ we should probably use a class.
Re: Group / Aggregate QAbstractItemModel
Here is another simpler, although far less efficient way of doing it:
Code:
#include <QApplication>
#include <QSqlDatabase>
#include <QDebug>
#include <QSqlTableModel>
#include <QAbstractItemModel>
#include <QStringList>
#include <QSqlQuery>
#include <QSqlQueryModel>
#include <QStandardItemModel>
#include <QTreeView>
#include <QHeaderView>
/* Given a data model, a column to group by, a column to aggregate and an
aggregate function, this function will return a model with the given
transformation. Available aggregate function include SUM, AVG, COUNT, MAX
and MIN.
*/
unsigned int groupByColumn,
unsigned int aggregateColumn,
const QString & aggregateFunction
= "SUM") {
/* Open an in-memory database. */
db.setDatabaseName(":memory:");
bool ok = db.open();
/* Create a table with the correct number of columns. */
for (int i = 0; i < m.columnCount(); i++)
columns.
push_back(QString("F%1").
arg(i
));
ok = query.exec(sql);
/* Set up a table model of our newly created table. */
tbl.setTable("T1");
tbl.insertRows(0, m.rowCount());
/* Now copy accross our model to the newly created table. */
for (int i = 0; i < m.rowCount(); i++)
for (int j = 0; j < m.columnCount(); j++)
tbl.setData(tbl.index(i, j), m.data(m.index(i, j)));
/* Submit changes to database. */
ok = tbl.submitAll();
/* Finally, query our database for the grouped by table. */
arg(groupByColumn).
arg(aggregateColumn).
arg(aggregateFunction);
out->setQuery(sql2, db);
return out;
}
int main(int argc, char * argv[])
{
for (int row = 0; row < 4; ++row)
{
model.setItem(row, 0, item);
model.setItem(row, 1, item);
model.setItem(row, 2, item);
}
for (int row = 4; row < 8; ++row)
{
model.setItem(row, 0, item);
model.setItem(row, 1, item);
model.setItem(row, 2, item);
}
tbl->setHeaderData(0, Qt::Horizontal, "Total");
tv.setModel(tbl);
tv.header()->moveSection(0, 3);
tv.show();
return a.exec();
}
Re: Group / Aggregate QAbstractItemModel
Hi numbat
It just compiled the zip and it works like a charm! Last time I added the two lines inside the class scope so that's why I could''t get it to work before. I must say I'm impressed with your post - so little code and yet so powerfull. This is exactly what I wanted to do. Thanks!
Re: Group / Aggregate QAbstractItemModel
Hi numbat
It just compiled the zip and it works like a charm! Last time I added the two lines inside the class scope so that's why I could''t get it to work before. I must say I'm impressed with your post - so little code and yet so powerfull. This is exactly what I wanted to do. Thanks!