Беседы о Qt: QSettings и XML
Класс QSettings служит Qt-программистам верой и правдой, когда нужно сохранить или загрузить какие-нибудь настройки программы. Технические заботы по хранению настроек класс берет на себя. В Linux по умолчанию создается ini-подобный файл в каталоге $HOME/.config/имя_программы, а в Windows записи помещаются в системный реестр. Такое поведение можно переопределить, вызвав конструктор с именем файла и указав формат: в случае QSettings::IniFormat и в Windows будет использован ini-подобный файл.
С точки зрения доступа к данным разницы нет никакой: для чтения и записи используются одни и те же функции вне зависимости от физического способа хранения данных. Пример чтения:
QString s = settings->value ("my string",
"my default value").toString();Пример записи:
QString s;
settings->setValue ("my string", s);Кроме таких обыденных вещей, как хранение «единичных» переменных, QSettings предоставляет мощный механизм сохранения других данных, например состояния QSplitter (то есть размеров виджетов, вставленных в сплиттер), а также состояния вспомогательных виджетов главного окна, таких как инструментальных панелей.
У класса QMainWindow есть методы restoreState и saveState. Для сохранения достаточно вызвать при закрытии окна:
settings->setValue ("state", saveState());Для восстановления можно поместить в конструктор следующее:
restoreState (settings->value ("state",
QByteArray()) .toByteArray());В итоге будет восстановлено положение панелей инструментов, причем если они были не припаркованы, то это состояние тоже сохраняется и восстанавливается. Замечу, что для правильной работы этого механизма панелям управления надо сначала дать имена, а потом уже сохранять или восстанавливать. Например:
tb _ my _ toolbar = new QToolBar;
tb _ my _ toolbar->setObjectName ("tb _ my _ toolbar");К сплиттерам это не относится. Состояние сплиттера восстанавливается примерно так:
my _ splitter->restoreState (settings->value
("splitterSizes").toByteArray());А сохраняется следующим образом:
settings->setValue ("splitterSizes", my _ splitter->saveState());Замечательное свойство QSettings сохранять и загружать данные любых типов объясняется тем, что записываются и возвращаются экземпляры не менее замечательного класса QVariant. В него можно поместить что угодно: от строки или числа до целой хэш-таблицы. Трудно вообразить для хранения пар формата «ключ=значение» что-либо удобнее, чем класс QSettings, однако, если возникнет надобность написать своё решение, это нетрудно сделать при наличии других классов из состава Qt. Всего-то и нужно, что прочитать текст из файла, разбить его на строки, разобрать пары из каждой строки и поместить их - для ускорения доступа - в хэш-таблицу.
Для чтения текста из файла можно использовать такую функцию:
QString qstring _ load (const QString SfileName,
const QString Scharset)
{
QFile file (fileName);
if (! file.open (QFile: :ReadOnly | QFile::Text))
return QString();
QTextStream in(Sfile);
in. setCodec (charset. toAscii ());
return in.readAll();
}Вот как читать текст из файла:
QString text = qstring _ load ("/test/test.txt", "UTF-8");Теперь - создание списка из полученного текста, предполагая, что в тексте каждая строка отделена от другой с помощью «n»:
QStringList 1 = text.split ("n");Для краткости можно эти два действия объединить в одно:
QStringList 1 = qstring _load (fname).split ("n");Что случится, если возвращенная от qstring_load строка вдруг окажется пустой? Ничего - ведь это не null-строка как указатель, а экземпляр класса. Возвратится вполне готовый класс QString, и не его вина, что текста-то в нем может не быть, и делить на строки нечего. При попытке разобрать на строки такую «пустую» строку будет возвращен QStringList с нулем элементов.
Преобразование QStringList в хэш-таблицу делается просто. Вообразим, что мы хотим трактовать не только ключи (из прочитанного файла), но и сопоставленные им значения как строки. Такому положению вещей будет соответствовать хэш-таблица следующего вида:
QHash <QString, QString> my _ table;Напишем функцию, которая получает в качестве параметра имя файла, загружает из него «ini-данные» и возвращает их как готовую к использованию хэш-таблицу:
QHash <QString, QString> hash _ load _ keyval (const QString Sfname) {
//Сюда будем записывать разобранные строки: QHash<QString, QString> result;
//Читаем из файла в список:
QStringList l = qstring _load (fname).split ("n");
//Проходим по всему списку: foreach (QString s, l) {
//Делим текущую строку списка на два элемента, //разделенные знаком равенства:
QStringList sl = s.split ("=");
//Если элементов больше 1, всё правильно, //вставляем ключ и значение в хэш-таблицу if (sl.size() > 1)
result.insertMulti (sl[0], sl[1]);
}
//Возвращаем заполненную таблицу: return result;
}Обратите внимание на функцию QHash::insertMulti. Вместо неё можно было бы использовать просто insert, но в таком случае теряются все повторяющиеся значения (для ключа), кроме последнего.
Сохранение хэш-таблицы в файле выглядит примерно так: получаем список уникальных ключей QList[Key] QHash::uniqueKeys (), затем для каждого из них получаем список значений QList[T] QHash::values (const Key & key), формируем строки вида «ключ=значение» и записываем их в QStringList, после чего вызываем QStringList::join («n»), чтобы получить готовую строку со всеми данными хэш-таблицы. Теперь эту строку можно сохранить в файл, причем хорошо бы отрезать последний символ строки (там находится лишний «n»).
Теперь - об XML. К XML обращаются, как правило, в двух случаях: когда совсем неудобно хранить данные в формате «ключ=значение» или когда под рукой есть хороший XML-парсер и очень уж хочется его опробовать. По большому счету там, где можно обойтись ini-подобными файлами, нет нужды палить из пушки по воробьям. XML хорош там, где надо сохранять и загружать древовидные структуры данных. Qt дает целых три механизма работы с XML. Первый называется SAX и предоставляет способ работы с XML, основанный на событиях. То есть запускается парсер и на возникающие при разборе XML-данных события (например, открытие тега) вызываются заданные вами обработчики. Для работы методом SAX у Qt служит QXmlSimpleReader и сопутствующие ему классы.
Другой механизм - QDomDocument. Подход тут другой: парсеру передается весь текст документа, который сразу разбирается, и вам остается «ходить» по уже разобранному дереву данных, доступному внутри QDomDocument. Остановлюсь на этом подробнее. Допустим, мы хотим написать простой парсер документов OpenDocument Text (ODT). Напомню, что ODT - это обычный ZIP-архив, наполненный файлами как с текстом/изображениями, так и служебного назначения. Собственно текст находится в файле content.xml. Распаковка ZIP-архивов выходит за рамки этой статьи. Предположим, что мы уже прошли этап распаковки и можем передать парсеру содержимое файла content.xml. Напишем класс CODTXMLWalker, который будет извлекать весь текст из XML-дерева. В классе нам понадобятся следующие поля:
- QString data - сюда мы будем помещать параграфы текста, извлеченные из узлов дерева;
- QDomDocument doc - экземпляр парсера.
Важна функция void step (QDomNode node) - она послужит для итерационного прохождения по всем элементам дерева, на всех его уровнях. Итак, объявление класса:
class CODTXMLWalker: public QObject {
Q _ OBJECT public:
QString data; QDomDocument doc;
void step (QDomNode node);
};Воплощение функции step:
void CODTXMLWalker::step (QDomNode node) {
//Для всех детей узла node проходим в цикле:
for (QDomNode n = node.firstChild(); ! n.isNull();
n = n.nextSibling())
{
//Преобразуем узел (текущее «дитя») в элемент: QDomElement e = n.toElement();
//Если элемент ничего не содержит, переходим к следующему:
if (e.isNull())
continue;
//Если имя узла равно «text:s», у нас возможен отступ: if (e.nodeName() == "text:s") {
//Проверяем наличие отступа:
QString indent = e.attribute ("text:c");
//Если есть отступ, создаем строку из пробелов в количестве
//значений отступа:
if (! indent.isEmpty())
{
QString fillval;
fillval = fillval.fill (' indent.toInt()); //И добавляем эту строку к data: data.append (fillval);
}
}
else
//Иначе, если такие-то имена узлов, то у нас параграф либо заголовок if (e.nodeName() == "text:p" || e.nodeName() == "text:h")
{
//Если текст в них не пуст, добавляем его к data if (! e.text().isEmpty())
{
data.append (e.text()); data.append ("n");
}
}
//Если у узла есть дети, делаем рекурсивный вызов step,
//чтобы этих детей тоже обработать:
if (e.hasChildNodes())
step (n);
}
}Пример использования нашего парсера:
QString string _ data = //Читаем сюда содержимое content.xml
//Создаем парсер:
CODTXMLWalker walker;
//Устанавливаем ему содержимое, что вызывает разбор оного:
walker->doc.setContent (string _ data);
//Теперь проходим по всем элементам разобранного XML-файла:
walker->step (walker->doc.documentElement());
//Выводим извлеченный текст на консоль:
qDebug() << walker->data;Конечно же, дерево объектов можно изменять (например, добавлять в него новые элементы), а потом получить готовый XML-файл в виде текстового объекта, вызвав функцию QDomDocument::toString().

