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

I'm still struggling with model/view



  • ...I must have a mental block about this. I'm trying to follow a suggestion of using the QStandardItemModel instead of subclassing QAbstractTableModel. I've put together a painfully simple example, and I can't even get that right. Here's the code:

        QApplication a(argc, argv);
        QStandardItemModel myModel;
        QStandardItem myItem;
        QTableView tableView;
    
        myItem.setData(QVariant (1.0), Qt::UserRole);
        myModel.setColumnCount(3);
        myModel.appendRow(&myItem);
        tableView.setModel( &myModel );
    
        tableView.show();
    

    When I run it, I get a row with three empty columns. I was expecting my data (the 1.0 value above) to show up somewhere...what step did I leave out?

    Thanks...


  • Lifetime Qt Champion

    Hi
    its just because you store data in UserRole.
    The default delegate takes/show EditRole/DisplayRole
    so
    myItem.setData(QVariant (1.0), Qt::EditRole); ( or Qt::DisplayRole)
    will be more fun.

    UserRole is your own data and unless you make delegate to use it, its just ignored.

    Also, as a note.
    You actually only give it one item, even a row is 3 cols.
    So col 2,3, is NULL items. So if you click on them and have slot connected the item u get wil be NULL.

    Often , you want to do something like

      QList<QStandardItem*> OneRow;
      OneRow << &myItem << new QStandardItem("100")  << new QStandardItem("100");
      myModel.appendRow(OneRow);
    
    

    so you actually have items in all cols for a row.

    That fooled me a bit in the beginning as the cell is shown but, it can/will be a NULL item
    unless you gave it an item for that cell.

    if you need sample
    http://www.bogotobogo.com/Qt/Qt5_QTableView_QItemDelegate_ModelView_MVC.php

    update: ( for later readers )
    OneRow << &myItem is bad idea. you should always new QStandardItem



  • Ah, thank you for that. Now, let's say that in my program, my data is represented by a struct:

    struct DeviceDetails
    {
        string macAddr;
        string devName;
        string latestTimestamp;
    };
    DeviceDetails tableData[100]; // I won't really do it this way.
    

    And I'm going to have a bunch of these to display (therefore using a table). What's the cleanest way to map my array to the myModel variable?

    Thanks...


  • Lifetime Qt Champion

    @mzimmers
    Hi
    Will you need to write them back to the struct in the list?
    as in user will edit info.



  • My final UI will have two parts: the summary table which I've described above, and a details area. The user will select a row from the table, and this will populate the details area (with more information than just the three strings). (SGaist suggested the books example, which I like and want to emulate.) So, yes, the structure array must be updated from time to time.


  • Lifetime Qt Champion

    @mzimmers
    Ok. well the Address Book Example uses an QAbstractTableModel
    subclass that handles the data
    the real data lives in QList<Contact> contacts;
    Which is similar to your DeviceDetails
    so it should lead you on the right track.
    You can reuse most of the code as is, and only change the function accessing
    Contact to use your DeviceDetails instead. Rest is the same.



  • So, in your opinion, I should go ahead and use QAbstractTableModel instead of QStandardItemModel?


  • Lifetime Qt Champion

    @mzimmers
    well the sample uses it, so it would be natural when you need full editing also.
    Also since most of the code will be 100% the same , except other data structure.

    However, if you were suggested to use QStandardItemModel, there must been a reason for that.
    Maybe to cut down on the details one must handle.
    Since DeviceDetails is flat, using a QStandardItemModel is less code and would work just as fine.

    But im not sure how the write back would e made. Were you suggested to use QStandardItemModel directly
    or to subclass it ?



  • To make sure I'm not misquoting anyone, here's where it was first suggested:

    https://forum.qt.io/topic/90985/use-of-model-view-programming/23

    I don't really care too strongly which way I go, as long as it works and is maintainable. I don't even need to use the struct I mentioned above; I can store the data elsewhere if it simplifies the programming.


  • Lifetime Qt Champion

    Hi
    ah, QStandardItemModel was suggested over QAbstractTableModel
    since its more complex to handle. (as you might have noticed)
    with QStandardItemModel , you just give it items and overall its
    less involving than QAbstractTableModel where you will have to juggle ModelIndex() and
    call begin/end at right times etc.

    However, it does sounds like your requirements are very close to Address Book Example
    so you could reuse its model from there with very little change.
    Also, it already filters i same way as you will need.
    User click Contact, details are shown.
    User click Device, details are shown.

    But it sounded like you already copied the example and its not working 100% for you ?



  • No, I didn't copy the example yet...I actually started this application before being introduced to model/view. I'm now trying to retrofit whatever makes the most sense.

    In looking at the books example, I'm struck by the automation that the classes provide. I don't think there's a single instance of a "connect" command in the source code, for example. This is impressive, but it's also hard to use as an example. I'm reading the (long) page on Qt model/view and hope that will help out a bit.

    Edit: correction: there is indeed one connect. Still pretty impressive.


  • Lifetime Qt Champion

    @mzimmers
    Well the tablemodel class handles most of that internally and the views already automatically connect to
    the signals they need from model.
    You should be able to directly use tablemodel.cpp/.h and
    only replace Contact with your struct and fix the places where its members are used.
    Then it should be working with no extra code.

    Its a good idea to read the docs on this topic. Model & Views are a bit involved but
    also provides many benefits.



  • Subclassing models is a minefield. That's why I suggested using QStandardItemModel instead. The idea was that you can store

    struct DeviceDetails
    {
        QString macAddr;
        QString devName;
        QString latestTimestamp;
    };
    

    as:

    enum DeviceRoles{
    macAddr = Qt::UserRole
    , devName
    , latestTimestamp
    };
    auto item = new QStandardItem;
    item->setData(/*...*/,DeviceRoles::macAddr);
    item->setData(/*...*/,DeviceRoles::devName);
    item->setData(/*...*/,DeviceRoles::latestTimestamp);
    myModel.appendRow(item);
    

    The model subclass is probably the most correct way but before embarking into it, please read the chapter of Advanced Qt programming I linked in the previous thread and run your custom model through the Model Test

    P.S.
    Allocating QStandardItem on the stack is problematic. QStandardItemModel will take ownership of the item and try to delete it. That means that if you declare the item before the model

    QStandardItem myItem;
    QStandardItemModel myModel;
    

    You'll get a crash as well as if you try to remove that row you'll get a crash



  • @VRonin OK, that makes a lot more sense. So, is the missing field in your setData() examples a pointer to my structure element? Is that how the model keeps track of changes to that data?



  • No, my example was using QStandardItemModel. the missing argument is the actual value you want to save in the model, something like item->setData(QStringLiteral("00:A0:C9:14:C8:29"),DeviceRoles::macAddr);



  • Oh, I see. So, am I then responsible for manually updating the item when data changes?



  • @mzimmers said in I'm still struggling with model/view:

    am I then responsible for manually updating the item when data changes?

    Correct, but that would always be the case. if you subclass QAbstractItemModel it's your responsibility to signal changes of data emitting the dataChanged signal



  • So, if I understand this, the general program flow would be:

    1. the worker thread receives updates from the various devices
    2. the worker passes the updates (via a signal) to the model
    3. the model then either adds rows or modifies existing rows
    4. the UI automatically reflects the change to the model.

    That about right?

    One thing I'm still not clear on, is how to modify data in existing rows, for example, changing the timestamp on a device that's already been added.



  • @mzimmers said in I'm still struggling with model/view:

    That about right?

    What workflow did you decide to adopt?

    • Use QStandardItemModel
    • or subclass QAbstractItemModel


  • I'm now trying to use QStandardItemModel. Unfortunately, the data isn't showing up in the QTableView yet, so I'll have to work through to see where it's failing.



  • I'm now trying to use QStandardItemModel

    Then:

    1. the worker thread receives updates from the various devices
    2. the worker passes the updates (via a signal) to a QObject in the main thread that owns the model
    3. the object inserts/removes/updates the model
    4. the UI automatically reflects the change to the model.


  • Yes, that's how I'm doing it. (My model class is DeviceModel.) Just as a test, I put this into the c'tor:

    DeviceModel::DeviceModel()
    {
        QString s1 = "aaa", s2 = "bbb", s3 = "ccc";
        QVariant qv;
    
        qv = s1;
        item.setData(qv, DeviceRoles::macAddr);
        qv = s2;
        item.setData(qv, DeviceRoles::devName);
        qv = s3;
        item.setData(qv, DeviceRoles::latestTimestamp);
        appendRow(&item);
    }
    
    I'd expect to see this in my QTableView, but I don't. What might I be missing?
    


  • I'm now trying to use QStandardItemModel

    My model class is DeviceModel

    Your model class should be QStandardItemModel. Are you subclassing the model?

    P.S.

    item.

    as mentioned previously, you should avoid the allocation of QStandardItems on the stack as the model is supposed to own them



  • Yes:

    class DeviceModel : public QStandardItemModel
    {
    private:
        ModelData deviceTable;
        QStandardItem item;
    ...
    

    EDIT:

    Ohhhhhh...okay.


  • Lifetime Qt Champion

    so it would be like

    DeviceModel::DeviceModel()
    {
        QString s1 = "aaa", s2 = "bbb", s3 = "ccc";
        QVariant qv;
         QStandardItem * item = new   QStandardItem (); // model will clean up
        qv = s1;
        item->setData(qv, DeviceRoles::macAddr);
        qv = s2;
        item->setData(qv, DeviceRoles::devName);
        qv = s3;
        item->setData(qv, DeviceRoles::latestTimestamp);
        appendRow(item); 
    }
    


  • You should have something like this:

    #include <QApplication>
    #include <QStandardItemModel>
    #include <QTableView>
    #include <QDateTime>
    #include <QTimer>
    #include <functional>
    class ModelManager : public QObject{
        Q_OBJECT
        Q_DISABLE_COPY(ModelManager)
    public:
        enum DeviceCols{
            dc_macAddr
            ,dc_devName
            ,dc_latestTimestamp
            
            ,dc_count
        };
        explicit ModelManager(QObject* parent = Q_NULLPTR)
            :QObject(parent)
            , m_model(new QStandardItemModel(this))
        {
            m_model->insertColumns(0,dc_count);
            m_model->setHeaderData(dc_macAddr,Qt::Horizontal,tr("Mac Address"));
            m_model->setHeaderData(dc_devName,Qt::Horizontal,tr("Name"));
            m_model->setHeaderData(dc_latestTimestamp,Qt::Horizontal,tr("Time Stamp"));
        }
        QAbstractItemModel* model() const {return m_model;}
    public slots:
        void addDevice(const QString& macAdr, const QString& name, const QDateTime& timeStamp){
            const int newRow = m_model->rowCount();
            m_model->insertRow(newRow);
            m_model->setData(m_model->index(newRow,dc_macAddr),macAdr);
            m_model->setData(m_model->index(newRow,dc_devName),name);
            m_model->setData(m_model->index(newRow,dc_latestTimestamp),timeStamp);
        }
        void updateDevice(int row, const QString& macAdr, const QString& name, const QDateTime& timeStamp){
            if(row<0 || row>=m_model->rowCount())
                return;
            m_model->setData(m_model->index(row,dc_macAddr),macAdr);
            m_model->setData(m_model->index(row,dc_devName),name);
            m_model->setData(m_model->index(row,dc_latestTimestamp),timeStamp);
        }
    private:
        QAbstractItemModel* m_model;
    };
    
    
    int main(int argc, char *argv[])
    {
        QApplication application(argc, argv);
        ModelManager manager;
        manager.addDevice(QStringLiteral("Foo"),QStringLiteral("Bar"),QDateTime::currentDateTime());
        manager.addDevice(QStringLiteral("Foo1"),QStringLiteral("Bar1"),QDateTime::currentDateTime());
        manager.addDevice(QStringLiteral("Foo2"),QStringLiteral("Bar2"),QDateTime::currentDateTime());
        QTableView view;
        view.setModel(manager.model());
        view.show();
        // simulate adding a device and updating one
        QTimer::singleShot(1000,&manager,std::bind(&ModelManager::addDevice,&manager,QStringLiteral("AddMac"),QStringLiteral("AddName"),QDateTime::currentDateTime().addSecs(1)));
        QTimer::singleShot(2000,&manager,std::bind(&ModelManager::updateDevice,&manager,1,QStringLiteral("UpdateMac"),QStringLiteral("UpdateName"),QDateTime::currentDateTime().addSecs(2)));
        return application.exec();
    }
    

    Of course, instead of the QTimer the updates should come from your workers



  • Beautiful.

    I made a minor change to the update:

    void Device::update(DeviceDetails d)
    {
        int rowCnt = m_model->rowCount();
        int row ;
    
        for(row = 0; row < rowCnt; ++row)
        {
            if(m_model->index(row, dc_macAddr).data().toString() == d.macAddr)
            {
                break;
            }
        }
    
        if (row == rowCnt) // didn't find the Mac address; add a new row.
        {
            add(d);
        }
        else
        {
            m_model->setData(m_model->index(row, dc_macAddr), d.macAddr);
            m_model->setData(m_model->index(row, dc_devName), d.devName);
            m_model->setData(m_model->index(row, dc_latestTimestamp), d.latestHB);
        }
    }
    

    Using the QStandardItemModel is indeed much, much simpler than subclassing. Thanks for all the help.


Log in to reply