Custom QSettings :: ReadFunc and QSettings :: WriteFunc, ou como escrevi uma muleta para Russify o arquivo de configurações

Introdução



Olá, Habr!



Parte do meu trabalho é desenvolver pequenos aplicativos de desktop. Em particular, são programas que permitem rastrear o estado atual do equipamento, testá-lo, definir parâmetros de configuração, ler logs ou verificar o canal de comunicação entre dois dispositivos. Como você pode entender pelas tags, eu uso C ++ / Qt para criar aplicativos.



Problema



Recentemente, enfrentei o desafio de salvar as definições de configuração em um arquivo e carregá-las a partir dele. Gostaria que desta vez fizesse sem desenhar bicicletas e usasse alguma classe com custos mínimos para a sua utilização.



Como os parâmetros são divididos em grupos de acordo com os módulos do dispositivo, a versão final é a estrutura "Grupo - Chave - Valor". QSettings tornou-se adequado (mas destinado a esta tarefa). A primeira tentativa da "caneta" deu um fiasco, que eu não esperava enfrentar.



Os parâmetros são exibidos no programa para o usuário em russo, portanto, gostaríamos de armazená-los da mesma forma (para que as pessoas que não estão muito familiarizadas com o inglês possam ver o conteúdo do arquivo).



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


Que conteúdo de arquivo eu queria ver:



[ ]
 №1= №1


e que continha 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


Ao mesmo tempo, o que é interessante. Se você fizer o procedimento de leitura inversa, ao exibir o valor, poderá ver que foi lido corretamente.



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


Saída do console:



" №1" " №1" true


A "velha" solução



Eu fui ao google (em Yandex). É claro que o problema é com as codificações, mas por que descobrir você mesmo, se em um minuto você já pode descobrir a resposta :) Fiquei surpreso que não houvesse soluções claramente escritas (clique aqui, escreva, viva e seja feliz).



Um dos poucos tópicos com o título [RESOLVIDO]: www.prog.org.ru/topic_15983_0.html . Mas, como se viu durante a leitura do tópico, no Qt4 foi possível resolver o problema com as codificações, mas no Qt5 não existe mais: www.prog.org.ru/index.php?topic=15983.msg182962#msg182962 .



Tendo adicionado as linhas com a solução do fórum ao início do código de "amostra" (por baixo do capô estão "jogos" escondidos com todas as codificações e funções possíveis das classes Qt associadas a eles), percebi que isso resolve o problema apenas parcialmente.



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

    // ...   


Pequena alteração em Application.ini (agora o valor do parâmetro é salvo em 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



Um colega de outro departamento, que lida com coisas sérias, me aconselhou a lidar com codificações ou escrever funções personalizadas de leitura e gravação para QSettings que dariam suporte a grupos, chaves e seus valores em cirílico. Como a primeira opção não deu frutos, passei para a segunda.



Como descobrimos na documentação oficial doc.qt.io/qt-5/qsettings.html, você pode registrar seu próprio formato para armazenar dados: doc.qt.io/qt-5/qsettings.html#registerFormat . Tudo o que é necessário é selecionar a extensão do arquivo (que seja "* .habr") onde os dados serão armazenados e escrever as funções acima.



Agora, o "recheio" de main.cpp se parece com isto:



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


Vamos começar escrevendo uma função para gravar dados em um arquivo (salvar dados é mais fácil do que analisá-los). A documentação doc.qt.io/qt-5/qsettings.html#WriteFunc-typedef diz que a função grava um conjunto de pares chave / valor . Ele é chamado uma vez, portanto, você precisa salvar os dados de cada vez. Os parâmetros da função são QIODevice & device (link para "dispositivo de E / S") e QSettings :: SettingsMap (QMap <QString, QVariant> container).



Visto que o nome da chave é armazenado no container na forma "Grupo / parâmetro" (interpretando para sua tarefa), você deve primeiro separar os nomes do grupo e do parâmetro. Então, se o próximo grupo de parâmetros foi iniciado, você precisa inserir um separador como uma linha vazia.



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


Você pode executar e ver o resultado sem uma função de leitura personalizada. Você só precisa substituir a string de inicialização de formato para QSettings:



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

    // ...  


Dados no arquivo:



[ ]
 №1= №1


Saída do console:



" №1" " №1" true


Isso poderia ter acabado. QSettings desempenha sua função de ler todas as chaves, armazenando-as em um arquivo. Só há uma nuance de que se você escrever um parâmetro sem um grupo, QSettings irá armazená-lo em sua memória, mas não irá salvá-lo em um arquivo (você precisa adicionar o código na função readParameters em um lugar onde o separador "/" não seja encontrado no nome da chave do contêiner const QSettings :: SettingsMap e mapa).



Eu preferi escrever minha própria função para analisar dados de um arquivo a fim de ser capaz de controlar com flexibilidade o tipo de armazenamento de dados (por exemplo, os nomes dos grupos não são enquadrados com colchetes, mas com outros símbolos de reconhecimento). Outro motivo é mostrar como as coisas funcionam com funções personalizadas de leitura e gravação. Consulte a



documentação doc.qt.io/qt-5/qsettings.html#ReadFunc-typedefdiz-se que a função lê um conjunto de pares de chave / valor . Ele deve ler todos os dados em uma passagem e retornar todos os dados ao contêiner, que é especificado como um parâmetro de função e está inicialmente vazio.



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


Retornamos a função de leitura personalizada para a inicialização do formato para QSettings e verificamos se tudo funciona:



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

    // ...  


Saída do console:



" №1" " №1" true


Trabalho de muleta



Já que "aprimorei" a implementação de funções para minha tarefa, preciso mostrar como usar a "prole" resultante. Como eu disse antes, se você tentar escrever um parâmetro sem um grupo, QSettings o salvará em sua memória e o exibirá quando o método allKeys () for chamado.



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


Saída do console ("Parâmetro # 4" é obviamente supérfluo aqui):



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


Nesse caso, o conteúdo do arquivo:



[ ]
 №3= №3

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


A solução para o problema das chaves solitárias é controlar como os dados são gravados ao usar QSettings. Não permite salvar parâmetros sem o início e o fim do grupo ou chaves de filtro que não contenham o nome do grupo em seu nome.



Conclusão



O problema de exibição correta de grupos, chaves e seus valores foi resolvido. Há uma nuance de usar a funcionalidade criada, mas se usada corretamente, não afetará a operação do programa.



Depois do trabalho feito, parece que seria perfeitamente possível escrever um wrapper para QFile e viver feliz. Mas, por outro lado, além das mesmas funções de leitura e gravação, você teria que escrever funcionalidades adicionais que QSettings já possui (obter todas as chaves, trabalhar com um grupo, escrever dados não salvos e outras funcionalidades que não aparecem no artigo).



Qual o uso? Talvez para aqueles que enfrentam um problema semelhante, ou que não entendem imediatamente como implementar e integrar suas funções de leitura e escrita, o artigo parecerá útil. Em qualquer caso, será bom ler sua opinião nos comentários.



All Articles