Наконец, в Qt существует третий способ работы с XML, который подразумевает использование классов QXmlStreamReader и QXmlStreamWriter. Оба «заточены» на работу с рекурсией, хотя сами классы обрабатывают данные последовательно. Что имеется в виду? Допустим, мы рекурсивно обходим какое-то дерево и по ходу дела пишем в XML-поток (это может быть файл, буфер в памяти и тому подобное) элементы. А при чтении мы последовательно получаем элементы, один за другим, «знакомясь» с ними по мере их вложенности. Чтобы проверить, является ли очередная разобранная составляющая потока началом элемента, достаточно вызвать функцию isStartElement(). Если она возвращает истину - мы находимся за открывающим тегом. Для проверки на закрытие тега есть парная функция - isEndElement(). В примере ниже показано чтение XML-файла и разбор его на элементы. В цикле разбора на консоль выводится название очередного элемента, а также список его атрибутов и их значений:
QFile file (filename);
if (! file.open (QIODevice::ReadOnly | QIODevice::Text))
return;
QXmlStreamReader xml (Sfile);
while (! xml.atEnd()) {
xml.readNext();
if (xml.isStartElement()) {
qDebug() << xml.name().toString(); QXmlStreamAttributes attrs =
xml.QXmlStreamReader::attributes(); for (int i = 0; i < attrs.count();
qDebug() << attrs[i].name().toString()
<< "=" << attrs[i].value().toString();
}
}Именно этот, третий, способ работы с XML кажется мне наиболее удобным при сохранении/загрузке каких-нибудь настроек или для построения интерфейса на основе XML. Ведь если в случае с QDomDocument вы после разбора XML должны пройтись рекурсивно по всему дереву, то здесь можете что-либо делать прямо во время разбора. А QDomDocument более пригоден, когда требуется именно работа с элементами дерева, а не просто хранение и загрузка.