Custom QSettings :: ReadFunc and QSettings :: WriteFunc, or as I wrote a crutch to Russify the settings file

Introduction



Hello, Habr!



Part of my job is developing small desktop applications. In particular, these are programs that allow you to track the current state of the equipment, test it, set configuration parameters, read logs or check the communication channel between two devices. As you can understand from the tags, I use C ++ / Qt to create applications.



Problem



I recently faced the task of saving configuration settings to a file and loading them from it. I would like this time to do without designing bicycles and use some class with minimal costs for its use.



Since the parameters are divided into groups according to device modules, the final version is the structure "Group - Key - Value". QSettings became suitable (but designed for this task). The first try of the "pen" gave a fiasco, which I did not expect to face.



The parameters are displayed in the program to the user in Russian, so we would like to store them in the same form (so that people with little knowledge of English could view the contents of the file).



    //   (   : 
    // C:\Users\USER_NAME\AppData\Roaming\)
    QSettings parameters(QSettings::IniFormat, QSettings::UserScope,
                         QString(""), QString(""));

    // 
    const QString group = QString(" ");
    const QString key = QString(" β„–1");
    const QString value = QString(" β„–1");

    //   -  - 
    parameters.beginGroup(group);
    parameters.setValue(key, value);
    parameters.endGroup();

    //  
    parameters.sync();


What file content I wanted to see:



[ ]
 β„–1= β„–1


and that contained Prilozhenie.ini :



[%U041E%U0441%U043D%U043E%U0432%U043D%U044B%U0435%20%U043F%U0430%U0440%U0430%U043C%U0435%U0442%U0440%U044B]
%U041F%U0430%U0440%U0430%U043C%U0435%U0442%U0440%20%U21161=\x417\x43d\x430\x447\x435\x43d\x438\x435 \x2116\x31


At the same time, what is interesting. If you do the reverse reading procedure, then when displaying the value, you can see that it was read correctly.



    // ...   
    
    // 
    const QString group = QString(" ");
    const QString key = QString(" β„–1");
    const QString value = QString(" β„–1");

    //   -  - 
    parameters.beginGroup(group);
    QString fileValue = parameters.value(key).toString();
    parameters.endGroup();

    //    
    qDebug() << value << fileValue << (value == fileValue);


Console output:



" β„–1" " β„–1" true


The "old" solution



I went to google (in Yandex). It is clear that the problem is with encodings, but why figure it out yourself, when in a minute you can already find out the answer :) I was surprised that there were no clearly written solutions (click here, write this down, live and be happy).



One of the few topics with the title [RESOLVED]: www.prog.org.ru/topic_15983_0.html . But, as it turned out during the reading of the thread, in Qt4 it was possible to solve the problem with encodings, but in Qt5 there is no longer: www.prog.org.ru/index.php?topic=15983.msg182962#msg182962 .



Having added the lines with the solution from the forum to the beginning of the "sample" code (under the hood are hidden "games" with all the possible encodings and functions of Qt classes associated with them), I realized that this only partially solves the problem.



    // 
    QTextCodec *codec = QTextCodec::codecForName("UTF-8");
    QTextCodec::setCodecForLocale(codec);
    //    Qt5
    // QTextCodec::setCodecForTr(codec);
    // QTextCodec::setCodecForCStrings(codec);

    //   (   :
    // C:\Users\USER_NAME\AppData\Roaming\)
    QSettings parameters(QSettings::IniFormat, QSettings::UserScope,
                         QString(""), QString(""));
    parameters.setIniCodec(codec);

    // ...   


Small change in Application.ini (now the parameter value is saved in Cyrillic):



[%U041E%U0441%U043D%U043E%U0432%U043D%U044B%U0435%20%U043F%U0430%U0440%U0430%U043C%U0435%U0442%U0440%U044B]
%U041F%U0430%U0440%U0430%U043C%U0435%U0442%U0440%20%U21161= β„–1


Crutch



A colleague from another department, which deals with serious things, advised me to deal with encodings or write custom read and write functions for QSettings that would support groups, keys and their values ​​in Cyrillic. Since the first option did not bear fruit, I proceeded to the second.



As it turned out from the official documentation doc.qt.io/qt-5/qsettings.html, you can register your own format for storing data: doc.qt.io/qt-5/qsettings.html#registerFormat . All that is required is to select the file extension (let it be "* .habr") where the data will be stored and write the above functions.



Now the "stuffing" of main.cpp looks like this:



bool readParameters(QIODevice &device, QSettings::SettingsMap &map);
bool writeParameters(QIODevice &device, const QSettings::SettingsMap &map);

int main(int argc, char *argv[])
{
    //  
    const QSettings::Format habrFormat = QSettings::registerFormat(
                "habr", readParameters, writeParameters, Qt::CaseSensitive);
    if (habrFormat == QSettings::InvalidFormat) {
        qCritical() << "  -";
        return 0;
    }

    //   (   :
    // C:\Users\USER_NAME\AppData\Roaming\)
    QSettings *parameters = new QSettings(habrFormat, QSettings::UserScope,
                                          QString(""), QString(""));

    // ...   

    return 0;
}


Let's start by writing a function for writing data to a file (saving data is easier than parsing it). The doc.qt.io/qt-5/qsettings.html#WriteFunc-typedef documentation says that the function writes a set of key / value pairs . It is called once, so you need to save the data at a time. The function parameters are QIODevice & device (link to "I / O device") and QSettings :: SettingsMap (QMap <QString, QVariant> container).



Since the name of the key is stored in the container in the form "Group / parameter" (interpreting to your task), you must first separate the names of the group and parameter. Then, if the next group of parameters has started, you need to insert a separator as an empty line.



