End to End GUI Development with Qt5
上QQ阅读APP看书,第一时间看更新

DataDecorators

A simple implementation of the name property of our client model would be to add it as a QString; however, this approach has some shortcomings. Whenever we display this property in the UI, we will probably want to display an informative label next to the textbox so that the user knows what it is for, saying “Name” or something similar. Whenever we want to validate a name entered by the user, we have to manage that in the code somewhere else. Finally, if we want to serialize the value to or from JSON, again there needs to be some other component that does it for us.

To solve all of these problems we will introduce the concept of a DataDecorator, which will lift a given base data type and give us a label, validation capabilities, and JSON serialization out of the box. Our models will maintain a collection of DataDecorators, allowing them to validate and serialize themselves to JSON too by simply walking through the data items and performing the relevant action.

In our cm-lib project, create the following classes in a new folder cm-lib/source/data:

Our DataDecorator base class will house the features shared across all of our data items.

data-decorator.h:

#ifndef DATADECORATOR_H
#define DATADECORATOR_H

#include <QJsonObject>
#include <QJsonValue> #include <QObject> #include <QScopedPointer>
#include <cm-lib_global.h>
namespace cm { namespace data {
class Entity;
class CMLIBSHARED_EXPORT DataDecorator : public QObject { Q_OBJECT Q_PROPERTY( QString ui_label READ label CONSTANT )
public: DataDecorator(Entity* parent = nullptr, const QString& key =
"SomeItemKey", const QString& label = "");
virtual ~DataDecorator();
const QString& key() const; const QString& label() const; Entity* parentEntity();
virtual QJsonValue jsonValue() const = 0; virtual void update(const QJsonObject& jsonObject) = 0;
private: class Implementation; QScopedPointer<Implementation> implementation; };
}} #endif

We inherit from QObject, add our dllexport macro and wrap the whole thing in namespaces as usual. Also, because this is an abstract base class, we ensure that we’ve implemented a virtual destructor.

We know that because we are inheriting from QObject, we want to receive a pointer to a parent in our constructor. We also know that all data items will be children of an Entity (which we will write soon and have forward declared here), which will itself be derived from QObject. We can leverage these two facts to parent our DataDecorator directly to an Entity.

We construct the decorator with a couple of strings. All of our data decorators must have a key that will be used when serializing to and from JSON, and they will also share a label property that the UI can use to display descriptive text next to the data control. We tuck these members away in the private implementation and implement some accessor methods for them.

Finally, we begin implementing our JSON serialization by declaring virtual methods to represent the value as a QJsonValue and to update the value from a provided QJsonObject. As the value is not known in the base class and will instead be implemented in the derived classes, both these methods are pure virtual functions.

data-decorator.cpp:

#include "data-decorator.h"

namespace cm {
namespace data {
class DataDecorator::Implementation { public: Implementation(Entity* _parent, const QString& _key, const QString&
_label)
: parentEntity(_parent) , key(_key) , label(_label) { } Entity* parentEntity{nullptr}; QString key; QString label; };
DataDecorator::DataDecorator(Entity* parent, const QString& key, const QString& label) : QObject((QObject*)parent) { implementation.reset(new Implementation(parent, key, label)); }
DataDecorator::~DataDecorator() { }
const QString& DataDecorator::key() const { return implementation->key; }
const QString& DataDecorator::label() const { return implementation->label; }
Entity* DataDecorator::parentEntity() { return implementation->parentEntity; }
}}

The implementation is very straightforward, essentially just managing some data members.

Next, we'll implement our derived decorator class for handling strings.

string-decorator.h:

#ifndef STRINGDECORATOR_H
#define STRINGDECORATOR_H

#include <QJsonObject>
#include <QJsonValue>
#include <QObject>
#include <QScopedPointer>
#include <QString>
#include <cm-lib_global.h> #include <data/data-decorator.h>
namespace cm { namespace data {
class CMLIBSHARED_EXPORT StringDecorator : public DataDecorator { Q_OBJECT Q_PROPERTY( QString ui_value READ value WRITE setValue NOTIFY
valueChanged )
public: StringDecorator(Entity* parentEntity = nullptr, const QString& key = "SomeItemKey", const QString& label = "", const QString& value = ""); ~StringDecorator();
StringDecorator& setValue(const QString& value); const QString& value() const;
QJsonValue jsonValue() const override; void update(const QJsonObject& jsonObject) override;
signals: void valueChanged();
private: class Implementation; QScopedPointer<Implementation> implementation; };
}}
#endif

There isn’t much else going on here—we’re just adding a strongly typed QString value property to hold our value. We also override the virtual JSON-related methods.

When deriving from a class that inherits from QObject , you need to add the Q_OBJECT   macro to the derived class as well as the base class if the derived class implements its own signals or slots.

string-decorator.cpp:

#include "string-decorator.h"

#include <QVariant>

