Один из методов работы с конфигурационными файлами в С++ (Qt)

в 16:29, , рубрики: c++, config, qt, Qt Software, tricks, Программирование, метки: , , , ,

Практически в каждом проекте, встает задача персистентного чтения/записи конфигурации. Не секрет что существует большое количество уже готовых библиотек для решения этой задачи. Некоторые из-них просты, некоторые чуть сложнее в использовании.
Если же проект разрабатывается с использованием 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. Если имена ключей прописывать явно в коде, то в дальнейшем мы можем столкнуться с ситуацией когда будет сложно удалять/добавлять новые ключи конфигурации. Т.е. при таком подходе, тут проблема в том что на этапе компиляции невозможно выловить инвалидные ключи.
  2. Чтобы избежать проблемы #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].+)$");

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

вот пример синтаксиса описания настроек по умолчанию:

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

Спасибо за внимание. )

Автор: unixod


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js