//     
bool writeParameters(QIODevice &device, const QSettings::SettingsMap &map)
{
    // ,   
    if (device.isOpen() == false) {
        return false;
    }

    //  ,   
    QString lastGroup;

    //       
    QTextStream outStream(&device);

    //    
    // (      )
    for (const QString &key : map.keys()) {
        //        "/"
        int index = key.indexOf("/");
        if (index == -1) {
            //      
            //   (,   "")
            continue;
        }

        //     , 
        //      
        QString group = key.mid(0, index);
        if (group != lastGroup) {
            //   ()  . 
            //        
            if (lastGroup.isEmpty() == false) {
                outStream << endl;
            }
            outStream << QString("[%1]").arg(group) << endl;
            lastGroup = group;
        }

        //    
        QString parameter = key.mid(index + 1);
        QString value = map.value(key).toString();
        outStream << QString("%1=%2").arg(parameter).arg(value) << endl;
    }

    return true;
}


You can run and see the result without a custom read function. You just need to replace the format initialization string for QSettings:



    //  
    const QSettings::Format habrFormat = QSettings::registerFormat(
                "habr", QSettings::ReadFunc(), writeParameters, Qt::CaseSensitive);

    // ...  


Data in file:



[ ]
 β„–1= β„–1


Console output:



" β„–1" " β„–1" true


This could have ended. QSettings performs its function of reading all keys, storing them in a file. Only there is a nuance that if you write a parameter without a group, then QSettings will store it in its memory, but will not save it to a file (you need to add the code in the readParameters function in a place where the separator "/" is not found in the key name of the const QSettings container :: SettingsMap & map).



I preferred to write my own function for parsing data from a file in order to be able to flexibly control the type of data storage (for example, group names are framed not with square brackets, but with other recognition characters). Another reason is to show how things work with both custom read and write functions. See



documentation doc.qt.io/qt-5/qsettings.html#ReadFunc-typedefthe function is said to read a set of key / value pairs . It should read all the data in one pass and return all the data to the container, which is specified as a function parameter, and it is initially empty.



//     
bool readParameters(QIODevice &device, QSettings::SettingsMap &map)
{
    // ,   
    if (device.isOpen() == false) {
        return false;
    }

    //       
    QTextStream inStream(&device);

    //  
    QString group;

    //    
    while (inStream.atEnd() == false) {
        // 
        QString line = inStream.readLine();

        //       
        if (group.isEmpty()) {
            //      
            if (line.front() == '[' && line.back() == ']') {
                //   
                group = line.mid(1, line.size() - 2);
            }
            //  ,   
            //    
        }
        else {
            //  ,   
            if (line.isEmpty()) {
                group.clear();
            }
            //    
            else {
                // : =
                int index = line.indexOf("=");
                if (index != -1) {
                    QString name = group + "/" + line.mid(0, index);;
                    QVariant value = QVariant(line.mid(index + 1));
                    //   
                    map.insert(name, value);
                }
            }
        }
    }

    return true;
}


We return the custom read function to initialize the format for QSettings and check that everything works:



    //  
    const QSettings::Format habrFormat = QSettings::registerFormat(
                "habr", readParameters, writeParameters, Qt::CaseSensitive);

    // ...  


Console output:



" β„–1" " β„–1" true


Crutch work



Since I "sharpened" the implementation of functions for my task, I need to show how to use the resulting "offspring". As I said earlier, if you try to write a parameter without a group, then QSettings will save it in its memory and will display it when the allKeys () method is called.



    //  
    const QSettings::Format habrFormat = QSettings::registerFormat(
                "habr", readParameters, writeParameters, Qt::CaseSensitive);
    if (habrFormat == QSettings::InvalidFormat) {
        qCritical() << "  -";
        return 0;
    }

    //   (   :
    // C:\Users\USER_NAME\AppData\Roaming\)
    QSettings *parameters = new QSettings(habrFormat, QSettings::UserScope,
                                          QString(""), QString(""));

    //  
    const QString firstGroup = " ";
    parameters->beginGroup(firstGroup);
    parameters->setValue(" β„–1", " β„–1");
    parameters->setValue(" β„–2", " β„–2");
    parameters->endGroup();

    //  
    const QString secondGroup = " ";
    parameters->beginGroup(secondGroup);
    parameters->setValue(" β„–3", " β„–3");
    parameters->endGroup();

    //   
    parameters->setValue(" β„–4", " β„–4");

    //   
    parameters->sync();

    qDebug() << parameters->allKeys();
    delete parameters;

    //    
    parameters = new QSettings(habrFormat, QSettings::UserScope,
                               QString(""), QString(""));

    qDebug() << parameters->allKeys();
    delete parameters;


Console output ("Parameter # 4" is obviously superfluous here):



(" / β„–3", " β„–4", " / β„–1", " / β„–2")
(" / β„–3", " β„–4", " / β„–1", " / β„–2")


In this case, the contents of the file:



[ ]
 β„–3= β„–3

[ ]
 β„–1= β„–1
 β„–2= β„–2


The solution to the lone keys problem is to control how data is written when using QSettings. Do not allow saving parameters without the beginning and end of the group or filter keys that do not contain the group name in their name.



Conclusion



The problem of correctly displaying groups, keys and their values ​​has been solved. There is a nuance of using the created functionality, but if used correctly, it will not affect the operation of the program.



After the work done, it seems that it would be perfectly possible to write a wrapper for QFile and live happily. But on the other hand, in addition to the same read and write functions, you would have to write additional functionality that QSettings already has (getting all keys, working with a group, writing unsaved data and other functionality that did not appear in the article).



What's the use? Maybe those who are faced with a similar problem, or who do not immediately understand how to implement and integrate their read and write functions, will find the article useful. In any case, it will be nice to read your thoughts in the comments.



All Articles