1. 目标
客户端程序经常需要读取/保存配置,而配置项的内容各种各样,且可能存在多个配置文件,所以我们写一个通用的配置类,具备以下功能特性:
-
json格式
-
读取/保存配置文件
-
统一访问接口
-
线程安全
2. 实现
创建一个configuration模块,包含一个Configuration类:
configuration
- configuration.pri
- configuration.h
- configuration.cpp
2.1 json格式
在Qt程序中,一般使用QVariantMap/QVariantList来匹配json文档结构,而QFile读取文件返回的数据为QByteArray类型,Qt提供了QJsonDocument等类来实现QVariant和QByteArray之间的转换,我们封装一下:
// configuration.h
class Configuration {
...
private:
QVariant json2variant(const QByteArray &json);
QByteArray variant2json(const QVariant &variant);
}
// configuration.cpp
QVariant Configuration::json2variant(const QByteArray &json) {
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(json, &parseError);
if (QJsonParseError::NoError != parseError.error) {
return QVariant();
}
return doc.toVariant();
}
QByteArray Configuration::variant2json(const QVariant &variant) {
QJsonDocument doc = QJsonDocument::fromVariant(variant);
return doc.toJson();
}
2.2 读取/保存配置文件
定义2个protected类型的成员变量:
protected:
QString m_path = ""; // 存储配置文件路径
QVariantMap m_config; // 存储配置
因为配置文件可能默认不存在,所以我们在读取接口中增加一个配置,如果配置文件不存在,可以选择报错或者返回空:
bool readFile(const QString &path, bool allowNotExist = false);
bool saveFile();
实现如下:
bool Configuration::readFile(const QString &path, bool allowNotExist) {
if (!QFile::exists(path)) {
if (!allowNotExist) {
qCritical() << "configuration file not exists: " << path;
return false;
}
m_path = path;
m_config = QVariantMap();
return true;
}
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << "open file failed: " << file.errorString();
return false;
}
QByteArray data = file.readAll();
file.close();
m_path = path;
m_config = json2variant(data).toMap();
return true;
}
bool Configuration::saveFile() {
if (m_path.isEmpty()) {
qCritical() << "configuration file's path cannot be empty";
return false;
}
QFile file(m_path);
if (!file.open(QIODevice::WriteOnly)) {
qCritical() << "open configuration file failed: " << file.errorString();
return false;
}
file.write(variant2json(m_config));
file.flush();
file.close();
return true;
}
2.3 统一访问接口
得益于QVariant类型,我们可以方便地实现统一的读写接口,而不需要根据配置项类型来写不同的读写接口:
bool Configuration::setValue(const QString &key, const QVariant &variant) {
m_config.insert(key, variant);
return saveFile();
}
QVariant Configuration::value(const QString &key) {
return m_config[key];
}
另外程序中有时需要一些临时配置,不需要写入配置文件,可以简单地新增一个成员变量,并增加读写接口:
// configuration.h
class Configuration {
public:
...
QVariant tempValue(const QString &key);
void setTempValue(const QString &key, const QVariant &variant);
protected:
QVariantMap m_tempConfig;
}
// configuration.cpp
QVariant Configuration::tempValue(const QString &key) const { return m_tempConfig[key]; }
void Configuration::setTempValue(const QString &key, const QVariant &variant) {
m_tempConfig.insert(key, variant);
}
如果是自定义类型,可以参考Creating Custom Qt Types | Qt Core 6.7.2
2.4 线程安全
配置类是典型的多读少写模式,用读写锁非常适合(读写锁具备读共享,写独占的特性),我们以QReadWriteLock为例子,在value和setValue两个函数中加入读写锁:
// configuration.h
class Configuration {
...
private:
QReadWriteLock m_lock;
}
// configuration.cpp
QVariant Configuration::value(const QString &key) {
QReadLocker locker(&m_lock);
return m_config[key];
}
bool Configuration::setValue(const QString &key, const QVariant &variant) {
QWriteLocker locker(&m_lock);
m_config.insert(key, variant);
return saveFile();
}
PS: 代码已经开源在github:linqiaqun/music-player: A cross platform music player (github.com) 欢迎star/fork/issue