转自:http://hi.baidu.com/cyclone/blog/item/1bcb5edfed60d20348540320.html
早就注意到 QSettings 提供了自定义配置文件格式的功能,却一直没怎么看。今天用了一天时间,做了个练习,特此记录一下。
-
-- dbzhang800 2010-11-06 23:05:10
拿什么练习呢?
前段时间,有网友抱怨,QSettings 的ini格式中,在section和key中的中文在文件中看到的是乱码(其实是汉字对应的utf16的转义字符)。就以此开始吧:
- 一个自定义的 .ini 格式的文件
- 文件中的 section 和 key 都可以直接显示中文
-
section 的 嵌套方式采用 [A/B/C]
- 尽可能完备一点:QVariant都能正确写入与读出
效果
一个配置文件的例子:
[A/B] emptystringlist = stringlist = abc, d ef, [General] double = 3.14159 emptystring = nullstring = string = hello [section1] bytearray = @Variant(\0\0\0\xc\0\0\0\t......121) point = @Variant(\0\0\0\x19\0\0\0\x64\0\0\0() [section1/section11/section111] nested = @Variant(\0\0\0\x15\0\0\0\n\0\0\0\x14), @Variant(\0\0\0\x13\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0) [你好] 列表 = 中, 国 [你好/汉民] 微测 = 北京
生成该文件的代码(使用起来和标准的 IniFormat 没有区别,自己想换个格式还是蛮方便的):
#include <QtCore/QCoreApplication> #include <QtCore/QDebug> #include <QtCore/QPoint> #include "custom.h" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); QSettings::Format format = QSettings::registerFormat("ini", IniReadFunc, IniWriteFunc); QSettings settings("config.ini", format); settings.setValue("string", "hello"); settings.setValue("emptystring", ""); settings.setValue("nullstring", QString()); settings.setValue("double", 3.14159); settings.beginGroup("section1"); settings.setValue("point", QPoint(100, 40)); settings.setValue("bytearray", QByteArray("......121")); settings.endGroup(); settings.setValue("section1/section11/section111/nested", QVariantList()<<QSize(10,20)<<QRect(0,0,1,1)); settings.beginGroup("A"); settings.beginGroup("B"); settings.setValue("stringlist", QStringList()<<"abc"<<"d ef"<<""); settings.setValue("emptystringlist", QStringList()); settings.endGroup(); settings.endGroup(); settings.beginGroup(QString::fromLocal8Bit("你好")); settings.setValue(QString::fromLocal8Bit("汉民/微测"), QString::fromLocal8Bit("北京")); settings.setValue(QString::fromLocal8Bit("列表"), QStringList() << QString::fromLocal8Bit("中") << QString::fromLocal8Bit("国")); settings.endGroup(); return a.exec(); }
如何实现
如何用操作我们自己定义格式的配置文件呢?
从前面的代码也可以看出:注册自定义格式,使用的是 registerFormat 函数:
Format QSettings::registerFormat ( const QString & extension, ReadFunc readFunc, WriteFunc writeFunc, Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive ) [static]
该函数的中间两个参数,是两个函数指针。我们的任务就是实现这两个函数:一个负责读文件,一个负责写文件
代码(1)
读函数:解析文件,将解析出的内容存放到一个QMap<QString, QVariant>中
bool IniReadFunc(QIODevice &device, QSettings::SettingsMap &settingsMap) { QString currentSection; QTextStream stream(&device); stream.setCodec("UTF-8"); QString data; bool ok = true; while (!stream.atEnd()) { data = stream.readLine(); if (data.trimmed().isEmpty()) { continue; } if (data[0] == QChar('[')) { QString iniSection; int inx = data.indexOf(QChar(']')); if (inx == -1){ ok = false; iniSection = data.mid(1); } else { iniSection = data.mid(1, inx - 1); } iniSection = iniSection.trimmed(); if (iniSection.compare(QString("general"), Qt::CaseInsensitive) == 0) { currentSection.clear(); } else { if (iniSection.compare(QString("%general"), Qt::CaseInsensitive) == 0) { currentSection = QString("general"); } else { currentSection = iniSection; } currentSection += QChar('/'); } } else { bool inQuotes = false; int equalsPos = -1; QList<int> commaPos; int i = 0; while (i < data.size()) { QChar ch = data.at(i); if (ch == QChar('=')) { if (!inQuotes && equalsPos == -1) { equalsPos = i; } } else if (ch == QChar('"')) { inQuotes = !inQuotes; } else if (ch == QChar(',')) { if (!inQuotes && equalsPos != -1) { commaPos.append(i); } } else if (ch == QChar(';') || ch == QChar('#')) { if (!inQuotes) { data.resize(i); break; } } else if (ch == QChar('\\')) { if (++i < data.size()) { } else { ok = false; break; } } i++; } if (equalsPos == -1) { break; } else { QString key = data.mid(0, equalsPos).trimmed(); if (key.isEmpty()) { break; } else { key = currentSection + key; } if (commaPos.isEmpty()) { //value QString v = data.mid(equalsPos+1).trimmed(); if (v.startsWith("\"") && v.endsWith("\"") && v.length()>1) { v = v.mid(1, v.length()-2); } settingsMap[key] = stringToVariant(unescapedString(v)); } else { //value list commaPos.prepend(equalsPos); commaPos.append(-1); QVariantList vals; for (int i=1; i<commaPos.size(); ++i) { QString d = data.mid(commaPos.at(i-1)+1, commaPos.at(i)-commaPos.at(i-1)-1); QString v = d.trimmed(); if (v.startsWith("\"") && v.endsWith("\"") && v.length()>1) { v = v.mid(1, v.length()-2); } vals.append(stringToVariant(unescapedString(v))); } settingsMap[key] = vals; } } } } return ok; }
写函数:内存中的配置信息QMap<QString, QVariant>写入文件
bool IniWriteFunc(QIODevice &device, const QSettings::SettingsMap &settingsMap) { #ifdef Q_OS_WIN const char * const eol = "\r\n"; #else const char eol = '\n'; #endif bool writeError = false; QString lastSection; QMapIterator<QString,QVariant> it(settingsMap); while(it.hasNext() && !writeError) { it.next(); QString key = it.key(); QString section; qDebug()<<"key: "<<key; int idx = key.lastIndexOf(QChar('/')); if (idx == -1) { section = QString("[General]"); } else { section = key.left(idx); key = key.mid(idx+1); if (section.compare(QString("General"), Qt::CaseInsensitive) == 0) { section = QString("[%General]"); } else { section.prepend(QChar('[')); section.append(QChar(']')); } } if (section.compare(lastSection, Qt::CaseInsensitive)) { if (!lastSection.isEmpty()) { device.write(eol); } lastSection = section; if (device.write(section.toUtf8() + eol) == -1) { writeError = true; } } QByteArray block = key.toUtf8(); block += " = "; if (it.value().type() == QVariant::StringList) { foreach (QString s, it.value().toStringList()) { block += escapedString(s); block += ", "; } if (block.endsWith(", ")) { block.chop(2); } } else if (it.value().type() == QVariant::List) { foreach (QVariant v, it.value().toList()) { block += escapedString(variantToString(v)); block += ", "; } if (block.endsWith(", ")) { block.chop(2); } } else { block += escapedString(variantToString(it.value())); } block += eol; if (device.write(block) == -1) { writeError = true; } } return writeError; }
- 我们将ini文件中的键值读入到 QVariant 中,需要两个步骤:
-
因为文件内的一些字符被转义了,比如 "\x1234\t\0"等,所以需要 unescape
- 从 unescape 后的字符串构造出 QVariant
-
- 当将QVariant写入文件时:
- 将 QVariant 转换成字符串
- 处理字符串中的特殊字符,即 escape
于是 4种操作,就对应了下面4个函数:
将 QVariant 转成字符串
QString variantToString(const QVariant &v) { QString result; switch (v.type()) { case QVariant::String: case QVariant::LongLong: case QVariant::ULongLong: case QVariant::Int: case QVariant::UInt: case QVariant::Bool: case QVariant::Double: case QVariant::KeySequence: { result = v.toString(); if (result.startsWith(QChar('@'))) result.prepend(QChar('@')); break; } default: { QByteArray a; { QDataStream s(&a, QIODevice::WriteOnly); s.setVersion(QDataStream::Qt_4_0); s << v; } result = QString("@Variant("); result += QString::fromLatin1(a.constData(), a.size()); result += QChar(')'); break; } } return result; }
从string构造QVariant
QVariant stringToVariant(const QString &s) { if (s.startsWith(QChar('@'))) { if (s.endsWith(QChar(')'))) { if (s.startsWith(QString("@Variant("))) { QByteArray a(s.toUtf8().mid(9)); QDataStream stream(&a, QIODevice::ReadOnly); stream.setVersion(QDataStream::Qt_4_0); QVariant result; stream >> result; return result; } } if (s.startsWith(QString("@@"))) return QVariant(s.mid(1)); } return QVariant(s); }
将字符串进行转义处理
QByteArray escapedString(const QString &src) { bool needsQuotes = false; bool escapeNextIfDigit = false; int i; QByteArray result; result.reserve(src.size() * 3 / 2); for (i = 0; i < src.size(); ++i) { uint ch = src.at(i).unicode(); if (ch == ';' || ch == ',' || ch == '=' || ch == '#') { needsQuotes = true; } if (escapeNextIfDigit && ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'))) { result += "\\x"; result += QByteArray::number(ch, 16); continue; } escapeNextIfDigit = false; switch (ch) { case '\0': result += "\\0"; escapeNextIfDigit = true; break; case '\n': result += "\\n"; break; case '\r': result += "\\r"; break; case '\t': result += "\\t"; break; case '"': case '\\': result += '\\'; result += (char)ch; break; default: if (ch <= 0x1F) { result += "\\x"; result += QByteArray::number(ch, 16); escapeNextIfDigit = true; } else{ result += QString(src[i]).toUtf8(); } } } if (result.size()>0 && (result.at(0)==' ' || result.at(result.size() - 1) == ' ')) { needsQuotes = true; } if (needsQuotes) { result.prepend('"'); result.append('"'); } return result; }
解析转义字符
const char hexDigits[] = "0123456789ABCDEF"; QString unescapedString(const QString &src) { static const char escapeCodes[][2] = { { 'a', '\a' }, { 'b', '\b' }, { 'f', '\f' }, { 'n', '\n' }, { 'r', '\r' }, { 't', '\t' }, { 'v', '\v' }, { '"', '"' }, { '?', '?' }, { '\'', '\'' }, { '\\', '\\' } }; static const int numEscapeCodes = sizeof(escapeCodes) / sizeof(escapeCodes[0]); QString stringResult; int escapeVal = 0; QChar ch; int i = 0; normal: while (i < src.size()) { ch = src.at(i); if (ch == QChar('\\')) { ++i; if (i >= src.size()) { break; } ch = src.at(i++); for (int j = 0; j < numEscapeCodes; ++j) { if (ch == escapeCodes[j][0]) { stringResult += QChar(escapeCodes[j][1]); goto normal; } } if (ch == 'x') { escapeVal = 0; if (i >= src.size()) break; ch = src.at(i); if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f')) goto hexEscape; } else if (ch >= '0' && ch <= '7') { escapeVal = ch.unicode() - '0'; goto octEscape; } else { //skip } } else { stringResult += ch; } i++; } goto end; hexEscape: if (i >= src.size()) { stringResult += QChar(escapeVal); goto end; } ch = src.at(i); if (ch >= 'a') ch = ch.unicode() - ('a' - 'A'); if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')) { escapeVal <<= 4; escapeVal += strchr(hexDigits, ch.toLatin1()) - hexDigits; ++i; goto hexEscape; } else { stringResult += QChar(escapeVal); goto normal; } octEscape: if (i >= src.size()) { stringResult += QChar(escapeVal); goto end; } ch = src.at(i); if (ch >= '0' && ch <= '7') { escapeVal <<= 3; escapeVal += ch.toLatin1() - '0'; ++i; goto octEscape; } else { stringResult += QChar(escapeVal); goto normal; } end: return stringResult; }