Posted inUncategorized

So You Can’t Get Your Qt Models to Work With QML?

This particular rant was started by my looking to divert my mind by answering a question on StackOverflow. Never ever ever go to StackOverflow. Kids today just don’t know squat and there is nothing you can do to help them. Let’s start with my initial response an a slightly improved image.

The gist of the issue

QML is just a hand polished turd. There is no polite way to put it. Qt made a desperate ploy to attract script kiddies in the iDiot phone market with QML. The problem is, by and large, script kiddies don’t know anything. They tend to be “self-taught” with far more emphasis on “self” than “taught.” No grounding in the fundamentals of application design and software development. They just want to hack.

They like scripting languages because you can “just hack” without rules. If you want to get an idea for just how much disinformation exists in the script kiddie world, check out the claims about Python being type safe.

Hopefully you will all click the link and look at the original question and code. Please do so now!

My Initial Response

This image may not be 100% accurate but it is how one must visualize things in their mind. You have three distinct lanes an object lives in when you introduce QML: C++, QML Engine, and JavaScript engine. Each and every one of those lanes believes beyond a shadow of a doubt that they control the life and death of the object.

When you are just passing integers around that are passed by value, this is no issue. When you are passing QStrings around, because of the copy-on-write charter of Qt, this is only a minor issue. When you are passing real objects around, worse yet, complex containers of real objects, you have to understand this completely. The answer that “fixed” your problem really only masked it. You will find there is a lot of QML and JavaScript code out there that exists only to mask an application not respecting the lanes.

Had this application used an actual database there would be a fourth lane. Yes, SQLite provides an SQL interface and allows many things, but an actual database has an external engine providing shared access and controlling the life of cursors. SQLite files tend to be single user. Yes, multiple threads within your application can access it, but while your application is running you cannot open a terminal window and use command line tools to examine the database. Think of it more as a really nice indexed file system without sharing.

So, you create an object in C++ and then expose it to QML. The QML engine now believes beyond a doubt that it controls the life and death of that object despite not having its own copy.

QML is really feeble. It can’t actually do much, so it has to hand any significant object off to JavaScript. The JavaScript engine now believes beyond a shadow of a doubt that it now controls the life and death of that object.

You need to also envision these lanes as independent threads. There will most likely be many threads within each, but, in general, any signal or communication between these will go on the event loop of the target as a queued event. That means it will only be processed when it finally bubbles to the top of the queue for that event loop.

This, btw, is why you can never use Smart Pointers when also using QML/JavaScript. Especially the kind that do reference counting and delete the object when there are no more references. There is no way for the C++ lane to know that QML or JavaScript are still using the object.

The answer telling you to check for undefined property is masking the problem that your code is drunk driving across all lanes. Eventually, on a faster (or sometimes slower) processor garbage collection for one of the lanes will run at a most inopportune moment and you will be greeted with a stack dump. (The drunk driving code will hit a tree that does not yield.)

Correct Solution #1: Never use QML or JavaScript. Just use C++ and Widgets. Stay entirely within the C++ lane. If that is a route open to you it’s a good way to go. There is an awful lot of production code out there doing just that. You can obtain a copy of this book (or just download the source code from the page) and muddle through building it.

Correct Solution #2: Never actually do anything in QML or JavaScript. This is an all together different solution than #1. You can use QML for UI only, leaving all logic in C++. Your code is failing because you are trying to actually do something. I haven’t built or tried your code. I simply saw

function deleteRowFromDatabase(row)

which shouldn’t exist at all. C++ holds your model. You emit a signal from your QML/JavaScript lanes when user action requires deletion. This signal becomes a queued event in the C++ lane. When it is processed the row will be deleted and the model updated. If you have properly exposed your model to QML it will emit some form of “model changed” signal and the UI will update accordingly. One of the main points of MVC (Model-View-Controler) is communication to/from the model. When the data changes it notifies the view(s).

Correct Solution #3: Never use C++. Have your C++ be a “Hello World!” shell that just launches your QML. Never create and share an object between C++ and the other two lanes. Do everything inside of JavaScript and QML.

