Практически в каждом проекте, встает задача персистентного чтения/записи конфигурации. Не секрет что существует большое количество уже готовых библиотек для решения этой задачи. Некоторые из-них просты, некоторые чуть сложнее в использовании.
Если же проект разрабатывается с использованием Qt, думаю нет смысла линковать дополнительную библиотеку, так как в Qt есть все средства для создания очень простого, гибкого и кроссплатформенного решения.
Как раз о таком решении хочу расказать вам в этом посте.
Введение
В Qt есть очень удобный класс QSettings. В принципе он очень прост в использовании:
/*
main.cpp
*/
int main(int argc, char *argv[]){
// эти настройки используются (неявно) классом QSettgins для
// определения имени и местоположения конфига
QCoreApplication::setOrganizationName("org");
QCoreApplication::setApplicationName("app");
...
return 0;
}
/*
some.cpp
*/
void func(){
QSettgins conf;
...
// запись в конфиг
conf.setValue("section1/key1", someData); // запись в секцию section1
conf.setValue("key2", someData2); // запись в секцию General
...
// чтение из конфига
QString strData = conf.value("section1/key1").toString();
}
Из приведенного выше примера, обычного использования QSetgins, сразу становятся видны проблемы расширяемости и поддержки кода:
- Если имена ключей прописывать явно в коде, то в дальнейшем мы можем столкнуться с ситуацией когда будет сложно удалять/добавлять новые ключи конфигурации. Т.е. при таком подходе, тут проблема в том что на этапе компиляции невозможно выловить инвалидные ключи.
- Чтобы избежать проблемы #1 мы могли бы выписать все ключи в отдельный заголовочный файл, и обращаться к ним через строковые константы. Для улучшения модульности кода и очистки глобальной области видимости, также стоило бы поместить все ключи в отдельное пространство имен.
namespace Settings{ const char * const key1 = "key1"; const char * const section1_key1 = "section1/key1"; const char * const section1_key2 = "section1/key2"; }
Но тут у нас появляется другая не очень приятная деталь:
* во первых слишком многословно, т.е. информация дублируется (key1 -> «key1», и т.д.). В принципе это не удивительно, так как мы же как-то должны описать сериализацию имен ключей. Да мы могли бы написать макрос, но, по известным причинам, макросы стоит избегать, тем более если есть альтернативные варианты.
* во вторых при достаточном количестве ключей и секций, велика вероятность, что придется прописывать константы для всех комбинаций, что не очень удобно. Конечно же мы можем завести константы для ключей и для секций отдельно, но тогда, при каждом обращении в QSettings, придется производить объединение строк.
Если внимательно еще раз просмотреть все вышеописанные проблемы, то можно сделать вывод: ключ представлен строкой — это и есть основная проблема. Ведь действительно, если в качестве ключа мы будем использовать перечисления (enums), то все вышеперечисленное разом улетучивается.
Перечисления конечно же удобны, но QSettings требует, в качестве параметра ключа — строку. Т.е. нам нужен некоторый механизм, который давал бы нам возможность транслировать значения перечислений в строки (извлекать строковые значения элементов перечислений). Например из следующего перечисления:
enum Key{
One,
Two,
Three
};
нужно как-то извлечь 3 строки: «One», «Two», «Three».
К сожалению стандартными средствами C++ это сделать невозможно. Но как же быть?
Тут нам на помощь приходит Qt со своей метаобъектной моделью, а если точнее QMetaEnum. Про QMetaEnum писать не буду, так как это уже отдельная тема. Могу лишь дать ссылки: раз, два.
Реализация
Имея на вооружении QMetaEnum, теперь мы можем реализовать класс Settings, лишенный всех вышеперечисленных недостатков, а также предоставляющий возможность задания дефолтных настроек. Класс Settings представляет из себя синглтон Мейерса, это нам дает простоту настройки и его использования:
/*
settings.h
*/
#ifndef SETTINGS_H
#define SETTINGS_H
#include <QVariant>
#include <QSettings>
#include <QMetaEnum>
/**
@brief Синглтон для доступа к конфигурации
Usage:
@code
...
...
//пердварительная настройка (должн быть где-нибуль в main)
QApplication::setOrganizationName("Organization name");
QApplication::setApplicationName("App name");
...
...
//установка значений по умолчанию (строка может быть многострочной)
Settings::setDefaults("SomeKey: value1; SomeSection/SomeKey: value2");
//или так
QFile f(":/defaults/config");
f.open(QIODevice::ReadOnly);
Settings::setDefaults(f.readAll());
...
...
void fun(){
...
QVariant val1 = Settings::get(Settings::SomeKey);
Settings::set(Settings::SomeKey) = "new val1";
...
QVariant val2 = Settings::get(Settings::SomeKey, Settings::SomeSection);
Settings::set(Settings::SomeKey, Settings::SomeSection) = "new val2";
...
}
@endcode
*/
class Settings{
Q_GADGET
Q_ENUMS(Section)
Q_ENUMS(Key)
public:
enum Section{
General,
Network,
Proxy
};
enum Key{
URI,
Port,
User,
Password
};
class ValueRef{
public:
ValueRef(Settings &st, const QString &kp) :
parent(st), keyPath(kp){}
ValueRef & operator = (const QVariant &d);
private:
Settings &parent;
const QString keyPath;
};
static void setDefaults(const QString &str);
static QVariant get(Key, Section /*s*/ = General);
static ValueRef set(Key, Section /*s*/ = General);
private:
QString keyPath(Section, Key);
static Settings & instance();
QMetaEnum keys;
QMetaEnum sections;
QMap<QString, QVariant> defaults;
QSettings conf;
Settings();
Settings(const Settings &);
Settings & operator = (const Settings &);
};
#endif // SETTINGS_H
/*
settings.cpp
*/
#include "settings.h"
#include <QSettings>
#include <QMetaEnum>
#include <QRegExp>
#include <QStringList>
Settings::Settings(){
const QMetaObject &mo = staticMetaObject;
int idx = mo.indexOfEnumerator("Key");
keys = mo.enumerator(idx);
idx = mo.indexOfEnumerator("Section");
sections = mo.enumerator(idx);
}
QVariant Settings::get(Key k, Section s){
Settings &self = instance();
QString key = self.keyPath(s, k);
return self.conf.value(key, self.defaults[key]);
}
Settings::ValueRef Settings::set(Key k, Section s){
Settings &self = instance();
return ValueRef(self, self.keyPath(s, k));
}
void Settings::setDefaults(const QString &str){
Settings &self = instance();
//section/key : value
//section - optional
QRegExp rxRecord("^\s*(((\w+)/)?(\w+))\s*:\s*([^\s].+)$");
auto kvs = str.split(QRegExp(";\W*"), QString::SkipEmptyParts); //key-values
for(auto kv : kvs){
if(rxRecord.indexIn(kv) != -1){
QString section = rxRecord.cap(3);
QString key = rxRecord.cap(4);
QString value = rxRecord.cap(5);
int iKey = self.keys.keyToValue(key.toLocal8Bit().data());
if(iKey != -1){
int iSection = self.sections.keyToValue(section.toLocal8Bit().data());
if(section.isEmpty() || iSection != -1){
self.defaults[rxRecord.cap(1)] = value;
}
}
}
}
}
//Settings::ValueRef-----------------------------------------------------------
Settings::ValueRef & Settings::ValueRef::operator = (const QVariant &data){
parent.conf.setValue(keyPath, data);
return *this;
}
//PRIVATE METHODS--------------------------------------------------------------
QString Settings::keyPath(Section s, Key k){
auto szSection = sections.valueToKey(s);
auto szKey = keys.valueToKey(k);
return QString(s == General ? "%1" : "%2/%1").arg(szKey).arg(szSection);
}
Settings & Settings::instance(){
static Settings singleton;
return singleton;
}
В данной реализации, класс QSettings, используется исключительно для кроссплатформенного доступа к настройкам. Конечно же по желанию QSettgins может быть заменен любым другим механизмом, например SQLite.
Пример использования
Класс Settings предоставляет очень простой и удобный интерфейс, состоящий всего из трех статических методов:
void setDefaults(const QString &str);
— установка параметров поумолчанию
QVariant get(Key, Section);
— чтение значения (секция может быть опущена)
ValueRef set(Key, Section);
— запись значения (секция может быть опущена)
/*
main.cpp
*/
#include <QtCore/QCoreApplication>
#include <QUrl>
#include <QFile>
#include "settings.h"
void doSome(){
//чтение из секции General
QString login = Settings::get(Settings::User).toString(); // login == "unixod"
QUrl proxyUrl = Settings::get(Settings::URI, Settings::Proxy).toUrl(); // http://proxy_uri
QString generalUrl = Settings::get(Settings::URI).toString(); // пусто
if(generalUrl.isEmpty())
Settings::set(Settings::URI) = "http://some_uri";
}
int main(int argc, char *argv[]){
//данные параметры используются QSetgins для определения куда сохранять конфигурацию
QCoreApplication::setOrganizationName("unixod");
QCoreApplication::setApplicationName("app");
//по желанию можем установить дефолтную конфигурацию:
QFile cfgDefaults(":/config/default.cfg"); // я обычно дефолтовые настройки помещаю в ресурсы
cfgDefaults.open(QIODevice::ReadOnly);
Settings::setDefaults(cfgDefaults.readAll());
//...
doSome();
//...
return 0;
}
вот пример синтаксиса описания настроек по умолчанию:
Proxy/URI: http://proxy_uri;
User: unixod;
как можно заметить формат — простой:
[section name]/key : value;
Заключение
Стоит заметить что данный класс Settings легко расширяется. Т.е. при желании, добавить/удалить/переименовать какие-нибудь ключи или секции, всего лишь надо изменить соответствующий enum!
У читающего может возникнуть вопрос а нельзя ли как нибудь вынести общую логику «за скобки».
Ответ: можно но лучше не стоит. Так как метаобъектная модель Qt не работает с шаблонами, придется использовать макросы, что в свою очередь влечет за собой известные проблемы:
- Сложность отладки
- Затруднение анализа кода для IDE
- Сложность восприятия, читающим, кода
- и т.д.
При сборке не забываем включить поддержку С++11:
- GCC:
-std=с++0x - Qt project file:
QMAKE_CXXFLAGS += -std=c++0x
Спасибо за внимание. )
Автор: unixod