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.