Game Programming Using Qt Beginner's Guide
上QQ阅读APP看书,第一时间看更新

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?

  1. QString
  2. QByteArray
  3. 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?

  1. JSON
  2. XML
  3. QDataStream