Loading and Saving
We will now implement the loading and saving of Spreadsheet files using a custom binary format. We will do this using QFile and QDataStream, which together provide platform-independent binary I/O.
We will start with writing a Spreadsheet file:
bool Spreadsheet::writeFile(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::warning(this, tr("Spreadsheet"), tr("Cannot write file %1:\n%2.") .arg(file.fileName()) .arg(file.errorString())); return false; } QDataStream out(&file); out.setVersion(QDataStream::Qt_4_3); out << quint32(MagicNumber); QApplication::setOverrideCursor(Qt::WaitCursor); for (int row = 0; row < RowCount; ++row) { for (int column = 0; column < ColumnCount; ++column) { QString str = formula(row, column); if (!str.isEmpty()) out << quint16(row) << quint16(column) << str; } } QApplication::restoreOverrideCursor(); return true; }
The writeFile() function is called from MainWindow::saveFile() to write the file to disk. It returns true on success, false on error.
We create a QFile object with the given file name and call open() to open the file for writing. We also create a QDataStream object that operates on the QFile and use it to write out the data.
Just before we write the data, we change the application's cursor to the standard wait cursor (usually an hourglass) and restore the normal cursor once all the data is written. At the end of the function, the file is automatically closed by QFile's destructor.
QDataStream supports basic C++ types as well as many of Qt's types. The syntax is modeled after the Standard C++ <iostream> classes. For example,
out << x << y << z;
writes the variables x, y, and z to a stream, and
in >> x >> y >> z;
reads them from a stream. Because the C++ primitive integer types may have different sizes on different platforms, it is safest to cast these values to one of qint8, quint8, qint16, quint16, qint32, quint32, qint64, and quint64, which are guaranteed to be of the size they advertise (in bits).
The Spreadsheet application's file format is fairly simple. A Spreadsheet file starts with a 32-bit number that identifies the file format (MagicNumber, defined as 0x7F51C883 in spreadsheet.h, an arbitrary random number). Then comes a series of blocks, each containing a single cell's row, column, and formula. To save space, we don't write out empty cells. The format is shown in Figure 4.3.
Figure 4.3 The Spreadsheet file format
The precise binary representation of the data types is determined by QDataStream. For example, a quint16 is stored as two bytes in big-endian order, and a QString as the string's length followed by the Unicode characters.
The binary representation of Qt types has evolved quite a lot since Qt 1.0. It is likely to continue evolving in future Qt releases to keep pace with the evolution of existing types and to allow for new Qt types. By default, QDataStream uses the most recent version of the binary format (version 9 in Qt 4.3), but it can be set to read older versions. To avoid any compatibility problems if the application is recompiled later using a newer Qt release, we explicitly tell QDataStream to use version 9 irrespective of the version of Qt we are compiling against. (QDataStream::Qt_4_3 is a convenience constant that equals 9.)
QDataStream is very versatile. It can be used on a QFile, and also on a QBuffer, a QProcess, a QTcpSocket, a QUdpSocket, or a QSslSocket. Qt also offers a QTextStream class that can be used instead of QDataStream for reading and writing text files. Chapter 12 explains these classes in depth, and also describes various approaches to handling different QDataStream versions.
bool Spreadsheet::readFile(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, tr("Spreadsheet"), tr("Cannot read file %1:\n%2.") .arg(file.fileName()) .arg(file.errorString())); return false; } QDataStream in(&file); in.setVersion(QDataStream::Qt_4_3); quint32 magic; in >> magic; if (magic != MagicNumber) { QMessageBox::warning(this, tr("Spreadsheet"), tr("The file is not a Spreadsheet file.")); return false; } clear(); quint16 row; quint16 column; QString str; QApplication::setOverrideCursor(Qt::WaitCursor); while (!in.atEnd()) { in >> row >> column >> str; setFormula(row, column, str); } QApplication::restoreOverrideCursor(); return true; }
The readFile() function is very similar to writeFile(). We use QFile to read in the file, but this time using the QIODevice::ReadOnly flag rather than QIODevice::WriteOnly. Then we set the QDataStream version to 9. The format for reading must always be the same as for writing.
If the file has the correct magic number at the beginning, we call clear() to blank out all the cells in the spreadsheet, and we read in the cell data. Since the file only contains the data for non-empty cells, and it is very unlikely that every cell in the spreadsheet will be set, we must ensure that all cells are cleared before reading.