Практически в каждом проекте, встает задача персистентного чтения/записи конфигурации. Не секрет что существует большое количество уже готовых библиотек для решения этой задачи. Некоторые из-них просты, некоторые чуть сложнее в использовании.
Если же проект разрабатывается с использованием Qt, думаю нет смысла линковать дополнительную библиотеку, так как в Qt есть все средства для создания очень простого, гибкого и кроссплатформенного решения.
Как раз о таком решении хочу расказать вам в этом посте.
В Qt есть очень удобный класс QSettings. В принципе он очень прост в использовании:
Из приведенного выше примера, обычного использования QSettings, сразу становятся видны проблемы расширяемости и поддержки кода:
Если внимательно еще раз просмотреть все вышеописанные проблемы, то можно сделать вывод: ключ представлен строкой — это и есть основная проблема. Ведь действительно, если в качестве ключа мы будем использовать перечисления (enums), то все вышеперечисленное разом улетучивается.
Перечисления конечно же удобны, но QSettings требует, в качестве параметра ключа — строку. Т.е. нам нужен некоторый механизм, который давал бы нам возможность транслировать значения перечислений в строки (извлекать строковые значения элементов перечислений). Например из следующего перечисления:
нужно как-то извлечь 3 строки: «One», «Two», «Three».
К сожалению стандартными средствами C++ это сделать невозможно. Но как же быть?
Тут нам на помощь приходит Qt со своей метаобъектной моделью, а если точнее QMetaEnum. Про QMetaEnum писать не буду, так как это уже отдельная тема. Могу лишь дать ссылки: раз, два.
Имея на вооружении QMetaEnum, теперь мы можем реализовать класс Settings, лишенный всех вышеперечисленных недостатков, а также предоставляющий возможность задания дефолтных настроек. Класс Settings представляет из себя синглтон Мейерса, это нам дает простоту настройки и его использования:
В данной реализации, класс QSettings, используется исключительно для кроссплатформенного доступа к настройкам. Конечно же по желанию QSettgins может быть заменен любым другим механизмом, например SQLite.
Класс Settings предоставляет очень простой и удобный интерфейс, состоящий всего из трех статических методов:
вот пример синтаксиса описания настроек по умолчанию:
как можно заметить формат — простой:
Стоит заметить что данный класс Settings легко расширяется. Т.е. при желании, добавить/удалить/переименовать какие-нибудь ключи или секции, всего лишь надо изменить соответствующий enum!
У читающего может возникнуть вопрос а нельзя ли как нибудь вынести общую логику «за скобки».
Ответ: можно но лучше не стоит. Так как метаобъектная модель Qt не работает с шаблонами, придется использовать макросы, что в свою очередь влечет за собой известные проблемы:
При сборке не забываем включить поддержку С++11:
Спасибо за внимание. )
Если же проект разрабатывается с использованием 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(){ QSettings conf; ... // запись в конфиг conf.setValue("section1/key1", someData); // запись в секцию section1 conf.setValue("key2", someData2); // запись в секцию General ... // чтение из конфига QString strData = conf.value("section1/key1").toString(); }
Из приведенного выше примера, обычного использования QSettings, сразу становятся видны проблемы расширяемости и поддержки кода:
- Если имена ключей прописывать явно в коде, то в дальнейшем мы можем столкнуться с ситуацией когда будет сложно удалять/добавлять новые ключи конфигурации. Т.е. при таком подходе, тут проблема в том что на этапе компиляции невозможно выловить инвалидные ключи.
- Чтобы избежать проблемы #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 (Раскрыть спойлер)
/* 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 (Раскрыть спойлер)
/* 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].{0,})\\b\\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[]){ //данные параметры используются QSettings для определения куда сохранять конфигурацию QCoreApplication::setOrganizationName("unixod"); QCoreApplication::setApplicationName("app"); //по желанию можем установить дефолтную конфигурацию: QFile cfgDefaults(":/config/default.cfg"); // я обычно дефолтовые настройки помещаю в ресурсы cfgDefaults.open(QIODevice::ReadOnly); Settings::setDefaults(cfgDefaults.readAll()); //... doSome(); //... return 0; }
вот пример синтаксиса описания настроек по умолчанию:
default.cfg (Раскрыть спойлер)
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
Спасибо за внимание. )
