introduction
Bonjour, Habr!
Une partie de mon travail consiste à développer de petites applications de bureau. En particulier, ce sont des programmes qui vous permettent de suivre l'état actuel de l'équipement, de le tester, de définir les paramètres de configuration, de lire les journaux ou de vérifier le canal de communication entre deux appareils. Comme vous pouvez le comprendre à partir des balises, j'utilise C ++ / Qt pour créer des applications.
Problème
J'ai récemment été confronté à la tâche d'enregistrer les paramètres de configuration dans un fichier et de les charger à partir de celui-ci. Je voudrais cette fois faire sans concevoir de vélos et utiliser une classe avec des coûts minimes pour son utilisation.
Les paramètres étant divisés en groupes selon les modules de l'appareil, la version finale est la structure "Groupe - Clé - Valeur". QSettings est devenu approprié (mais destiné à cette tâche). Le premier essai du «stylo» a donné un fiasco auquel je ne m'attendais pas à affronter.
Les paramètres sont affichés dans le programme à l'utilisateur en russe, nous aimerions donc les stocker sous la même forme (afin que les personnes ayant une faible connaissance de l'anglais puissent voir le contenu du fichier).
// ( :
// 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();
Quel contenu de fichier je voulais voir:
[ ]
â„–1= â„–1
et qui contenait 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
En même temps, ce qui est intéressant. Si vous effectuez la procédure de lecture inversée, lors de l'affichage de la valeur, vous pouvez voir qu'elle a été lue correctement.
// ...
//
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);
Sortie de la console:
" â„–1" " â„–1" true
La «vieille» solution
Je suis allé sur google (dans Yandex). Il est clair que le problème vient des encodages, mais pourquoi le découvrir vous-même, alors que dans une minute vous pouvez déjà trouver la réponse :) J'ai été surpris qu'il n'y ait pas de solutions clairement écrites (cliquez ici, notez ceci, vivez et soyez heureux).
Un des rares sujets avec le titre [RÉSOLU]: www.prog.org.ru/topic_15983_0.html . Mais, comme il s'est avéré lors de la lecture du thread, dans Qt4, il était possible de résoudre le problème des encodages, mais dans Qt5 il n'y en a plus: www.prog.org.ru/index.php?topic=15983.msg182962#msg182962 .
Après avoir ajouté les lignes avec la solution du forum au début du code "exemple" (sous le capot se trouvent des "jeux" cachés avec tous les encodages et fonctions possibles des classes Qt qui leur sont associées), je me suis rendu compte que cela ne résout que partiellement le problème.
//
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);
// ...
Petit changement dans Application.ini (maintenant la valeur du paramètre est enregistrée en cyrillique):
[%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
BĂ©quille
Un collègue d'un autre département, qui s'occupe des choses sérieuses, m'a conseillé de gérer les encodages ou d'écrire des fonctions de lecture et d'écriture personnalisées pour QSettings qui prendraient en charge les groupes, les clés et leurs valeurs en cyrillique. La première option n'ayant pas porté ses fruits, je suis passée à la seconde.
Comme il ressort de la documentation officielle doc.qt.io/qt-5/qsettings.html, vous pouvez enregistrer votre propre format de stockage des données: doc.qt.io/qt-5/qsettings.html#registerFormat . Il suffit de sélectionner l'extension de fichier (que ce soit "* .habr") où les données seront stockées et d'écrire les fonctions ci-dessus.
Maintenant, le "bourrage" de main.cpp ressemble Ă ceci:
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;
}
Commençons par écrire une fonction pour écrire des données dans un fichier (enregistrer des données est plus facile que de les analyser). La documentation doc.qt.io/qt-5/qsettings.html#WriteFunc-typedef indique que la fonction écrit un ensemble de paires clé / valeur . Il est appelé une fois, vous devez donc enregistrer les données à la fois. Les paramètres de la fonction sont QIODevice & device (lien vers "I / O device") et QSettings :: SettingsMap (QMap <QString, QVariant> container).
Le nom de la clé étant stocké dans le conteneur sous la forme "Groupe / paramètre" (interprétation à votre tâche), vous devez d'abord séparer les noms du groupe et du paramètre. Ensuite, si le groupe de paramètres suivant a commencé, vous devez insérer un séparateur en tant que ligne vide.
//
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;
}
Vous pouvez exécuter et voir le résultat sans fonction de lecture personnalisée. Il vous suffit de remplacer la chaîne d'initialisation du format pour QSettings:
//
const QSettings::Format habrFormat = QSettings::registerFormat(
"habr", QSettings::ReadFunc(), writeParameters, Qt::CaseSensitive);
// ...
Données dans le fichier:
[ ]
â„–1= â„–1
Sortie de la console:
" â„–1" " â„–1" true
Cela aurait pu prendre fin. QSettings remplit sa fonction de lecture de toutes les clés, en les stockant dans un fichier. Seulement il y a une nuance que si vous écrivez un paramètre sans groupe, alors QSettings le stockera dans sa mémoire, mais ne l'enregistrera pas dans un fichier (vous devez ajouter le code dans la fonction readParameters à un endroit où le séparateur "/" ne se trouve pas dans le nom de clé du conteneur const QSettings :: ParamètresMap et carte).
J'ai préféré écrire ma propre fonction pour analyser les données d'un fichier afin de pouvoir contrôler de manière flexible le type de stockage de données (par exemple, les noms de groupe ne sont pas encadrés par des crochets, mais avec d'autres caractères de reconnaissance). Une autre raison est de montrer comment les choses fonctionnent avec les fonctions de lecture et d'écriture personnalisées. Voir la
documentation doc.qt.io/qt-5/qsettings.html#ReadFunc-typedefon dit que la fonction lit un ensemble de paires clé / valeur . Il doit lire toutes les données en une seule passe et renvoyer toutes les données au conteneur, qui est spécifié en tant que paramètre de fonction, et il est initialement vide.
//
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;
}
Nous retournons la fonction de lecture personnalisée pour initialiser le format des QSettings et vérifier que tout fonctionne:
//
const QSettings::Format habrFormat = QSettings::registerFormat(
"habr", readParameters, writeParameters, Qt::CaseSensitive);
// ...
Sortie de la console:
" â„–1" " â„–1" true
Travail à la béquille
Depuis que j'ai "affiné" l'implémentation des fonctions pour ma tâche, j'ai besoin de montrer comment utiliser la "progéniture" résultante. Comme je l'ai dit plus tôt, si vous essayez d'écrire un paramètre sans groupe, QSettings l'enregistrera dans sa mémoire et l'affichera lorsque la méthode allKeys () sera appelée.
//
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;
Sortie console ("Paramètre n ° 4" est évidemment superflu ici):
(" / â„–3", " â„–4", " / â„–1", " / â„–2")
(" / â„–3", " â„–4", " / â„–1", " / â„–2")
Dans ce cas, le contenu du fichier:
[ ]
â„–3= â„–3
[ ]
â„–1= â„–1
â„–2= â„–2
La solution au problème des clés isolées est de contrôler la façon dont les données sont écrites lors de l'utilisation de QSettings. N'autorisez pas l'enregistrement de paramètres sans le début et la fin du groupe ou des clés de filtre qui ne contiennent pas le nom du groupe dans leur nom.
Conclusion
Le problème de l'affichage correct des groupes, des clés et de leurs valeurs a été résolu. Il y a une nuance d'utilisation de la fonctionnalité créée, mais si elle est utilisée correctement, elle n'affectera pas le fonctionnement du programme.
Une fois le travail terminé, il semble qu'il serait parfaitement possible d'écrire un wrapper pour QFile et de vivre heureux. Mais d'un autre côté, en plus des mêmes fonctions de lecture et d'écriture, vous devrez écrire des fonctionnalités supplémentaires que QSettings possède déjà (obtenir toutes les clés, travailler avec un groupe, écrire des données non enregistrées et d'autres fonctionnalités qui n'apparaissaient pas dans l'article).
Quel en est l'usage? Peut-être que ceux qui sont confrontés à un problème similaire, ou qui ne comprennent pas immédiatement comment implémenter et intégrer leurs fonctions de lecture et d'écriture, trouveront l'article utile. Dans tous les cas, il sera agréable de lire vos pensées dans les commentaires.