QSettings personalizados :: ReadFunc y QSettings :: WriteFunc, o como escribí una muleta para rusificar el archivo de configuración

Introducción



¡Hola, Habr!



Parte de mi trabajo consiste en desarrollar pequeñas aplicaciones de escritorio. En particular, estos son programas que le permiten rastrear el estado actual del equipo, probarlo, establecer parámetros de configuración, leer registros o verificar el canal de comunicación entre dos dispositivos. Como puede comprender por las etiquetas, uso C ++ / Qt para crear aplicaciones.



Problema



Recientemente me enfrenté a la tarea de guardar los ajustes de configuración en un archivo y cargarlos desde él. Me gustaría esta vez prescindir de diseñar bicicletas y usar alguna clase con costos mínimos para su uso.



Dado que los parámetros se dividen en grupos según los módulos del dispositivo, la versión final es la estructura "Grupo - Clave - Valor". QSettings se volvió adecuado (pero diseñado para esta tarea). El primer intento de la "pluma" dio un fiasco, que no esperaba enfrentar.



Los parámetros se muestran en el programa al usuario en ruso, por lo que nos gustaría almacenarlos en el mismo formulario (para que las personas con poco conocimiento de inglés puedan ver el contenido del archivo).



    //   (   : 
    // 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();


Qué contenido de archivo quería ver:



[ ]
 №1= №1


y que contenía 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


Al mismo tiempo, lo interesante. Si realiza el procedimiento de lectura inversa, cuando muestre el valor, podrá ver que se leyó correctamente.



    // ...   
    
    // 
    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);


Salida de consola:



" №1" " №1" true


La "vieja" solución



Fui a google (en Yandex). Está claro que el problema está en las codificaciones, pero para qué resolverlo usted mismo, cuando en un minuto ya puede encontrar la respuesta :) Me sorprendió que no hubiera soluciones claramente escritas (haga clic aquí, anote esto, viva y sea feliz).



Uno de los pocos temas con el título [RESUELTO]: www.prog.org.ru/topic_15983_0.html . Pero, como resultó durante la lectura del hilo, en Qt4 fue posible resolver el problema con las codificaciones, pero en Qt5 ya no existe: www.prog.org.ru/index.php?topic=15983.msg182962#msg182962 .



Habiendo agregado las líneas con la solución del foro al comienzo del código de "muestra" (bajo el capó hay "juegos" ocultos con todas las codificaciones y funciones posibles de las clases Qt asociadas con ellos), me di cuenta de que esto solo resuelve parcialmente el problema.



    // 
    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);

    // ...   


Pequeño cambio en Application.ini (ahora el valor del parámetro se guarda en cirílico):



[%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


Muleta



Un colega de otro departamento, en el que están haciendo cosas serias, me recomendó que me ocupara de las codificaciones o escribiera funciones personalizadas de lectura y escritura para QSettings que admitirían grupos, claves y sus valores en cirílico. Como la primera opción no dio sus frutos, pasé a la segunda.



Como resultó de la documentación oficial doc.qt.io/qt-5/qsettings.html, puede registrar su propio formato para almacenar datos: doc.qt.io/qt-5/qsettings.html#registerFormat . Todo lo que se requiere es seleccionar la extensión del archivo (sea "* .habr") donde se almacenarán los datos y escribir las funciones anteriores.



Ahora el "relleno" de main.cpp se ve así:



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;
}


Comencemos escribiendo una función para escribir datos en un archivo (guardar datos es más fácil que analizarlos). La documentación doc.qt.io/qt-5/qsettings.html#WriteFunc-typedef dice que la función escribe un conjunto de pares clave / valor . Se llama una vez, por lo que debe guardar los datos a la vez. Los parámetros de la función son QIODevice & device (enlace a "dispositivo de E / S") y QSettings :: SettingsMap (contenedor QMap <QString, QVariant>).



Dado que el nombre de la clave se almacena en el contenedor en la forma "Grupo / parámetro" (interpretando a su tarea), primero debe separar los nombres del grupo y el parámetro. Luego, si ha comenzado el siguiente grupo de parámetros, entonces es necesario insertar el separador como una línea vacía.



//     
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;
}


Puede ejecutar y ver el resultado sin una función de lectura personalizada. Solo necesita reemplazar la cadena de inicialización de formato para QSettings:



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

    // ...  


Datos en archivo:



[ ]
 №1= №1


Salida de consola:



" №1" " №1" true


Esto podría haber terminado. QSettings realiza su función de leer todas las claves, almacenándolas en un archivo. Solo hay un matiz de que si escribe un parámetro sin un grupo, QSettings lo almacenará en su memoria, pero no lo guardará en un archivo (debe agregar el código en la función readParameters en un lugar donde el separador "/" no se encuentra en el nombre de la clave del contenedor const QSettings :: ConfiguraciónMapa y mapa).



Preferí escribir mi propia función para analizar datos de un archivo para poder controlar de manera flexible el tipo de almacenamiento de datos (por ejemplo, los nombres de los grupos no están enmarcados con corchetes, sino con otros caracteres de reconocimiento). Otra razón es mostrar cómo funcionan las cosas con las funciones personalizadas de lectura y escritura. Consulte la



documentación doc.qt.io/qt-5/qsettings.html#ReadFunc-typedefse dice que la función lee un conjunto de pares clave / valor . Debe leer todos los datos en una sola pasada y devolver todos los datos al contenedor, que se especifica como un parámetro de función, y está inicialmente vacío.



//     
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;
}


Devolvemos la función de lectura personalizada para inicializar el formato de QSettings y comprobar que todo funciona:



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

    // ...  


Salida de consola:



" №1" " №1" true


Trabajo con muletas



Dado que "agudicé" la implementación de funciones para mi tarea, necesito mostrar cómo usar la "descendencia" resultante. Como dije anteriormente, si intenta escribir un parámetro sin un grupo, QSettings lo guardará en su memoria y lo mostrará cuando se llame al método allKeys ().



    //  
    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;


Salida de consola (el "Parámetro # 4" es obviamente superfluo aquí):



(" / №3", " №4", " / №1", " / №2")
(" / №3", " №4", " / №1", " / №2")


En este caso, el contenido del archivo:



[ ]
 №3= №3

[ ]
 №1= №1
 №2= №2


La solución al problema de las teclas solitarias es controlar cómo se escriben los datos cuando se usa QSettings. No permita guardar parámetros sin el comienzo y el final del grupo o las claves de filtro que no contengan el nombre del grupo en su nombre.



Conclusión



Se solucionó el problema de mostrar correctamente los grupos, las claves y sus valores. Hay un matiz en el uso de la funcionalidad creada, pero si se usa correctamente, no afectará el funcionamiento del programa.



Una vez hecho el trabajo, parece que sería perfectamente posible escribir un contenedor para QFile y vivir felizmente. Pero por otro lado, además de las mismas funciones de lectura y escritura, tendría que escribir funcionalidades adicionales que QSettings ya tiene (obtener todas las claves, trabajar con un grupo, escribir datos no guardados y otras funcionalidades que no aparecían en el artículo).



¿Cual es el uso? Quizás aquellos que se enfrentan a un problema similar, o que no entienden de inmediato cómo implementar e integrar sus funciones de lectura y escritura, encontrarán útil el artículo. En cualquier caso, será bueno leer sus pensamientos en los comentarios.



All Articles