Binding loop errors, in general, happen when code drunk drives across all three lanes. They also happen when code doesn’t view each lane as at least one distinct thread with its own event loop. The C++ lane hasn’t finished (perhaps not even started) the delete operation but your code is already trying to use the result.

The Final Solution

Lots of back and forth happened. When someone has locked themselves into a failed architecture they are generally the last ones to see the failure of their plan. I know. I’ve been on the other side of that. You have to be on the other side of that a few times to learn how to avoid it in the future.

First problem is they chose to use QML. That locked them into using a model. You will find out why that was bad in a bit.

Second problem was drunk driving across all of the lanes. When you stay in the green lane, life is good. Theoretically, when you stay in the red lane life is just as good, you simply can’t do anything real. If you choose to exist only in the yellow lane, then you don’t need Qt at all.

Third problem was the fact there was absolutely no reason for a proxy. You are supposed to use a proxy when you need to transform data for display, like formatting a date or populating a combo box of reference table values. Some also use them to ensure a read-only data source cannot accidentally receive a write request.

Fourth problem was not really knowing anything about a relational database. I see this a lot in the self-taught universe. I even see it with recent college graduates because they can pursue some kind of Web/Game development degree and nobody thinks to include a course on how to care for and feed a relational database.

The Models

QML Turd

There is just no polite way to describe QML. This forcing of models upon the world would be fine if it was properly architected. Hell, suitable debugging and error information would go a long way.

QVariant data(const QModelIndex &index, int role) const;

That one line causes more problems than anything else. If it doesn’t exactly match that in your concrete implementation, you will get no data. The documentation and the IDE will lead many to believe it should have a different signature.

“Oh, but if it’s not right it will just give me an error, right?”

Nope!

If you are using this model in a table the model will return the row count and your table will be all NULL data. If QML can’t find this, it won’t tell you. Adding insult to injury there is not a flag or debugging option you can set to force QML to identify which data() method it used.

So, if you have 60 rows in your table the model will tell the UI there are 60 rows and you will have 60 NULL rows and, most likely a scrollbar.

Broken

This is where OOP jumped the shark. You can get my latest book and read about MVVM. I won’t repeat that essay. What I will tell you is that MVVM was created by people who don’t know how to use a relational database and are physically incapable of understanding data, without a relation, is useless.

If someone walks into a room and says “25” then leaves you have no idea WTF they are talking about. If someone walks into a room and says “John Smith is 25” then leaves, you may or may not know who John Smith is, but you now know he is 25. The 25 now has meaning because it has a relation. With the possible exception of NULL, there is no meaning without relation.

You need to know the previous information because it provides a frame of reference. People who don’t know how to properly use relational databases create the models. When they create automated tests for them these tests will be worse than non-existent.

void SQLiteModel::deleteRow(int rowNo)
 {
     qDebug() << "removeRow called for " << rowNo << "\n";
     bool rslt = removeRow(rowNo);
     qDebug() << "result of removeRow() " << rslt << "\n";
     qDebug() << "Error: " << lastError().text() << "\n";
     qDebug() << "query: " << query().lastQuery() << "\n";
     submitAll();
 }

When the above executes you see the following.

removeRow called for  3
 result of removeRow()  false
 Error:  "near \"WHERE\": syntax error Unable to execute statement"
 query:  "select * from questions;" 

It didn’t matter if the table was created with this

query.exec("CREATE TABLE IF NOT EXISTS questions (Q_NO INTEGER PRIMARY KEY, Q_TEXT TEXT);"); 

or with this

