
Time for action – implementing a JSON parser
Let's extend the PlayerInfoJSON
class and equip it with a reverse conversion:
PlayerInfo PlayerInfoJSON::readPlayerInfo(const QByteArray &ba) const { QJsonDocument doc = QJsonDocument::fromJson(ba); if(doc.isEmpty() || !doc.isArray()) return PlayerInfo(); return readPlayerInfo(doc.array()); }
First, we read the document and check whether it is valid and holds the expected array. Upon failure, an empty structure is returned; otherwise, readPlayerInfo
is called and is given QJsonArray
to work with:
PlayerInfo PlayerInfoJSON::readPlayerInfo(const QJsonArray &array) const { PlayerInfo pinfo; foreach(QJsonValue value, array) pinfo.players << readPlayer(value.toObject()); return pinfo; }
Since the array is iterable, we can again use foreach
to iterate it and use another method—readPlayer
—to extract all the needed data:
Player PlayerInfoJSON::readPlayer(const QJsonObject &object) const { Player player; player.name = object.value("name").toString(); player.password = object.value("password").toString(); player.experience = object.value("experience").toDouble(); player.hitPoints = object.value("hitpoints").toDouble(); player.location = object.value("location").toString(); QVariantMap positionMap = object.value("position").toVariant().toMap(); player.position = QPoint(positionMap["x"].toInt(), positionMap["y"].toInt()); player.inventory = readInventory(object.value("inventory").toArray()); return player; }
In this function, we used QJsonObject::value()
to extract data from the object and then we used different functions to convert the data to the desired type. Note that in order to convert to QPoint
, we first converted it to QVariantMap
and then extracted the values before using them to build QPoint
. In each case, if the conversion fails, we get a default value for that type (for example, an empty string). To read the inventory, we employ a custom method:
QList<InventoryItem> PlayerInfoJSON::readInventory(const QJsonArray &array) const { QList<InventoryItem> inventory; foreach(QJsonValue value, array) inventory << readItem(value.toObject()); return inventory; }
What remains is to implement readItem()
:
InventoryItem PlayerInfoJSON::readItem(const QJsonObject &object) const { Item item; item.type = (InventoryItem::Type)object.value("type").toDouble(); item.subType = object.value("subtype").toString(); item.durability = object.value("durability").toDouble(); return item; }
What just happened?
The class that was implemented can be used for bidirectional conversion between Item
instances and a QByteArray
object, which contains the object data in the JSON format. We didn't do any error checking here; instead, we relied on automatic type conversion handling in QJsonObject
and QVariant
.
QSettings
While not strictly a serialization issue, the aspect of storing application settings is closely related to the described subject. A Qt solution for this is the QSettings
class. By default, it uses different backends on different platforms, such as system registry on Windows or INI files on Linux. The basic use of QSettings
is very easy—you just need to create the object and use setValue()
and value()
to store and load data from it:
QSettings settings; settings.setValue("windowWidth", 80); settings.setValue("windowTitle", "MySuperbGame"); // … int windowHeight = settings.value("windowHeight").toInt();
The only thing you need to remember is that it operates on QVariant
, so the return value needs to be converted to the proper type if needed as shown in the last line of the preceding code. A call to value()
can take an additional argument that contains the value to be returned if the requested key is not present in the map. This allows you to handle default values, for example, in a situation when the application is first started and the settings are not saved yet:
int windowHeight = settings.value("windowHeight", 800);
The simplest scenario assumes that settings are "flat" in the way that all keys are defined on the same level. However, this does not have to be the case—correlated settings can be put into named groups. To operate on a group, you can use the beginGroup()
and endGroup()
calls:
settings.beginGroup("Server"); QString srvIP = settings.value("host").toString(); int port = settings.value("port").toInt(); settings.endGroup();
When using this syntax, you have to remember to end the group after you are done with it. An alternative to using the two mentioned methods is to pass the group name directly to invocation of value()
:
QString srvIP = settings.value("Server/host").toString(); int port = settings.value("Server/port").toInt();
As was mentioned earlier, QSettings
can use different backends on different platforms; however, we can have some influence on which is chosen and which options are passed to it by passing appropriate options to the constructor of the settings
object. By default, the place where the settings for an application are stored is determined by two values—the organization and the application name. Both are textual values and both can be passed as arguments to the QSettings
constructor or defined a priori using appropriate static methods in QCoreApplication
:
QCoreApplication::setOrganizationName("Packt"); QCoreApplication::setApplicationName("Game Programming using Qt"); QSettings settings;
This code is equivalent to:
QSettings settings("Packt", "Game Programming using Qt");
All of the preceding code use the default backend for the system. However, it is often desirable to use a different backend. This can be done using the Format
argument, where we can pass one of the two options—NativeFormat
or IniFormat
. The former chooses the default backend, while the latter forces the INI-file backend. When choosing the backend, you can also decide whether settings should be saved in a system-wide location or in the user's settings storage by passing one more argument—the scope of which can be either UserScope
or SystemScope
. This can extend our final construction call to:
QSettings settings(QSettings::IniFormat, QSettings::UserScope, "Packt", "Game Programming using Qt");
There is one more option available for total control of where the settings data resides—tell the constructor directly where the data should be located:
QSettings settings( QStandardPaths::writableLocation( QStandardPaths::ConfigLocation ) +"/myapp.conf", QSettings::IniFormat );
Tip
The QStandardPaths
class provides methods to determine standard locations for files depending on the task at hand.
QSettings
also allows you to register your own formats so that you can control the way your settings are stored—for example, by storing them using XML or by adding on-the-fly encryption. This is done using QSettings::registerFormat()
, where you need to pass the file extension and two pointers to functions that perform reading and writing of the settings, respectively, as follows:
bool readCCFile(QIODevice &device, QSettings::SettingsMap &map) { CeasarCipherDevice ccDevice; ccDevice.setBaseDevice(&device); // ... return true; } bool writeCCFile(QIODevice &device, const QSettings::SettingsMap &map) { ... } const QSettings::Format CCFormat = QSettings::registerFormat("ccph", readCCFile, writeCCFile);
Pop quiz – Qt core essentials
Q1. What is the closest equivalent std::string
in Qt?
QString
QByteArray
QStringLiteral
Q2. Which regular expression can be used to validate an IPv4 address, which is an address composed of four dot-separated decimal numbers with values ranging from 0 to 255?
Q3. Which do you think is the best serialization mechanism to use if you expect the data structure to evolve (gain new information) in future versions of the software?
- JSON
- XML
- QDataStream