Жасмин Бланшет - QT 4: программирование GUI на С++
QDataStream работает с разнообразными типами данных С++ и Qt; полный их список доступен в сети Интернет по адресу http://doc.trolltech.com/4.1/datastreamformat.html. Кроме того, можно добавить поддержку своих собственных пользовательских типов, перегружая операторы << и >>. Ниже приводится определение пользовательского типа данных, которое может быть использовано совместно с QDataStream:
01 class Painting
02 {
03 public:
04 Painting() { myYear = 0; }
05 Painting(const QString &title, const QString &artist, int year) {
06 myTitle = title;
07 myArtist = artist;
08 myYear = year;
09 }
10 void setTitle(const QString &title) { myTitle = title; }
11 QString title() const { return myTitle; }
12 …
13 private:
14 QString myTitle;
15 QString myArtist;
16 int myYear;
17 };
18 QDataStream &operator << (QDataStream &out, const Painting &painting);
19 QDataStream &operator >> (QDataStream &in, Painting &painting);
Ниже показана возможная реализация оператора <<:
01 QDataStream &operator << (QDataStream &out, const Painting &painting)
02 {
03 out << painting.title() << painting.artist()
04 << quint32(painting.year());
05 return out;
06 }
Для вывода Painting мы просто выводим две строки типа QString и значение типа quint32. В конце функции мы возвращаем поток. Этот обычный в С++ прием позволяет использовать последовательность операторов << для вывода данных в поток. Например:
out << painting1 << painting2 << painting3;
Реализация оператора >> аналогична реализации оператора <<.
01 QDataStream &operator >> (QDataStream &in, Painting &painting)
02 {
03 QString title;
04 QString artist;
05 quint32 year;
06 in >> title >> artist >> year;
07 painting = Painting(title, artist, year);
08 return in;
09 }
Обеспечение в пользовательских типах данных операторов ввода—вывода в поток дает несколько преимуществ. Одно из них заключается в том, что это позволяет нам выводить в поток контейнеры с пользовательскими типами. Например:
QList<Painting> paintings = …;
out << paintings;
Мы можем так же просто считывать контейнеры:
QList<Painting> paintings;
in >> paintings;
Это привело бы к ошибке компиляции, если бы тип Painting не поддерживал операции << или >>. Еще одно преимущество обеспечения потоковых операторов в пользовательских типах заключается в возможности хранения этих типов в виде объектов QVariant, что расширяет возможности их применения, например, в объектах QSettings. Это будет работать при условии предварительной регистрации типа с помощью функции qRegisterMetaTypeStreamOperators<T>(), работа которой рассматривается в главе 11.
При использовании QDataStream Qt обеспечивает чтение и запись каждого типа, включая контейнеры с произвольным числом элементов. Это освобождает нас от структурирования того, что мы записываем, и от выполнения какого бы то ни было синтаксического анализа того, что мы считываем. Необходимо лишь гарантировать чтение всех типов в той же последовательности, в какой они были записаны, предоставляя Qt обработку всех деталей.
QDataStream имеет смысл использовать как для своих собственных пользовательских форматов файлов, так и для стандартных двоичных форматов. Мы можем считывать и записывать стандартные форматы двоичных данных, используя потоковые операторы для базовых типов (например, quint16 или float) или при помощи функций readRawBytes() и writeRawBytes(). Если QDataStream используется только для чтения и записи «чистых» типов данных С++, нет необходимости вызывать функцию setVersion().
До сих пор мы загружали и сохраняли данные, жестко задавая в программе версию потока QDataStream::Qt_4_1. Этот подход прост, и он надежно работает, но он имеет один небольшой недостаток: мы не сможем воспользоваться новыми форматами и обновленными версиями форматов. Например, если в более поздней версии Qt добавится новый атрибут к QFont (кроме размера точки, наименования шрифта и так далее) и мы жестко закодируем номер версии Qt_4_1, этот атрибут не будет сохраняться и загружаться. Существует два решения. Первое решение заключается во включении номера версии QDataStream в файл:
QDataStream out(&file);
out << quint32(MagicNumber) << quint16(out.version());
(MagicNumber — это константа, которая уникально идентифицирует тип файла.) В этом случае мы всегда будем записывать данные с применением последней версии QDataStream (каким бы результат ни был). При считывании файла мы считываем номер версии потока:
01 quint32 magic;
02 quint16 streamVersion;
03 QDataStream in(&file);
04 in >> magic >> streamVersion;
05 if (magic != MagicNumber) {
06 cerr << "File is not recognized by this application" << endl;
07 return false;
08 } else if (streamVersion > in.version()) {
09 cerr << "File is from a more recent version of the application"
10 << endl;
11 return false;
12 }
13 in.setVersion(streamVersion);
Мы можем считывать данные, если версия потока меньше или совпадает с версией, используемой в приложении; в противном случае мы выдаем сообщение об ошибке.
Если файл использует формат с собственным номером версии, мы можем его использовать для определения номера версии потока, а не хранить этот номер в явном виде. Предположим, что файл сформирован в формате версии 1.3 нашего приложения. Тогда мы могли бы записать данные следующим образом:
QDataStream out(&file);
out.setVersion(QDataStream::Qt_4_1);
out << quint32(MagicNumber) << quint16(0x0103);
При считывании данных мы определяем версию QDataStream на основе номера версии приложения:
01 QDataStream in(&file);
02 in >> magic >> appVersion;
03 if (magic != MagicNumber) {
04 cerr << "File is not recognized by this application" << endl;
05 return false;
06 } else if (appVersion > 0x0103) {
07 cerr << "File is from a more recent version of the application"
08 << endl;
09 return false;
10 }
11 if (appVersion < 0x0103) {
12 in.setVersion(QDataStream::Qt_3_0);
13 } else {
14 in.setVersion(QDataStream::Qt_4_1);
15 }
В этом примере мы говорим, что для любого файла, сохраненного в приложении с версией меньшей, чем 1.3, используется версия 4 потока данных (Qt_3_0), а для файлов, сохраненных в приложении с версией 1.3, используется версия 7 потока данных (Qt_4_1).
Итак, существует три политики работы с версиями потоков данных QDataStream: жесткое кодирование номера версии, запись и чтение номера версии в явном виде и использование различных жестко закодированных номеров версий в зависимости от версии приложения. Можно применять любую из этих политик для гарантирования чтения данных новой версией приложения, записанных в старой версии, даже если сборка новой версии приложения выполняется с более свежей версией Qt. После выбора политики обработки версий QDataStream чтение и запись двоичных данных в Qt становятся простыми и надежными.
Если мы хотим выполнить чтение или запись за один шаг, мы не должны использовать QDataStream, а вместо этого мы должны вызывать функции write() и readAll() класса QIODevice. Например:
01 bool copyFile(const QString &source, const QString &dest)
02 {
03 QFile sourceFile(source);
04 if (!sourceFile.open(QIODevice::ReadOnly))
05 return false;
06 QFile destFile(dest);
07 if (!destFile.open(QIODevice::WriteOnly))
08 return false;
09 destFile.write(sourceFile.readAll());
10 return sourceFile.error() == QFile::NoError
11 && destFile.error() == QFile::NoError;
12 }
В строке, где вызывается readAll(), все содержимое входного файла считывается в QByteArray, который затем передается функции write() для записи в выходной файл. Хранение всех данных в QByteArray ведет к большему расходу памяти, чем при последовательном чтении элементов, однако это дает некоторые преимущества. Например, мы можем затем использовать функции qCompress() и qUncompress() для упаковки и распаковки данных.
Существуют другие сценарии, когда прямой доступ к QIODevice оказывается более подходящим, чем использование QDataStream. Класс QIODevice имеет функцию peek(), которая возвращает следующие байты данных, перемещая позицию устройства, а также функцию ungetChar(), которая возвращает считанный байт в поток. Эти функции работают как на устройствах произвольного доступа (таких, как файлы), так и на последовательных устройствах (таких, как сетевые сокеты). Имеется также функция seek(), которая используется для установки позиции устройств, поддерживающих произвольный доступ.