namespace cm {
namespace data {
class StringDecorator::Implementation { public: Implementation(StringDecorator* _stringDecorator, const QString&
_value)
: stringDecorator(_stringDecorator) , value(_value) { }
StringDecorator* stringDecorator{nullptr}; QString value; };
StringDecorator::StringDecorator(Entity* parentEntity, const QString& key, const QString& label, const QString& value) : DataDecorator(parentEntity, key, label) { implementation.reset(new Implementation(this, value)); }
StringDecorator::~StringDecorator() { }
const QString& StringDecorator::value() const { return implementation->value; }
StringDecorator& StringDecorator::setValue(const QString& value) { if(value != implementation->value) { // ...Validation here if required... implementation->value = value; emit valueChanged(); } return *this; }
QJsonValue StringDecorator::jsonValue() const { return QJsonValue::fromVariant(QVariant(implementation->value)); }
void StringDecorator::update(const QJsonObject& _jsonObject) { if (_jsonObject.contains(key())) { setValue(_jsonObject.value(key()).toString()); } else {
setValue("");
} } }}

Again, there is nothing particularly complicated here. By using the READ and WRITE property syntax rather than the simpler MEMBER keyword, we now have a way of intercepting values being set by the UI, and we can decide whether or not we want to apply the change to the member variable. The mutator can be as complex as you need it to be, but all we’re doing for now is setting the value and emitting the signal to tell the UI that it has been changed. We wrap the operation in an equality check, so we don’t take any action if the new value is the same as the old one.

Here, the mutator returns a reference to self (*this), which is helpful because it enables method chaining, for example,  myName.setValue(“Nick”).setSomeNumber(1234).setSomeOtherProperty(true). However, this is not necessary for the property bindings, so feel free to use the more common void return type if you prefer.

We use a two-step conversion process, converting our QString value into a QVariant before converting it into our target QJsonValue type. The QJsonValue will be plugged into the parent Entity JSON object using the key from the DataDecorator base class. We will cover that in more detail when we write the Entity related classes.

An alternative approach would be to simply represent the value of our various data items as a QVariant member in the DataDecorator base class, removing the need to have separate classes for QString, int, and so on. The problem with this approach is that you end up having to write lots of nasty code that says “if you have a QVariant containing a string then run this code if it contains an int then run this code...”. I prefer the additional overhead of writing the extra classes in exchange for having known types and cleaner, simpler code. This will become particularly helpful when we look at data validation. Validating a string is completely different from validating a number and different again from validating a date.

IntDecorator and DateTimeDecorator are virtually identical to StringDecorator, simply substituting QString values for int or QDateTime. However, we can supplement DateTimeDecorator with a few additional properties to help us out. Add the following properties and an accessor method to go with each:

Q_PROPERTY( QString ui_iso8601String READ toIso8601String NOTIFY valueChanged )
Q_PROPERTY( QString ui_prettyDateString READ toPrettyDateString NOTIFY valueChanged )
Q_PROPERTY( QString ui_prettyTimeString READ toPrettyTimeString NOTIFY valueChanged )
Q_PROPERTY( QString ui_prettyString READ toPrettyString NOTIFY valueChanged )

The purpose of these properties is to make the UI easily access the date/time value as a QString preformatted to a few different styles. Let's run through the implementation for each of the accessors.

Qt has inbuilt support for ISO8601 format dates, which is a very common format when transmitting datetime values between systems, for example, in HTTP requests. It is a flexible format that supports several different representations but generally follows the format yyyy-MM-ddTHH:mm:ss.zt, where T is a string literal, z is milliseconds, and t is the timezone information:

QString DateTimeDecorator::toIso8601String() const
{ if (implementation->value.isNull()) { return ""; } else { return implementation->value.toString(Qt::ISODate); } }

Next, we provide a method to display a full datetime in a long human readable format, for example, Sat 22 Jul 2017 @ 12:07:45:

QString DateTimeDecorator::toPrettyString() const
{
 if (implementation->value.isNull()) {
 return "Not set";
 } else {
 return implementation->value.toString( "ddd d MMM yyyy @ HH:mm:ss" );
 }
}

The final two methods display either the date or time component, for example, 22 Jul 2017 or 12:07 pm:

QString DateTimeDecorator::toPrettyDateString() const
{
 if (implementation->value.isNull()) {
 return "Not set";
 } else {
 return implementation->value.toString( "d MMM yyyy" );
 }
}
QString DateTimeDecorator::toPrettyTimeString() const { if (implementation->value.isNull()) { return "Not set"; } else { return implementation->value.toString( "hh:mm ap" ); } }

Our final type, EnumeratorDecorator, is broadly the same as IntDecorator, but it also accepts a mapper. This container helps us map the stored int value to a string representation. If we consider the Contact.type enumerator we plan to implement, the enumerated value will be 0, 1, 2, so on; however, when it comes to the UI, that number won't mean anything to the user. We really need to present Email, Telephone, or some other string representation, and the map allows us to do just that.

enumerator-decorator.h:

#ifndef ENUMERATORDECORATOR_H
#define ENUMERATORDECORATOR_H
#include <map>
#include <QJsonObject> #include <QJsonValue> #include <QObject> #include <QScopedPointer>
#include <cm-lib_global.h> #include <data/data-decorator.h>
namespace cm { namespace data {
class CMLIBSHARED_EXPORT EnumeratorDecorator : public DataDecorator { Q_OBJECT Q_PROPERTY( int ui_value READ value WRITE setValue NOTIFY
valueChanged )
Q_PROPERTY( QString ui_valueDescription READ valueDescription
NOTIFY valueChanged )

public: EnumeratorDecorator(Entity* parentEntity = nullptr, const QString&
key = "SomeItemKey", const QString& label = "", int value = 0,
const std::map<int, QString>& descriptionMapper = std::map<int,
QString>());
~EnumeratorDecorator();
EnumeratorDecorator& setValue(int value); int value() const; QString valueDescription() const;
QJsonValue jsonValue() const override; void update(const QJsonObject& jsonObject) override;
signals: void valueChanged();
private: class Implementation; QScopedPointer<Implementation> implementation; };
}} #endif

We store the map as another member variable in our private implementation class and then use it to provide the string representation of the enumerated value:

QString EnumeratorDecorator::valueDescription() const
{
 if (implementation->descriptionMapper.find(implementation->value) 
!= implementation->descriptionMapper.end()) {
return implementation->descriptionMapper.at(implementation-
>value);
} else { return {}; } }

Now that we have covered the data types we need for our entities, let’s move on to the entities themselves.