Question

I'm trying to create my own model based on QAbstractItemModel. It seems to work fine. It passes modeltest assertions.

I've this strange problem when I remove a row. Removal operation works ok. But then other rows become unselectable (not all of them). Have You ever come across such behaviour ?

In which conditions QTreeView could decide that row can not be selected ?

Any ideas ? If needed I can provide the whole model implementation.


EDIT: As an alternative I'm looking for an example of 100% working QAbstractItemModel + QtSql + QTreeView implementation. Model should provide add and remove methods and it has to pass modeltest. This also would answer my question :-)


EDIT: Below is my source code. Compacted a little to make it smaller

ps I see now that there is a bug in parent() implementation. After removing a row values in nodeParams[*].row contain incorrect positions. How do You solve this issue without loading the whole tree into memory ?

class TasksModel : public QAbstractItemModel
{
    Q_OBJECT
public:
    explicit TasksModel(QObject *parent = 0);

    virtual QVariant data ( const QModelIndex & index, int role = Qt::DisplayRole ) const;
    virtual Qt::ItemFlags flags ( const QModelIndex & index ) const;
    virtual int columnCount ( const QModelIndex & parent = QModelIndex() ) const;
    virtual QVariant headerData ( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const;
    virtual int rowCount (const QModelIndex & parent = QModelIndex() ) const;
    virtual bool hasChildren ( const QModelIndex & parent = QModelIndex() ) const;
    virtual void sort ( int column, Qt::SortOrder order = Qt::AscendingOrder );
    virtual QModelIndex index ( int row, int column, const QModelIndex & parent = QModelIndex() ) const;
    virtual QModelIndex parent ( const QModelIndex & index ) const;
    virtual bool setData ( const QModelIndex & index, const QVariant & value, int role = Qt::EditRole );
    virtual bool setHeaderData ( int section, Qt::Orientation orientation, const QVariant & value, int role = Qt::EditRole );
    int selectedId;
    QModelIndex indexForId(int id);

    // add,remove..
    int addTask(QMap<QString,QVariant> params);
    void removeTask(int id, bool children);

private:
    int nrOfColumns;
    QSqlDatabase* dbh;

    mutable QMap<qint64, QSqlQuery*> subQueries;
    mutable QMap<qint64, int> rowsCount;
    mutable QSqlQuery topQuery;
    mutable int topRowsCount;
    mutable bool topQueryReady;
    QSqlQuery* verifyAndPrepareQuery (const QModelIndex& index) const;
    int totalCount(const qint64 id, bool force=false) const;
    void recountTotalCount(const qint64 id) const;

    struct NodeParams {
        int row;
        int parentId;
    };
    mutable QMap<qint64, NodeParams> nodeParams;

signals:

public slots:

};

// ------------------ implementation ---------------------------


TasksModel::TasksModel(QObject *parent) : QAbstractItemModel(parent)
{
    nrOfColumns = 2;

    topQueryReady = false;
    topRowsFetched = 0;
    topRowsCount = 0;
    selectedId = 0;

    // db connection
    dbh = Config::connection();
}


QVariant TasksModel::data ( const QModelIndex & index, int role ) const
{
    if (!index.isValid()) return QVariant();
    int column = index.column();

    if (role == Qt::DisplayRole || role == Qt::EditRole)
    {
        QSqlQuery* query = verifyAndPrepareQuery(index.parent());
        if (!query->seek(index.row())) return QVariant("x");
        switch (column)
        {
            case 0: return query->value(2).toString();
            case 1: return query->value(4).toString() +"%";
        }
    }
    else if (role == Qt::CheckStateRole) {
        // set status of checkbox in 2nd column
        if (column == 1) {

            QSqlQuery* query = verifyAndPrepareQuery(index.parent());
            if (!query->seek(index.row())) return QVariant();

            if (query->value(3).toInt() > 0)
                return Qt::Checked;
            else
                return Qt::Unchecked;
        }
    }
    else if (role == Qt::TextAlignmentRole) {
        switch (column)
        {
            case 0: return Qt::AlignLeft + Qt::AlignVCenter;
            case 1: return Qt::AlignRight + Qt::AlignVCenter;
        }
    }

    return QVariant();

}

Qt::ItemFlags TasksModel::flags ( const QModelIndex & index ) const
{
    if (!index.isValid()) return 0;

    Qt::ItemFlags result = Qt::ItemIsEnabled | Qt::ItemIsSelectable;

    if (index.column()==0) {
        result |= Qt::ItemIsEditable;
    }
    else if (index.column()==1) {
        result |= Qt::ItemIsUserCheckable;
    }

    return result;
}

QVariant TasksModel::headerData ( int section, Qt::Orientation orientation, int role) const
{
    return QVariant();
}

int TasksModel::columnCount ( const QModelIndex & parent ) const
{
    return nrOfColumns;
}


int TasksModel::rowCount (const QModelIndex & parent) const
{
    if (parent.isValid() && parent.column() != 0)
        return 0;

    int id;
    if (parent.isValid())
        id = parent.internalId();
    else
        id = 0;

    return totalCount(id);
}


bool TasksModel::hasChildren ( const QModelIndex & parent) const
{
    if (parent.isValid()) {
        if (totalCount(parent.internalId()) > 0) return true;
    } else {
        if (totalCount(0) > 0) return true;
    }

    return false;
}


void TasksModel::sort ( int column, Qt::SortOrder order )
{
}



// TreeView methods
QModelIndex TasksModel::index ( int row, int column, const QModelIndex& parent ) const
{
    if (row < 0 || column < 0 || column >= nrOfColumns)
            // || (parent.isValid() && parent.column() != 0))
        return QModelIndex();

    QSqlQuery* query = verifyAndPrepareQuery(parent);

    if (!query->seek(row)) return QModelIndex();
    int id = query->value(0).toInt();

    if (!nodeParams.contains(id)) {
        NodeParams params;
        params.parentId = (int)query->value(1).toInt();
        params.row = row;
        nodeParams.insert(id, params);
    }

    return QAbstractItemModel::createIndex(row, column, id);
}

QModelIndex TasksModel::parent ( const QModelIndex & index ) const
{
    return QModelIndex();

    if (!index.isValid()) { return QModelIndex(); }
    if (!nodeParams.contains(index.internalId())) { qDebug("b"); return QModelIndex();}

    NodeParams itemParams = nodeParams.value(index.internalId());

    if (itemParams.parentId == 0) return QModelIndex();
    if (!nodeParams.contains(itemParams.parentId)) { qDebug("d"); return QModelIndex(); }

    NodeParams parentParams = nodeParams.value(itemParams.parentId);

    int parentId = itemParams.parentId;
    int parentRow = parentParams.row;

    return QAbstractItemModel::createIndex(parentRow, 0, parentId);

}


// Edit methods
bool TasksModel::setData ( const QModelIndex & index, const QVariant & value, int role )
{
    return false;
}

bool TasksModel::setHeaderData ( int section, Qt::Orientation orientation, const QVariant & value, int role )
{
    return false;
}


// Build and return query object for current index parent
QSqlQuery* TasksModel::verifyAndPrepareQuery (const QModelIndex& index) const
{
    if (!index.isValid()) {
        // prepare query for root
        if (!topQueryReady) {
            QString sql = "SELECT id,id_parent,title,complete,completion_rate,priority,date_start,date_deadline,date_preferred FROM tasks WHERE id_parent = 0";
            topQuery = QSqlQuery(sql, *dbh);
            topRowsFetched = 0;
            topRowsCount = 0;
            topQueryReady = true;
        }
        return &topQuery;

    } else {
        // prepare queries for subitems (queries stored in subQueries QMap)
        qint64 id = index.internalId();
        if (!subQueries.contains(id)) {
            QString sql = "SELECT id,id_parent,title,complete,completion_rate,priority,date_start,date_deadline,date_preferred FROM tasks WHERE id_parent = "+ QString::number(id);
            QSqlQuery* querySub = new QSqlQuery(sql, *dbh);

            subQueries.insert(id, querySub);
            rowsFetched.insert(id, 0);
            return querySub;
        }
        return subQueries.value(id);
    }

}

int TasksModel::totalCount(const qint64 id, bool force) const
{
    force = true; // temporary setting, to force recalculation in each request, to be optimized
    if (id > 0) {
        if (!rowsCount.contains(id) || force) {
            QString sql = "SELECT COUNT(*) FROM tasks WHERE id_parent = "+ QString::number(id);
            QSqlQuery countQuery(sql, *dbh);
            countQuery.next();
            int count = countQuery.value(0).toInt();

            rowsCount[id] = count;
            return count;
        }
        return rowsCount.value(id);
    } else {
        if (topRowsCount == 0 || force) {
            QString sql = "SELECT COUNT(*) FROM tasks WHERE id_parent = 0 ";
            QSqlQuery countQuery(sql, *dbh);
            countQuery.next();
            topRowsCount = countQuery.value(0).toInt();
        }

        return topRowsCount;
    }
}

void TasksModel::recountTotalCount(const qint64 id) const
{
    // reset variables related to rowsCount and data functions. Called after new child is created or removed
    if (id > 0) {
        rowsCount.remove(id);
        subQueries.remove(id);
    }
    else {
        topRowsCount = 0;
        topQueryReady = false;
    }
    totalCount(id);
}

QModelIndex TasksModel::indexForId(int id)
{
    // convert id to index based on data stored in nodeParams
    if (id == 0) return QModelIndex();
    if (!nodeParams.contains(id)) { qDebug() << "z"; return QModelIndex(); }

    NodeParams params = nodeParams.value(id);
    return QAbstractItemModel::createIndex(params.row, 0, id);
}



// CRUD
int TasksModel::addTask(QMap<QString,QVariant> params)
{
    // create record
    QString sql;

    if (params.value("complete").toInt() == 1)
        params["completion_rate"] = 100;

    // Add task
    QSqlQuery query(*dbh);
    sql = "INSERT INTO tasks (id_parent,id_sibling,position,title,description,complete,completion_rate,priority,date_start,date_deadline,date_preferred) VALUES (?,?,?,?,?,?,?,?,?,?,?)";
    query.prepare(sql);
    query.addBindValue(params.value("id_parent",        0));
    query.addBindValue(params.value("id_sibling",       0));
    query.addBindValue(params.value("position",         0));
    query.addBindValue(params.value("title",            ""));
    query.addBindValue(params.value("description",      ""));
    query.addBindValue(params.value("complete",         0));
    query.addBindValue(params.value("completion_rate",  0));
    query.addBindValue(params.value("priority",         0));
    query.addBindValue(params.value("date_start",       0));
    query.addBindValue(params.value("date_deadline",    0));
    query.addBindValue(params.value("date_preferred",   0));

    // begin insert
    int parentId = params.value("id_parent").toInt();
    int count = totalCount(parentId);
    beginInsertRows(indexForId(parentId), count, count);

    query.exec();
    int taskId = query.lastInsertId().toInt();

    // update nodeParams map
    NodeParams subNodeParams;
    subNodeParams.row = count;
    subNodeParams.parentId = parentId;
    nodeParams[taskId] = subNodeParams;

    recountTotalCount(parentId);
    verifyAndPrepareQuery(indexForId(parentId));
    endInsertRows();
    // insert finished

    return taskId;
}


// method recursively removes task and its children
void TasksModel::removeTask(int id, bool children)
{
    if (!nodeParams.contains(id)) return; 
    NodeParams taskParams = nodeParams.value(id);

    QString sql;
    QSqlQuery query(*dbh);

    // remove children
    if (children) {
        sql = "SELECT id FROM tasks WHERE id_parent = "+ QString::number(id);
        QSqlQuery query2(sql, *dbh);
        while (query2.next()) {
            removeTask(query2.value(0).toInt(), true);
        }
    }

    // remove task (tasks)
    beginRemoveRows(indexForId(taskParams.parentId), taskParams.row, taskParams.row);

    sql = "DELETE FROM tasks WHERE id = "+ QString::number(id);
    query.exec(sql);

    // update ui
    recountTotalCount(taskParams.parentId);
    endRemoveRows();
    nodeParams.remove(id);

    // remove task (tasks_parents)
    sql = "DELETE FROM tasks_parents WHERE id_task = "+ QString::number(id) +" AND id_parent = "+ QString::number(taskParams.parentId);
    query.exec(sql);

    verifyAndPrepareQuery(indexForId(taskParams.parentId));

}
Was it helpful?

Solution

Your nodeParams always has to stay up to date. This means that after each add and remove you have to reload entries of all children of a parent that has been affected.

Wouldn't it be better to simply lazy load tree as items ? Create additional class eg. TreeItem and store children inside QList children.

OTHER TIPS

looking at the model's source code would be helpful,

without this I would start from checking what QAbstractItemModel::flags method is returning for items which you can't select

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top