
Time for action – implementing a device to encrypt data
Let's implement a really simple device that encrypts or decrypts the data that is streamed through it using a very simple algorithm—the Caesar cipher. What it does is that when encrypting, it shifts each character in the plaintext by a number of characters defined by the key and does the reverse when decrypting. Thus, if the key is 2
and the plaintext character is a
, the ciphertext becomes c
. Decrypting z
with the key 4
will yield the value v
.
We will start by creating a new empty project and adding a class derived from QIODevice
. The basic interface of the class is going to accept an integer key and set an underlying device that serves as the source or destination of data. This is all simple coding that you should already understand, so it shouldn't need any extra explanation, as shown:
class CaesarCipherDevice : public QIODevice { Q_OBJECT Q_PROPERTY(int key READ key WRITE setKey) public: explicit CaesarCipherDevice(QObject *parent = 0) : QIODevice(parent) { m_key = 0; m_device = 0; } void setBaseDevice(QIODevice *dev) { m_device = dev; } QIODevice *baseDevice() const { return m_device; } void setKey(int k) { m_key = k; } inline int key() const { return m_key; } private: int m_key; QIODevice *m_device; };
The next thing is to make sure that the device cannot be used if there is no device to operate on (that is, when m_device == 0
). For this, we have to reimplement the QIODevice::open()
method and return false
when we want to prevent operating on our device:
bool open(OpenMode mode) { if(!baseDevice()) return false; if(baseDevice()->openMode() != mode) return false; return QIODevice::open(mode); }
The method accepts the mode that the user wants to open the device with. We perform an additional check to verify that the base device was opened in the same mode before calling the base class implementation that will mark the device as open.
To have a fully functional device, we still need to implement the two protected pure virtual methods, which do the actual reading and writing. These methods are called by Qt from other methods of the class when needed. Let's start with writeData()
, which accepts a pointer to a buffer containing the data and size of that a buffer:
qint64 CaesarCipherDevice::writeData(const char *data, qint64 len) { QByteArray ba(data, len); for(int i=0;i<len;++i) ba.data()[i] += m_key; int written = m_device->write(ba); emit bytesWritten(written); return written; }
First, we copy the data into a local byte array. Then, we iterate the array, adding to each byte the value of the key (which effectively performs the encryption). Finally, we try to write the byte array to the underlying device. Before informing the caller about the amount of data that was really written, we emit a signal that carries the same information.
The last method that we need to implement is the one that performs decryption by reading from the base device and adding the key to each cell of the data. This is done by implementing readData()
, which accepts a pointer to the buffer that the method needs to write to and the size of the buffer. The code is quite similar to that of writeData()
except that we are subtracting the key value instead of adding it:
qint64 CaesarCipherDevice::readData(char *data, qint64 maxlen) { QByteArray baseData = m_device->read(maxlen); const int s = baseData.size(); for(int i=0;i<s;++i) data[i] = baseData[i]-m_key; return s; }
First, we read from the underlying device as much as we can fit into the buffer and store the data in a byte array. Then, we iterate the array and set subsequent bytes of data buffer to the decrypted value. Finally, we return the amount of data that was really read.
A simple main()
function that can test the class looks as follows:
int main(int argc, char **argv) { QByteArray ba = "plaintext"; QBuffer buf; buf.open(QIODevice::WriteOnly); CaesarCipherDevice encrypt; encrypt.setKey(3); encrypt.setBaseDevice(&buf); encrypt.open(buf.openMode()); encrypt.write(ba); qDebug() << buf.data(); CaesarCipherDevice decrypt; decrypt.setKey(3); decrypt.setBaseDevice(&buf); buf.open(QIODevice::ReadOnly); decrypt.open(buf.openMode()); qDebug() << decrypt.readAll(); return 0; }
We use the QBuffer
class that implements the QIODevice
API and acts as an adapter for QByteArray
or QString
.
What just happened?
We created an encryption object and set its key to 3
. We also told it to use a QBuffer
instance to store the processed content. After opening it for writing, we sent some data to it that gets encrypted and written to the base device. Then, we created a similar device, passing the same buffer again as the base device, but now, we open the device for reading. This means that the base device contains ciphertext. After this, we read all data from the device, which results in reading data from the buffer, decrypting it, and returning the data so that it can be written to the debug console.
Have a go hero – a GUI for the Caesar cipher
You can combine what you already know by implementing a full-blown GUI application that is able to encrypt or decrypt files using the Caesar cipher QIODevice
class that we just implemented. Remember that QFile
is also QIODevice
, so you can pass its pointer directly to setBaseDevice()
.
This is just a starting point for you. The QIODevice
API is quite rich and contains numerous methods that are virtual, so you can reimplement them in subclasses.
Text streams
Much of the data produced by computers nowadays is based on text. You can create such files using a mechanism that you already know—opening QFile
to write, converting all data into strings using QString::arg()
, optionally encoding strings using QTextCodec
, and dumping the resulting bytes to the file by calling write
. However, Qt provides a nice mechanism that does most of this automatically for you in a way similar to how the standard C++ iostream
classes work. The QTextStream
class operates on any QIODevice
API in a stream-oriented way. You can send tokens to the stream using the <<
operator, where they get converted into strings, separated by spaces, encoded using a codec of your choice, and written to the underlying device. It also works the other way round; using the >>
operator, you can stream data from a text file, transparently converting it from strings to appropriate variable types. If the conversion fails, you can discover it by inspecting the result of the status()
method—if you get ReadPastEnd
or ReadCorruptData
, then this means that the read has failed.
Tip
While QIODevice
is the main class that QTextStream
operates on, it can also manipulate QString
or QByteArray
, which makes it useful for us to compose or parse strings.
Using QTextStream
is simple—you just have to pass it the device that you want it to operate on and you're good to go. The stream accepts strings and numerical values:
QFile file("output.txt"); file.open(QFile::WriteOnly|QFile::Text); QTextStream stream(&file); stream << "Today is " << QDate::currentDate().toString() << endl; QTime t = QTime::currentTime(); stream << "Current time is " << t.hour() << " h and " << t.minute() << "m." << endl;
Apart from directing content into the stream, the stream can accept a number of manipulators, such as endl
, which have a direct or indirect influence on how the stream behaves. For instance, you can tell the stream to display a number as decimal and another as hexadecimal with uppercase digits using the following code (highlighted in the code are all manipulators):
for(int i=0;i<10;++i) { int num = qrand() % 100000; // random number between 0 and 99999 stream << dec << num << showbase << hex << uppercasedigits << num << endl; }
This is not the end of the capabilities of QTextStream
. It also allows us to display data in a tabular manner by defining column widths and alignments. Suppose that you have a set of records for game players that is defined by the following structure:
struct Player { QString name; qint64 experience; QPoint position; char direction; }; QList<Player> players;
Let's dump such info into a file in a tabular manner:
QFile file("players.txt"); file.open(QFile::WriteOnly|QFile::Text); QTextStream stream(&file); stream << center; stream << qSetFieldWidth(16) << "Player" << qSetFieldWidth(0) << " "; stream << qSetFieldWidth(10) << "Experience" << qSetFieldWidth(0) << " "; stream << qSetFieldWidth(13) << "Position" << qSetFieldWidth(0) << " "; stream << "Direction" << endl; for(int i=0;i<players.size();++i) { const Player &p = players.at(i); stream << left << qSetFieldWidth(16) << p.name << qSetFieldWidth(0) << " "; stream << right << qSetFieldWidth(10) << p.experience << qSetFieldWidth(0) << " "; stream << right << qSetFieldWidth(6) << p.position.x() << qSetFieldWidth(0) << " " << qSetFieldWidth(6) << p.position.y() << qSetFieldWidth(0) << " "; stream << center << qSetFieldWidth(10); switch(p.direction) { case 'n' : stream << "north"; break; case 's' : stream << "south"; break; case 'e' : stream << "east"; break; case 'w' : stream << "west"; break; default: stream << "unknown"; break; } stream << qSetFieldWidth(0) << endl; }
After running the program, you should get a result similar to the one shown in the following screenshot:

One last thing about QTextStream
is that it can operate on standard C file structures, which makes it possible for us to use QTextStream
to, for example, write to stdout
or read from stdin
, as shown in the following code:
QTextStream qout(stdout); qout << "This text goes to process standard output." << endl;
Data serialization
More than often, we have to store object data in a device-independent way so that it can be restored later, possibly on a different machine with a different data layout and so on. In computer science, this is called serialization. Qt provides several serialization mechanisms and now we will have a brief look at some of them.
Binary streams
If you look at QTextStream
from a distance, you will notice that what it really does is serialize and deserialize data to a text format. Its close cousin is the QDataStream
class that handles serialization and deserialization of arbitrary data to a binary format. It uses a custom data format to store and retrieve data from QIODevice
in a platform-independent way. It stores enough data so that a stream written on one platform can be successfully read on a different platform.
QDataStream
is used in a similar fashion as QTextStream
—the operators <<
and >>
are used to redirect data into or out of the stream. The class supports most of the built-in Qt types so that you can operate on classes such as QColor
, QPoint
, or QStringList
directly:
QFile file("outfile.dat"); file.open(QFile::WriteOnly|QFile::Truncate); QDataStream stream(&file); double dbl = 3.14159265359; QColor color = Qt::red; QPoint point(10, -4); QStringList stringList = QStringList() << "foo" << "bar"; stream << dbl << color << point << stringList;
If you want to serialize custom data types, you can teach QDataStream
to do that by implementing proper redirection operators.