Writing and Reading Using QData Stream

The QDataStream class can read and write Python Boolean and numeric types, and PyQt types, including images, in binary format. Files written by QData-

Approach-es to File Error Handling sidebar

Stream are platform-independent; the class automatically takes care of endian-ness and word size.

Almost every new version of PyQt has a QDataStream that uses a new binary format for data storage—this is done so that QDataStream can accommodate new data types, and to support enhancements to existing data types. This is not a problem, because every version of QDataStream can read data stored in the formats used by all its previous versions. In addition, QDataStream always stores integers the same way, no matter which version of QDataStream is being used.

def saveQDataStream(self): error = None fh = None try:

if not fh.open(QIODevice.WriteOnly):

raise IOError, unicode(fh.errorString()) stream = QDataStream(fh)

stream.writeInt32(MovieContainer.MAGIC_NUMBER) stream.writeInt32(MovieContainer.FILE_VERSION) stream.setVersion(QDataStream.Qt_4_2)

Since PyQt uses return values rather than exceptions, if the file cannot be opened we raise an exception ourselves since we prefer the exception-based approach to error handling. Having opened the file, we create a QDataStream object to write to it.

PyQt cannot guess what size integer we want to use to store int and long integers, so we must write integer values using the writeIntn() and writeUIntn() methods, where n is 8, 16, 32, or 64, that is, the number of bits to use to store the integer. For floating-point numbers, QDataStream provides the writeDouble() and readDouble() methods. These operate on Python floats (equivalent to C and C++ doubles), and are stored as 64-bit values in IEEE-754 format.

The first integer we write is the "magic number". This is an arbitrary number that we use to identify My Movies data files. This number will never change. We should give any custom binary data file a unique magic number, since filename extensions cannot always be relied upon to correctly identify a file's type. Next we write a "file version". This is the version of our file format (we have set it to be 100). If we decide to change the file format later, the magic number will remain the same—after all, the file will still hold movie data—but the file format will change (e.g., to 101) so that we can execute different code to load it to account for the difference in format.

Since integers are always saved in the same format, we can safely write them before setting the QDataStream version. But once we have written the magic number and file version, we should set the QDataStream version to the one that PyQt should use for writing and reading the rest of the data. If we want to take advantage of a later version we could use our original file format for version Qt_4_2, and another file format for the later version. Then, when we come to load the data, we could set the QDataStream version depending on our file format number.

Setting the QDataStream version is very important, since it will ensure that any PyQt data type is saved and loaded correctly. The only situation where it does not matter is if we are only saving and loading integers, since their representation never changes.

for key, movie in self._movies:

stream << movie.title stream.writeInt16(movie.year) stream.writeInt16(movie.minutes) stream << movie.acquired << movie.notes

Now we iterate over the movie data, writing each movie's data to the data stream. The data's format is illustrated in Figure 8.3. The QDataStream class overloads the << operator for many PyQt classes, including, for example, QString, QDate, and QImage, so we must use a C++-like streaming syntax to write our data. The << operator writes its right operand to the data stream that is its left operand. It can be applied repeatedly to the same stream, since it returns the stream it is applied to, but for integers, we must use the writeIntn() and writeUIntn() methods.

Figure 8.3 The QDataStream My Movies file format

Since we are writing binary data, we do not have to do any formatting. We just have to ensure that when we load the data back, we use the same QDataStream version, and that we load in the same data types in the same order as we saved. So, in this case, we will load back two integers (the magic and file version numbers), and then any number of movie records, each comprising a string, two integers, a date, and a string.

except (IOError, OSError), e:

error = "Failed to save: %s" % e finally:

if fh is not None:

fh.close() if error is not None:

return False, error self._dirty = False return True, "Saved %d movie records to %s" % (

len(self._movies),

QFileInfo(self._fname).fileName())

If there are any errors, we simply give up and return a failure flag and an error message. Otherwise, we clear the dirty flag and return a success flag and a message indicating how many records were saved.

The corresponding load method is just as straightforward, although it does have to do more error handling.

def loadQDataStream(self): error = None fh = None try:

if not fh.open(QIODevice.ReadOnly):

raise IOError, unicode(fh.errorString()) stream = QDataStream(fh) magic = stream.readInt32() if magic != MovieContainer.MAGIC_NUMBER:

raise IOError, "unrecognized file type" version = stream.readInt32() if version < MovieContainer.FILE_VERSION:

raise IOError, "old and unreadable file format" elif version > MovieContainer.FILE_VERSION:

raise IOError, "new and unreadable file format" stream.setVersion(QDataStream.Qt_4_2) self.clear(False)

We create the QFile object and QDataStream object the same as before, except this time using ReadOnly rather than WriteOnly mode. Then we read in the magic number. If this is not the unique My Movies data file number, we raise an exception. Next we read the file version, and make sure it is one that we can handle. At this point, we would branch depending on the file version, if we had more than one version of this file format in use. Then we set the QDataStream version.

