Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

How to properly subclass QSqlRelationalTableModel to set background color of cells in QTableView



  • I took "Books" example from Qt examples with minimal changes. Need to be able to set background color, text color and border width for arbitrary cells. Wrote on the Internet that, probably, the simplest way is to subclass the table model class. So, I tried to subclass QSqlRelationalTableModel class in tablemodel.h and tablemodel.cpp and redefine

    data(const QModelIndex &index, int role = Qt::DisplayRole) const override
    

    class method.
    I wrote

    model = new TableModel(ui.bookTable);
    

    instead of

    model = new QSqlRelationalTableModel(ui.bookTable);
    

    in bookwindow.cpp, where TableModel is the subclass of QSqlRelationalTableModel.
    But, as soon as I did it, the code began failing with a segmentation fault at the call of

    QSqlRelationalDelegate::paint(painter, opt, index);
    

    in bookdelegate.cpp on line 17, which is the subclass of QSqlRelationalDelegate.
    The reproducing error code is at: https://github.com/AndStorm/QtQuestion.git .
    I can not understand what causes this error, could You, please, explain in detail?
    How to properly subclass classes like QSqlRelationalTableModel and decorate cells of QTableView?
    If there are any examples on the Internet, couldn't You give a link? I didn't find any helpful myself.



  • @And92 said in How to properly subclass QSqlRelationalTableModel to set background color of cells in QTableView:

    But, as soon as I did it, the code began failing with a segmentation fault at the call of

    Two big problems in your QVariant TableModel::data() override in tablemodel.cpp, basic C++:

            case Qt::DisplayRole:
            {
               //return actual content for row here, i. e. text, numbers
            }
            case Qt::TextAlignmentRole:
    

    You can't leave the case Qt::DisplayRole: empty of any code and expect it to work/anything but disaster. Your code for DisplayRole will drop into your code for TextAlignmentRole and return the result from there, which is quite unsuitable for any displaying.

    Your whole data() method only returns any result in a couple of cases, in most cases it doesn't even hit any return statement. Does this really compile, and without warnings? I am surprised if it does.... You must return QSqlRelationalTableModel::data(index, role) where you don't have your own thing to return.



  • Thanks. I corrected the code:

    QVariant TableModel::data(const QModelIndex &index, int role) const
    {
        if(!index.isValid()) return QVariant();
        QVariant value = QSqlQueryModel::data(index, role);
        switch(role)
        {
            case Qt::BackgroundRole:
            {
                if(index.column() == 2)
                {
                    if(index.row()%3 == 0)      return QVariant::fromValue(QColor(Qt::red));
                    else if(index.row()%3 == 1) return QVariant::fromValue(QColor(Qt::green));
                    else                        return QVariant::fromValue(QColor(Qt::blue));
                }
                break;
            }
            case Qt::ForegroundRole:
            {
                if(index.column() == 1)
                    return QVariant::fromValue(QColor(Qt::blue));
                break;
            }
            case Qt::DisplayRole:
            {
                if(value.isValid())
                {
                    if(index.column() == 0)
                        return value.toString().prepend('#');
                    else if(index.column() == 2)
                        return value.toString();
                }
                break;
            }
            case Qt::TextAlignmentRole:
            {
               if(1==index.column())
                  return QVariant (Qt::AlignVCenter | Qt::AlignLeft);
               if(2==index.column())
                  return QVariant (Qt::AlignVCenter | Qt::AlignTrailing);
               return QVariant ( Qt::AlignVCenter | Qt::AlignHCenter );
               break;
            }
            default:
            {
               return value;
               break;
            }
        }
        return value;
    }
    

    Now the QTableWidget is displayed properly, but i cannot modify anything. After a double click on any modifiable field, the program crashes with a seg fault. The debugger says the crash is in the function QSqlRelationalDelegate::setModelData of qsqlrelationaldelegate.h. I guess that it is because setData method is not implemented.
    Tried to implement setData in tablemodel.cpp to be able to modify the column with the number 1 ("Title"):

    bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role)
    {
        if (index.column() < 1 || index.column() > 4) return false;
        QModelIndex primaryKeyIndex = QSqlQueryModel::index(index.row(), 0);
        int id = data(primaryKeyIndex).toInt();
        clear();
        bool ok = true;
        if(index.column() == 1) ok = setTitle(id, value.toString());
        refresh();
        return ok;
    }
    

    where

    bool TableModel::setTitle(int id, const QString &title)
    {
        QSqlQuery query;
        query.prepare("update books set title = ? where id = ?");
        query.addBindValue(title);
        query.addBindValue(id);
        return query.exec();
    }
    

    and

    void TableModel::refresh()
    {
        QSqlQuery q;
        q.exec("select * from books");
        setHeaderData(fieldIndex("author"), Qt::Horizontal, tr("Author Name"));
        setHeaderData(fieldIndex("genre"), Qt::Horizontal, tr("Genre"));
        setHeaderData(fieldIndex("title"), Qt::Horizontal, tr("Title"));
        setHeaderData(fieldIndex("year"), Qt::Horizontal, tr("Year"));
        setHeaderData(fieldIndex("rating"), Qt::Horizontal, tr("Rating"));
    }
    

    But this does not work. Unfortunately, I have no idea how to implement it yet. If You know, please, be so kind to give me any hints how to implement it.


  • Lifetime Qt Champion

    Hi,

    You can also use a QIdentityProxyModel and do your changes there.



  • Thank You for Your answer. But i would like to know how to get to work this actual code. I'm a beginner in Qt and would like to understand and learn some rules and examples I can use in everyday work.
    Thanks to @JonB, I understood how to make a custom visualization of the model on a QtableWidget. But the elements of the widget do not react on being edited. My question was how to properly perform the ability of editing items, so how to subclass QSqlRelationalTableModel and implement setData and flags methods properly. If there are any examples on the Internet, could You, please, give me a link.


  • Lifetime Qt Champion

    What exact minimal changes did you do with regard to the original example ?



  • @And92
    First, always please answer @SGaist questions first, as he always knows what he is talking about!

    I have a feeling your code may be a bit over the place now, as you've tried to introduce QSqlRealtionalTableModel, and we're not sure what you've changed/added etc.

    But your TableModel::setData() does not look good to me, and that has to work for any editing to work.

    Just like my earlier points about your data() implementation. It must handle all cases, correctly:

    • You need to be looking at role, which you are not
    • You need to call base QSqlQueryModel::setData() in cases you don't handle, which you are not.
    • You call clear() in the middle of setting data, that doesn't look right, it will throw everything away.
    • You need to respect the column() being changed, which you do not.


  • Thank You, @SGaist and @JonB, for answering my questions.
    Excuse me for not answering Your latest posts. I have been away from the town and did not have access to the Internet.
    Dear @SGaist, I took the "Books" example from Qt Creator examples and the only change I did with regard to the original example is that I created TableModel class inheriting QSqlRelationalTableModel, because I wanted to paint some rows of the QTableWidget in different colors and wanted to do this by redefining the method data() in TableModel. So, the only change is that i created TableModel class and used it in bookwindow.h/cpp instead of QSqlRelationalTableModel. Now the rows are painted in necessary colors, but now I can not modify the values in the cells (text, spinBoxes, comboBoxes) and in textEdit fields below the QTableWidget. The changes are not applied. the actual code is at https://github.com/AndStorm/QtQuestion.git .

    @JonB, I removed the call of clear() from setData(). I do not know how to properly handle cases (spinBox/comboBox ValueChanged signals) that I do not handle with QSqlQueryModel::setData(). I try to respect the column==1 in setData(), where the title is, but is does not change.
    Lastly, unfortunately, I do not understand what You are speaking about "role". My thought is that with switch, Qt::BackgroundRole, Qt::BackgroundRole, case Qt::ForegroundRole:, case Qt::DisplayRole:, case Qt::TextAlignmentRole: and return QSqlQueryModel::data(index, role); in default: I handle all the cases of all possible values of "role", don't I?

    I have searched on the Internet, but all I found yet is "Query Model Example" in Qt Creator. But it uses QSqlQueryModel instead of QSqlRelationalTableModel, it is much simpler, and I undestand everything there, it does not help in work on this code.



  • @And92 said in How to properly subclass QSqlRelationalTableModel to set background color of cells in QTableView:

    Lastly, unfortunately, I do not understand what You are speaking about "role". My thought is that with switch, Qt::BackgroundRole, Qt::BackgroundRole, case Qt::ForegroundRole:, case Qt::DisplayRole:, case Qt::TextAlignmentRole: and return QSqlQueryModel::data(index, role); in default: I handle all the cases of all possible values of "role", don't I?

    You do indeed in your data() override method. But you also have to deal with these in setData()! setData() has a role parameter just like data() does, and you need to act on it. And call the base QSqlQueryModel::setData() for those cases you do not handle, just like you call QVariant value = QSqlQueryModel::data(index, role) during data().



  • I rewrote setData as:

    bool TableModel::setData(const QModelIndex &index, const QVariant &value, int role)
    {
        if(index.column() < 1 || index.column() > 4) return false;
    
        QModelIndex primaryKeyIndex = QSqlQueryModel::index(index.row(), 0);
        int id = data(primaryKeyIndex).toInt();
    
        bool ok = true;
        switch (role)
        {
            case Qt::EditRole:
                switch(index.column())
                {
                case 1:
                    ok = setTitle(index.row()+1, value.toString());
                    emit dataChanged(index, index);
                    break;
                case 2:
                    break;
                case 3:
                    break;
                case 4:
                    break;
                default:
                    ok = QSqlRelationalTableModel::setData(index, value, role);
                    break;
            }
            default:
                QSqlRelationalTableModel::setData(index, value, role);
                break;
        }
        refresh();
        return ok;
    }
    

    where

    void TableModel::refresh()
    {
        QSqlQuery q;
        q.exec("select * from books");
        setHeaderData(fieldIndex("author"), Qt::Horizontal, tr("Author Name"));
        setHeaderData(fieldIndex("genre"), Qt::Horizontal, tr("Genre"));
        setHeaderData(fieldIndex("title"), Qt::Horizontal, tr("Title"));
        setHeaderData(fieldIndex("year"), Qt::Horizontal, tr("Year"));
        setHeaderData(fieldIndex("rating"), Qt::Horizontal, tr("Rating"));
    }
    

    and added

    TableModel::TableModel(QObject *parent) : QSqlRelationalTableModel(parent)
    {
        connect(this, &QSqlRelationalTableModel::dataChanged, this, &QSqlRelationalTableModel::select);
    }
    

    in the constructor of TableModel class. Now the titles are modified (and they are also modified in the database) and the values in QTableWidget widget 1st column are updated immediately at run-time.
    But I don't know how to handle and modify QSpinBox and QComboBox (2-4 columns) fields, that is why I left blank case 2-4: in the switch.



  • @And92
    For efficiency, why do you call refresh() on every setData() call?? That executes an actual SQL query (not to mention, which returns the whole of the table), and there could be many calls! I can only say this setting of header data should only need doing once, and not to do with setData() anyway.


Log in to reply