Reading and Writing Binary Data
Reading and writing binary data using QDataStream is the simplest way to load and save custom data with Qt. QDataStream supports many Qt data types, including QByteArray, QFont, QImage, QMap, QPixmap, QString, QValueList, and QVariant. The data types that QDataStream understands and the formats it uses to store them are described online at http://doc.trolltech.com/3.2/ datastreamformat.html.
To show how to handle binary data, we will use two example classes: Drawing and Gallery. The Drawing class holds some basic information about a drawing (the artist's name, the title, and the year it was created), and the Gallery class holds a list of Drawings.
We will start with the Gallery class.
class Gallery : public QObject { public: bool loadBinary(const QString &fileName); bool saveBinary(const QString &fileName); ... private: enum { MagicNumber = 0x98c58f26 }; void writeToStream(QDataStream &out); void readFromStream(QDataStream &in); void error(const QFile &file, const QString &message); void ioError(const QFile &file, const QString &message); QByteArray getData(); void setData(const QByteArray &data); QString toString(); std::list drawings; };
The Gallery class contains public functions to save and load its data. The data is a list of drawings held in the drawings data member. The private functions will be reviewed as we make use of them.
Here is a simple function for saving a Gallery' drawings as binary data:
bool Gallery::saveBinary(const QString &fileName) { QFile file(fileName); if (!file.open(IO_WriteOnly)) { ioError(file, tr("Cannot open file %1 for writing")); return false; } QDataStream out(&file); out.setVersion(5); out << (Q_UINT32)MagicNumber; writeToStream(out); if (file.status() != IO_Ok) { ioError(file, tr("Error writing to file %1")); return false; } return true; }
We open a file and make the file the target of a QDataStream. We set the QDataStream's version to 5 (the most recent version in Qt 3.2). The version number influences the way Qt data types are represented. Basic C++ data types are always represented the same way.
We then output a number that identifies the Gallery file format (MagicNumber). To ensure that the number is written as a 32-bit integer on all platforms, we cast it to Q_UINT32, a data type that is guaranteed to be exactly 32 bits.
The file body is written by the writeToStream() private function. We don't need to explicitly close the file; this is done automatically when the QFile variable goes out of scope at the end of the function.
After the call to writeToStream(), we check the status of the QFile device. If there was an error, we call ioError() to present a message box to the user and return false.
void Gallery::ioError(const QFile &file, const QString &message) { error(file, message + ": " + file.errorString()); }
The ioError() function relies on the more general error() function:
void Gallery::error(const QFile &file, const QString &message) { QMessageBox::warning(0, tr("Gallery"), message.arg(file.name())); }
Now let's review the writeToStream() function:
void Gallery::writeToStream(QDataStream &out) { list::const_iterator it = drawings.begin(); while (it != drawings.end()) { out << *it; ++it; } }
The writeToStream() function iterates over all of the Gallery's drawings and outputs them to the stream it has been given, relying on the Drawing class's << operator. If we had used a QValueList to store the drawings instead of a list, we could have omitted the loop and simply written
out << drawings;
When a QValueList is streamed, each item stored in the list is output using the item type's << operator.
QDataStream &operator<<(QDataStream &out, const Drawing &drawing) { out << drawing.myTitle << drawing.myArtist << drawing.myYear; return out; }
To output a Drawing, we simply output its three private member variables: myTitle, myArtist, and myYear. We need to declare operator<<() as a friend of Drawing for this to work. At the end of the function, we return the stream. This is a common C++ idiom that allows us to use a chain of << operators with an output stream. For example:
out << drawing1 << drawing2 << drawing3;
The definition of the Drawing class follows.
class Drawing { friend QDataStream &operator<<(QDataStream &, const Drawing &); friend QDataStream &operator>>(QDataStream &, Drawing &); public: Drawing() { myYear = 0; } Drawing(const QString &title, const QString &artist, int year) { myTitle = title; myArtist = artist; myYear = year; } QString title() const { return myTitle; } void setTitle(const QString &title) { myTitle = title; } QString artist() const { return myArtist; } void setArtist(const QString &artist) { myArtist = artist; } int year() const { return myYear; } void setYear(int year) { myYear = year; } private: QString myTitle; QString myArtist; int myYear; };
Now let's see how to read the data from a Gallery file:
bool Gallery::loadBinary(const QString &fileName) { QFile file(fileName); if (!file.open(IO_ReadOnly)) { ioError(file, tr("Cannot open file %1 for reading")); return false; } QDataStream in (&file); in.setVersion(5); Q_UINT32 magic; in >> magic; if (magic != MagicNumber) { error(file, tr("File %1 is not a Gallery file")); return false; } readFromStream(in); if (file.status() != IO_Ok) { ioError(file, tr("Error reading from file %1")); return false; } return true; }
We open the file for reading and create a QDataStream to extract the data from the file. We set the QDataStream's version to 5, because that's the version we used for writing. By using a fixed version number of 5, we guarantee that the application can always read and write the data, providing it is compiled with Qt 3.2 or later.
We start by reading back the magic number we wrote and compare it against MagicNumber. This ensures that we are really reading a Gallery file. We then read the data itself using the readFromStream() function.
void Gallery::readFromStream(QDataStream &in) { drawings.clear(); while (!in.atEnd()) { Drawing drawing; in >> drawing; drawings.push_back(drawing); } }
In readFromStream(), we start by clearing any existing data. We then read in one drawing at a time, relying on the >> operator, and append each one to the Gallery's list of drawings. If we were using a QValueList to store the data instead of a list, we could read in all the drawings without looping:
in >> drawings;
QValueList relies on the item type's >> operator to read in the items.
QDataStream &operator>>(QDataStream &in, Drawing &drawing) { in >> drawing.myTitle >> drawing.myArtist >> drawing.myYear; return in; }
The implementation of the >> operator mirrors that of the << operator. When we use QDataStream, we don't need to perform any kind of parsing.
If we want to read and write some raw binary data, we can use readRawBytes() and writeRawBytes() to read and write a block of bytes through a QDataStream. The raw bytes are not preceded by a block size.
We can read and write standard binary formats, such as DBF files and TEX DVI files, using the >> and << operators on basic types (like Q_UINT16 or float) or with readRawBytes() and writeRawBytes(). The default byte ordering used by QDataStream is big-endian. If we want to read and write data as little-endian, we must call
stream.setByteOrder(QDataStream::LittleEndian);
If the QDataStream is being used purely to read and write basic C++ data types, there is no need to use setVersion().
If we want to read or write a file in one go, we can avoid using QDataStream altogether and instead use QFile's writeBlock() and readAll() functions. For example:
file.writeBlock(getData());
Data written in this way is just a sequence of bytes. We are responsible for structuring the data when we write it and for parsing it when we read it back. We rely on Gallery's private getData() function to create the QByteArray and populate it with data. Reading it back is just as easy:
setData(file.readAll());
We use Gallery's setData() function to extract the information out of the QByteArray.
Having all the data in a QByteArray requires more memory, but it offers some advantages. For example, we can then use Qt's qCompress() function to compress the data (using zlib):
file.writeBlock(qCompress(getData()));
We can then use qUncompress() to uncompress the data:
setData(qUncompress(file.readAll()));
One way to implement getData() and setData() is to use a QDataStream on a QByteArray. Here's getData():
QByteArray Gallery::getData() { QByteArray data; QDataStream out(data, IO_WriteOnly); writeToStream(out); return data; }
We create a QDataStream that writes to a QByteArray rather than to a QFile, and we use the writeToStream() function we wrote earlier to fill the array with binary data.
Similarly, the setData() function can use the readFromStream() function we wrote earlier:
void Gallery::setData(const QByteArray &data) { QDataStream in(data, IO_ReadOnly); readFromStream(in); }
In the earlier examples, we loaded and saved the data with the stream's version hard-coded to 5. This approach is simple and safe, but it does have one small drawback: We cannot take advantage of new or updated formats. For example, if a later version of Qt added a new component to QFont (in addition to its point size, family, etc.), that component would not be saved or loaded.
One solution is to embed the QDataStream version number in the file:
QDataStream out(&file); out << (Q_UINT32)MagicNumber; out << (Q_UINT16)out.version(); writeToStream(out);
This ensures that we always write the data using the most recent version of QDataStream, whatever that happens to be.
When we come to read the file, we read the magic number and the stream version:
QDataStream in(&file); Q_UINT32 magic; Q_UINT16 streamVersion; in >> magic >> streamVersion; if (magic != MagicNumber) { error(file, tr("File %1 is not a Gallery file")); return false; } else if ((int)streamVersion > in.version()) { error(file, tr("File %1 is from a more recent version of the " "application")); return false; } in.setVersion(streamVersion); readFromStream(in);
We can read the data as long as the stream version is less than or equal to the version used by the application. Otherwise, we report an error.
If the file format contains a version number of its own, we can use that instead of the stream version number. For example, let's suppose that the file format is for version 1.3 of our application. We might then write the data as follows:
QDataStream out(&file); out.setVersion(5); out << (Q_UINT32)MagicNumber; out << (Q_UINT16)0x0103; writeToStream(out);
When we read it back, we determine which QDataStream version to use based on the application's version number:
QDataStream in(&file); Q_UINT32 magic; Q_UINT16 appVersion; in >> magic >> appVersion; if (magic != MagicNumber) { error(file, tr("File %1 is not a Gallery file")); return false; } else if (appVersion > 0x0103) { error(file, tr("File %1 is from a more recent version of the " "application")); return false; } if (appVersion <= 0x0102) { in.setVersion(4); } else { in.setVersion(5); } readFromStream(in);
In this example, we say that any file saved with version 1.2 or earlier of the application uses data stream version 4, and that files saved with version 1.3 of the application use data stream version 5.
Once we have a policy for handling QDataStream versions, reading and writing binary data using Qt is simple and reliable.