The next step is to clear the movies data structures. We do this as late as possible so that if an exception was raised earlier, the original data will be left intact. The False argument tells the clear() method to clear_movies and

_movieFromId, but not the filename.

while not stream.atEnd(): title = QString() acquired = QDate() notes = QString() stream >> title year = stream.readInt16()

Approaches to File Error Handling

The approach used for handling file errors in this chapter has the structure

shown here on the left. Another equally valid approach, used, for example,

in chap09/textedit.py and chap14/ships.py, is shown here on the right.

error = None

exception = None

fh = None

fh = None

try:

try:

# open file and read data

# open file and read data

except (IOError, OSError), e:

except (IOError, OSError), e:

error = unicode(e)

exception = e

finally:

finally:

if fh is not None:

if fh is not None:

fh.close()

fh.close()

if error is not None:

if exception is not None:

return False, error

raise exception

return True, "Success"

At the call point, and assuming we are dealing with a load() method, we

might use code like this for the left-hand approach:

ok, msg = load(args)

if not ok:

QMessageBox.warning(self, "File Error", msg)

And for the right-hand approach we could use code like this:

try:

load(args)

except (IOError, OSError), e:

QMessageBox.warning(self, "File Error", unicode(e))

Another approach, used in chap09/sditexteditor.pyw and chap12/pagedesign-

er.pyw, is to do all the error handling inside the file-handling method itself:

fh = None

try:

# open file and read data

except (IOError, OSError), e:

QMessageBox.warning(self, "File Error", unicode(e))

finally:

if fh is not None:

fh.close()

At the call point we simply call load(args), leaving the load() method itself

to report any problems to the user.

Table 8.1 Selected QDataStream Methods

Description

Returns True if the end of QDataStream s has been reached

Sets QDataStream s's version to v, where v is one of Qt_1_0, Qt_2_0, ..., Qt_4_2, or Qt_4_3

Writes object x to QDataStream s; x can be of type QBrush, QColor, QDate, QDateTime, QFont, Qlcon, Qlmage, QMatrix, QPainterPath, QPen, QPixmap, QSize, QString, QVariant, etc. Reads a bool from QDataStream s Reads a float from QDataStream s

Reads a 16-bit int from QDataStream s. There is also a readUInt16() method.

Reads a 32-bit int from QDataStream s. There is also a readUInt32() method.

Reads a 64-bit long from QDataStream s. There is also a readUInt64() method.

Reads object x from QDataStream s; x must already exist (so that the data stream knows what data type to read), and can be any of the types writable by << Writes bool b to QDataStream s Writes float f to QDataStream s

Writes int i as a 16-bit int to QDataStream s. There is also a writeUInt16() method.

Writes int i as a 32-bit int to QDataStream s. There is also a writeUInt32() method.

Writes long l as a 64-bit int to QDataStream s. There is also a writeUInt64() method.

minutes = stream.readInt16() stream >> acquired >> notes self.add(Movie(title, year, minutes, acquired, notes))

We could have stored the number of movies at the beginning of the file, after the file version. But instead we simply iterate over the data stream until we reach the end. For non-numeric data types we must create variables that hold empty values of the correct type. Then we use the >> operator, which takes a data stream as its left operand and a variable as its right operand; it reads a value of the right operand's type from the stream and puts it into the right operand. The operator returns the file stream, so it can be applied repeatedly.

Syntax s.atEnd()

s.readDouble()

s.readInt16()

s.readInt32()

s.readInt64()

s.write-

Double(f)

s.writeInt16(i)

s.writeInt32(i)

s.writeInt64(l)

For integers we must always read using the readIntn() and readUIntn() methods with the same number of bits as we specified when writing.

Once we have read in a single movie's data, we create a new Movie object and immediately add it to the container's data structures using the add() method we reviewed in the preceding section.

except (IOError, OSError), e:

error = "Failed to load: %s" % e finally:

if fh is not None:

fh.close() if error is not None:

return False, error self._dirty = False return True, "Loaded %d movie records from %s" % (

len(self._movies),

QFileInfo(self._fname).fileName())

The error handling and the final return statement are structurally the same as we used for the save method.

Using the PyQt QDataStream class to write binary data is not very different in principle from using Python's file class. We must be careful to use the correct QDataStream version, and we ought to use a magic number and file version, or some equivalent approach. The use of the << and >> operators is not very Pythonic, but it is easy to understand.

We could have put code for writing a movie in the Movie class itself, perhaps with a method that took a QDataStream argument and wrote the movie's data to it. In practice it is usually more convenient, and almost always more flexible, to have the data container do the file handling rather than the individual data items.

Was this article helpful?

+2 -3
Tuberminator

Tuberminator

The main focus of this report is to show how to get involved in video marketing on the run, how to rank quickly on YouTube and Google using FREE semi-automatic tools and services. QUICKLY AND FREE. I will show methods and techniques I use to rank my videos, as well as free resources and tools to make video clips, to get backlinks and free traffic.

Get My Free Ebook


Responses

  • Cecilia
    How to use qdatastream?
    8 years ago

Post a comment