Desktop application

Automatic UI validation in the Qt

During development of a larger desktop application using Qt widgets there is a need to connect many signals inside an application. There are two ways of doing this: using pointers to slots and signals or using names of slots and signals. I prefer the first one because this way I get slot/signal name validation at compile time. However in both scenarios developers can forget to connect the signal. It would be good if one could detect these kinds of errors programmatically. We will try to search for a solution.

Quick look at the methods available in QObject class reveals a method called isSignalConnected. This method exactly fits our needs. However it is marked protected and we want to use it from the outside of a class. We don’t want to promote all these push buttons or other widgets. We must use some trick to call this method. Fortunately in C++ one can get a pointer to almost everything, so using a dummy derived class we will steal a pointer to our method. Dummy class code is listed below:

class QObjectWrapper : public QObject
{
public:
    typedef bool (QObject::*FUNCTION)(const QMetaMethod &) const;
    FUNCTION GetPointer() { return &QObjectWrapper::isSignalConnected; }
};
 

After we have this class ready we can use it to call isSignalConnected on a real object. So first we create the wrapper and extract method pointer.

QObjectWrapper wrapper;
QObjectWrapper::FUNCTION isSignalConnectedFunc = wrapper.GetPointer();

Using this pointer we can call the method. Of course we need an argument to pass it to the function. We use QMetaMethod::fromSignal to prepare the argument. Then we can pass it to isSignalConnected. You can see it on the following code snippet:

const QMetaMethod signal = QMetaMethod::fromSignal(&QPushButton::clicked);
bool result = (object->*isSignalConnectedFunc)(signal);

With the result from that call we can do whatever we want, for example print a warning to console.

Now we will make use of our code on a larger scale. We will test signals of QPushButton and QAction and use templates to generate the code. We will create two template functions which will provide suitable signals for a specific class.

template<typename T>
QVector<const QMetaMethod*> GetSignals();

template<>
QVector<const QMetaMethod*> GetSignals<QPushButton>()
{
    static const QMetaMethod signal1 = QMetaMethod::fromSignal(&QPushButton::clicked);
    static const QMetaMethod signal2 = QMetaMethod::fromSignal(&QPushButton::toggled);
        return {&signal1, &signal2};
}

template<>
QVector<const QMetaMethod*> GetSignals<QAction>()
{
    static const QMetaMethod signal1 = QMetaMethod::fromSignal(&QAction::triggered);
    return {&signal1};
}

Then we create a core function of our testing solution. It will use a list of objects as an input. You can notice that only one connected signal will satisfy our test for a given object. We assume that if a push button uses the toggled signal, the other signal does not need to be used.

void CheckSignals(const QList<T*>& objects)
{
    QObjectWrapper wrapper;
    QObjectWrapper::FUNCTION isSignalConnectedFunc = wrapper.GetPointer();

    QVector<const QMetaMethod*> signalArr = GetSignals<T>();
    for(QObject* object : objects)
    {
        bool res = false;
        for(auto sig : signalArr)
        {
            if( (object->*isSignalConnectedFunc)(*sig) )
            {
                res = true;
                break;
            }
        }

        if(!res)
        {
            // do some logging
        }
    }
}

Finally we can call our tests:

template<typename T>
void CheckSignals(QObject* parent)
{
    CheckSignals<T>( parent->findChildren<T*>() );
}

void RunTests(QWidget* parent)
{
    CheckSignals<QPushButton>( parent );
    CheckSignals<QAction>( parent );
}

To test every window we can call the proceeding function inside their constructors and using similar templates we can also test other parts of the UI, eg. missing tooltips. I hope this solution will help you track problems in your UI before manual tests.

Automatic translation reloading in the Qt

Professional desktop applications require translations. The Qt framework has great support for translation like special CMake functions and Qt Linguist tool. Despite this fact, after translation is updated in the Qt Linguist, the person who is editing the translation must take some steps to view results of his/her work in the application. Below I will demonstrate how to make the work of the translator easier.

First of all we want to detect file changes. Qt has a special class called QFileSystemWatcher for doing this kind of job. We pass files (or directories) we want to monitor to the constructor of QFileSystemWatcher. Then we set the handler.

#include <QFileSystemWatcher>
...
#define COMMON_DIR "../common/"
#define SOURCE_DIR "../../src/myapp/"
...
// Some place of application initialization, in my case it was
// constructor of CApplication (derived form QApllication)
...
QStringList paths =
{
   COMMON_DIR "myapp_en_US.qm",
   COMMON_DIR "myapp_pl_PL.qm",
   SOURCE_DIR "myapp_en_US.ts",
   SOURCE_DIR "myapp_pl_PL.ts",
};

m_watcher = new QFileSystemWatcher(paths, m_mainWindow);
    connect(m_watcher, SIGNAL(fileChanged(QString)), m_mainWindow, SLOT( HandleFileChanged(QString) ));

After the file system watcher is in place we must write our handler. Below you can see the contents of this method.

void CMainWindow::HandleFileChanged(const QString& filename )
{
    if(filename.endsWith(".qm"))
    {
        //code for loading the translation
    }
    else if(filename.endsWith(".ts"))
    {
        QProcess cmdProcess;
        cmdProcess.setWorkingDirectory("../..");
        cmdProcess.start("cmd.exe", {"/c release_translations.bat"});
        cmdProcess.waitForFinished(-1);
    }
}

As you can see after *.ts file is modified we start a batch file. This batch file calls lrelease executable, which converts xml based ts files into binary qm files. Normally this step is done during the build. Below you can see the contents of release_translations.bat file.

SET PATH=%PATH%;C:\Qt\5.15.2\msvc2019_64\bin

lrelease src\myapp\myapp_en_US.ts -qm bin\common\myapp_en_US.qm
lrelease src\myapp\myapp_pl_PL.ts -qm bin\common\myapp_pl_PL.qm

As an alternative to using batch file one could call lrelease application directly from Qt application. In my case I decided to use batch file as proxy, where I can modify lrelease settings, without modifying the code.

After code is up and running user can just press Ctrl+S in the Qt Linguist and the new translation will jump right into the application he/she is working on.