1

I want to animate the color (in time) of a QTableview cell once it's value is updated via the connected data model to attract the end-users attention that something has changed.

The idea is that the color changes in gradients of f.i. blue, starts off blue just after the value change and fades to white in about 1 ~ 2 seconds.

I guess one has to use the QStyledItemDelegate here since I use the model-view concept (http://doc.qt.io/qt-5/model-view-programming.html).

One needs a trigger for the cell's value-change for the animation to start, this can be achieved via the paint() method since it is called on a value change. One can figure out the row and column, from the index parameter that is passed to paint(), to get a hold of which cell to animate.

So far so good, you can set the color of that cell to let's say blue. Here comes the problem; the color shall fade towards white in time steps. So I was thinking about a QTimer in the QStyledItemDelegate class and maintain a kinda bookkeeping for the cell's that are being animated (could be a simple list of countdown values that are used to calculate the blue-gradient color. The lower the value the more the gradient goes towards white, once 0 the result is white which is the default color of the cell. On each QTimer timeout() event all values not equal to 0 are lowered by 1. The QStyledItemDelegate is only connected to the row of the QTableview that I want to color-animate i.e. where the value items are displayed.

Problem I face is that:

  1. paint() is a const method so you cannot change any class parameters.
  2. how to re-paint the cell color on a QTimer event (re-paint the whole QTableview is not god-style)

I managed to get it to work but I consider it a dirty solution. What I did is maintain the bookkeeping of the color animation for each cell in the data-model. I consider it a dirty solution because the color-animation is only a visual aspect so imho it should not reside in the data-model. In this way it is also not a portable solution i.e. in another project you have to rework a lot to get it to work.

I stripped down my application to the core problem. Full code can be found here (a working application): https://github.com/fruitCoder123/animated_tableview_cell

void TableViewDelegateValueWritable::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    // Paint background
    uint8_t red_gradient = calculate_color_gradient(RGB_RED_MAX, RGB_RED_MIN, red_gradient_step_size, m_step_value);
    uint8_t green_gradient = calculate_color_gradient(RGB_GREEN_MAX, RGB_GREEN_MIN, green_gradient_step_size, m_step_value);
    painter->fillRect(option.rect, QColor(red_gradient, green_gradient, 255));

    // Paint text
    QStyledItemDelegate::paint(painter, option, index);
}

uint8_t TableViewDelegateValueWritable::calculate_color_gradient(const uint8_t MAX_COLOR, const uint8_t MIN_COLOR, const uint8_t step_size, uint8_t step) const
{
    uint16_t color = (step_size * (1 + MAX_COLOR_GRADIENT_STEP - step)) + MIN_COLOR;

    // Handle overflow and rounding errors
    if(color > MAX_COLOR || color > (MAX_COLOR-(step_size/2)))
        color = MAX_COLOR;

    return static_cast<uint8_t>(color);
}

void TableViewDelegateValueWritable::gradient_timer_elapsed()
{
    if(m_step_value)
    {
        m_step_value--;
        m_timer->start(GRADIENT_TIMEOUT_VALUE);
        //this->paint(m_painter, m_option, m_model_index);
    }
}

I spent a horrific amount of hours to find a nice solution. I started with Qt a month ago so maybe I lack knowledge. Hopefully someone can give a hint how to solve it in a nice way - encapsulated in the view and not entangled with the data-model.

Duck Dodgers
  • 3,409
  • 8
  • 29
  • 43
fruitCoder
  • 77
  • 5
  • Triggering value changed in paint() is not enough, the column, row in the index value may be changed when insert or remove row were called. I think you should handle the trigger correctly first. If you see the data items "changed" is an attribute of your data (data model - that you don't want to mess up with), it should be an easy way to implement your paint() function. – tunglt Jan 04 '19 at 10:09
  • I fully agree with you. It is not a problem to add slots for rowInserted(), rowDeleted() etc. and handle this accordingly in the bookkeeping. But I consider that to be the next problem to solve. Anyhow, thanks for answering! – fruitCoder Jan 04 '19 at 10:15
  • So basically my problem is, after I get the dataChanged trigger, how to change the cell color through a timer event - this is kind of "external" considering the model-view concept.. – fruitCoder Jan 04 '19 at 10:21
  • If you can add an attribute to your data like `QDateTime modifiedTime` for each item (should be for each row by calling setData with an userRole). The paint function will draw the gradient corresponding to this modified time. – tunglt Jan 04 '19 at 10:30
  • Actually I do have date/time in my application (this is a stripped down version) so that could be an option. However still open is how to force a paint() action via the QTimer::timeout() event. Do you know how to do that? (commented out line does not work: this->paint(m_painter, m_option, m_model_index) as I don't have those parameters m_painter, m_model and m_model_index. They come from the data model on a change. – fruitCoder Jan 04 '19 at 13:06
  • Have you considered using a [proxy model](http://doc.qt.io/qt-5/qabstractproxymodel.html) to handle/generate the animation updates? – G.M. Jan 06 '19 at 18:04
  • I was not aware of it's existence, I'll check it out and report here. Thanks for the hint! – fruitCoder Jan 07 '19 at 08:05

1 Answers1

0

For the stated problems :

  1. paint() is declared as const, you can use a mutable variable member and modify it whenever you want. For example :

    class TableViewDelegateValueWritable : public QStyledItemDelegate
    {
        Q_OBJECT
        mutable QTableView * m_view; 
    
    public:
        explicit TableViewDelegateValueWritable(QTableView * view,  QObject *parent){
            m_view = view;
            //...
        }
    
        void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
    
            //...
            int nFrame m_view->getFrameOfIndex(index);
            drawFrame (painter, nFrame ); 
            m_view->setFrameOfIndex(index, ++nFrame);
            //...
        }
        //class body
    }
    
  2. Repaint the whole QTableView is not a god-style but repaint only the QTableView's viewport is a good option, in the MainWindow constructor :

    m_db->insert_record(QString("my_val_1"), "0");
    m_db->insert_record(QString("my_val_2"), "0");
    m_db->insert_record(QString("my_val_3"), "0");
    
    QTimer * timer = new QTimer( this );
    
    connect( timer, &QTimer::timeout, this, [this](){
        ui->tableView->viewport()->repaint();
    });
    
    timer->start( TIME_RESOLUTION ); //set to 1000ms
    

Here is a snippet to animate a cell when it was modified, the color will be changed each 1s, the method used is to subclass QSqlTableModel to track the modified cell (via dataChanged signal) :

   enum MyDataRole {
        ItemModifiedRole = Qt::UserRole + 1
    };

    class MySqlTableModel : public QSqlTableModel {

        Q_OBJECT
        QMap<int, QVariant > mapTimeout;

    public:

        MySqlTableModel( QObject *parent = Q_NULLPTR, QSqlDatabase db = QSqlDatabase() )
            :QSqlTableModel( parent, db ) {
            connect( this, &QSqlTableModel::dataChanged,
                     this, [this]( const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles )
            {
                for(int i = topLeft.row(); i <= bottomRight.row(); i ++ ){
                    mapTimeout.insert( i , QDateTime::currentDateTime() );
                }
            } );
        }

        //this data function will be called in the delegate paint() function.
        QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE
        {
            if( role != ItemModifiedRole )
                return QSqlTableModel::data( idx, role );

            QMap<int, QVariant>::const_iterator it = mapTimeout.find( idx.row() );
            return it == mapTimeout.end() ?  QVariant() : it.value();
        }

        void clearEffects() {
            mapTimeout.clear();
        }

    };

And the delegate paint() function:

    // background color manipulation
    void TableViewDelegateValueWritable::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
    {
        QSqlTableModel * db_model  = (QSqlTableModel *)index.model();
        QVariant v = db_model->data( index, ItemModifiedRole );

        if( !v.isNull() ){

            QDateTime dt = v.toDateTime();
            int nTimePassed =  dt.secsTo( QDateTime::currentDateTime() );
            int step_value = nTimePassed + 2;

            uint8_t red_gradient = calculate_color_gradient( RGB_RED_MAX, RGB_RED_MIN, red_gradient_step_size,  step_value );
            uint8_t green_gradient = calculate_color_gradient( RGB_GREEN_MAX, RGB_GREEN_MIN, red_gradient_step_size, step_value );

            painter->fillRect( option.rect, QColor( red_gradient, green_gradient, 255) );
        }

        // Paint text
        QStyledItemDelegate::paint(painter, option, index);
    }

You should use QSqlRecord way if possible instead of executing a sql statement. In fact, after each sql statement was executed, you call the select() function, this will reset the whole table model. Here is an example of insert/update record :

    void dbase::insert_record(const QString &signal_name, const QString &value)
    {
        QSqlRecord r = db_model->record();

        r.setValue( "signal_name", signal_name );
        r.setValue( "signal_value", value );

        db_model->insertRecord(-1, r );
    }

    void dbase::update_record(const QString &signal_name, const QString &new_value)
    {
        for(int row = 0; row < db_model->rowCount(); row ++ ){
            QSqlRecord r = db_model->record( row );

            if( r.value("signal_name").toString() == signal_name ){
                r.setValue("signal_value", new_value );
                db_model->setRecord( row, r );
                break;
            }
        }
    }
tunglt
  • 1,022
  • 1
  • 9
  • 16