query.exec("CREATE TABLE IF NOT EXISTS questions (Q_NO INTEGER PRIMARY KEY AUTOINCREMENT, Q_TEXT TEXT NOT NULL);" 

It appears this has been broken for over a year, if it ever worked at all.

Lack of Formal Training

Formal training. Actually attending a good school for computer science cannot be overvalued. Sadly there are a lot of run-for-profit shit schools along with just plain shit schools.

Formal training. That’s what teaches you to generate large test data sets. Testing with only ten or fewer records won’t help you find anything. Even for some hokey little program like this you need to initially test with at least enough records to force a scrollbar onto the UI. At least 60 would be good. 500 would be better. There is an ipsum generator if you really hate typing that much.

Formal training. That’s what teaches you the first step to solving a database table issue of any kind is verifying the creation and population of the table from the command line. If you read through the original code you will see it was just stuffing records into the proxy.

Formal training. That’s what teaches you the “default debugger” and moving onto other I/O operations like deleting rows. Other things that can be verified from the command line.

Formal training. That is what teaches you proper software development technique. The language and tools you use don’t matter. Proper technique is what matters.

You can solve any programming problem with the proper technique formal training provides. That and a formal logic class that teaches no programming language is what one needs to succeed in IT. Tools come and go. Those two things are constants.

The Code

QT += quick sql
 CONFIG += c++11
 #
 This is important
 #
 CONFIG += qmltypes
 QML_IMPORT_NAME = Fred
 QML_IMPORT_MAJOR_VERSION = 1
 You can make your code fail to compile if it uses deprecated APIs.
 In order to do so, uncomment the following line.
 DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0
 SOURCES += \
         main.cpp \
         sqlitemodel.cpp
 RESOURCES += qml.qrc
 Additional import path used to resolve QML modules in Qt Creator's code model
 QML_IMPORT_PATH =
 Additional import path used to resolve QML modules just for Qt Quick Designer
 QML_DESIGNER_IMPORT_PATH =
 Default rules for deployment.
 qnx: target.path = /tmp/$${TARGET}/bin
 else: unix:!android: target.path = /opt/$${TARGET}/bin
 !isEmpty(target.path): INSTALLS += target
 HEADERS += \
     sqlitemodel.h
#ifndef SQLITEMODEL_H
#define SQLITEMODEL_H

#include <QtSql/QSqlQueryModel>

class SQLiteModel : public QSqlQueryModel {
Q_OBJECT

public:
    explicit SQLiteModel(QObject *parent = NULL);
    ~SQLiteModel();

    Q_INVOKABLE bool setQuery(const QString &query);

    virtual QHash<int, QByteArray> roleNames() const;

    QVariant data(const QModelIndex &index, int role) const;
    static void declareQML();

private:
    bool openDb();
    QSqlDatabase db;

    int m_recordCount;

};

#endif // SQLITEMODEL_H
 
#include "sqlitemodel.h"

#include <QDebug>
#include <QFileInfo>
#include <QtSql/QSqlQuery>
#include <QtSql/QSqlError>
#include <QtSql/QSqlRecord>
#include <QtSql/QSqlDatabase>
#include <QQmlEngine>
#include <QDir>



void SQLiteModel::declareQML() {
qmlRegisterType<SQLiteModel>("Extensions", 1, 0, "SQLiteModel");
}

SQLiteModel::SQLiteModel(QObject *parent)
    :QSqlQueryModel(parent)
{
    openDb();
}

SQLiteModel::~SQLiteModel()
{

}

bool SQLiteModel::openDb()
{
    QDir hDir = QDir::home();
    db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName(hDir.absoluteFilePath("sample.db"));
    return db.open();
}

bool SQLiteModel::setQuery(const QString &query)
{
    QSqlQueryModel::setQuery(query);

    if (this->query().record().isEmpty()) {
        qWarning() << "SQLiteModel::setQuery() -" << this->query().lastError();
        return false;
    }
    m_recordCount = record().count();
    qDebug() << "Records: " << m_recordCount;

    return true;
}

QHash<int, QByteArray> SQLiteModel::roleNames() const
{
    qDebug() << "roleNames() called\n";
    QHash<int, QByteArray> roles;// = QAbstractTableModel::roleNames();
    for( int i = 0; i < record().count(); i++) {
        roles[Qt::UserRole + i + 1] = record().fieldName(i).toLatin1();
    }
    return roles;
}

QVariant SQLiteModel::data(const QModelIndex &index, int role) const
{
    qDebug() << "data() called\n";
    QVariant value = QSqlQueryModel::data(index, role);
    if(role < Qt::UserRole)
    {
        value = QSqlQueryModel::data(index, role);
    }
    else
    {
        int columnIdx = role - Qt::UserRole - 1;
        QModelIndex modelIndex = this->index(index.row(), columnIdx);
        value = QSqlQueryModel::data(modelIndex, Qt::DisplayRole);
    }

    qDebug() << "data() returning: " << value << "\n";
    return value;
}
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>

#include "sqlitemodel.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);

    SQLiteModel::declareQML();

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);

    SQLiteModel *sqmodel = new SQLiteModel(nullptr);
    sqmodel->setQuery("select * from sample;");

    engine.rootContext()->setContextProperty("tableModel",sqmodel);
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 1.4

ApplicationWindow {
title: qsTr("SQL Demo")
width: 640
height: 480
visible: true

minimumWidth: 640
minimumHeight: 480


    TableView {
        anchors.fill: parent
        model: tableModel
        TableViewColumn{ role: "name"  ; title: "Name" ; width: 100 }
        TableViewColumn{ role: "age"  ; title: "Age" ; width: 100 }
        TableViewColumn{ role: "gender" ; title: "Gender" ; width: 100 }
    }
} 

Commentary

Too many of you will take this code and paste it into your own projects without even looking at it. You really need to read the comments found in sqlitemodel.cpp at lines 150 through 194.

I didn’t know the models were this busted because I never use them. I only use QML at gunpoint and even then I have to think about it.

The models are a failed architecture because they must run in the main event loop. Never do significant I/O in the main event loop. Yeah, this works nice with a few dozen records. Probably work nice with a few thousand on a fast machine that has plenty of RAM for disk caching and no other users needing to do I/O.

If your database is accross the Internet where you could have upwards of 20 second network timeouts your UI is locked up for that entire time. If your source data is stored on something like an IBM Data Cell;

Or its modern day replacement, the tape library

Tape library robot in action

your UI could be locked up for over half an hour waiting its turn in the I/O request queue. You don’t know the definition of “Big Data” until you are writing applications designed to use that amount of data. Despite what you may think, there are a lot of these still around. Chapel Hill didn’t retire Roberta until 2019.

Roberta at Chapel Hill

The “Data Appliance” which replaced Roberta isn’t going to be oceans faster. Faster, yes. Oceans faster, not really. Has to do with the compressing and de-duplicating.

Here is a tape robot library that just got installed in 2019.

The extra capacity needed to be at least 60 petabytes (PB) – the equivalent of storing more than 10 million DVDs – with the potential to increase again to meet future data demands.

From the above link
The Minimum You Need to Know About Qt and Databases

Roland Hughes started his IT career in the early 1980s. He quickly became a consultant and president of Logikal Solutions, a software consulting firm specializing in OpenVMS application and C++/Qt touchscreen/embedded Linux development. Early in his career he became involved in what is now called cross platform development. Given the dearth of useful books on the subject he ventured into the world of professional author in 1995 writing the first of the "Zinc It!" book series for John Gordon Burke Publisher, Inc.

A decade later he released a massive (nearly 800 pages) tome "The Minimum You Need to Know to Be an OpenVMS Application Developer" which tried to encapsulate the essential skills gained over what was nearly a 20 year career at that point. From there "The Minimum You Need to Know" book series was born.

Three years later he wrote his first novel "Infinite Exposure" which got much notice from people involved in the banking and financial security worlds. Some of the attacks predicted in that book have since come to pass. While it was not originally intended to be a trilogy, it became the first book of "The Earth That Was" trilogy:
Infinite Exposure
Lesedi - The Greatest Lie Ever Told
John Smith - Last Known Survivor of the Microsoft Wars

When he is not consulting Roland Hughes posts about technology and sometimes politics on his blog. He also has regularly scheduled Sunday posts appearing on the Interesting Authors blog.