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

Data models

Now that we have the infrastructure in place to be able to define data objects (entities and entity collections) and properties of various types (data decorators), we can move on and build the object hierarchy we laid out earlier in the chapter. We already have a default Client class created by Qt Creator, so supplement that in cm-lib/source/models with the following new classes:

We’ll start with the simplest of the models—the address.

address.h:

#ifndef ADDRESS_H
#define ADDRESS_H
#include <QObject>
#include <cm-lib_global.h> #include <data/string-decorator.h> #include <data/entity.h>
namespace cm { namespace models {
class CMLIBSHARED_EXPORT Address : public data::Entity { Q_OBJECT Q_PROPERTY(cm::data::StringDecorator* ui_building MEMBER building
CONSTANT)
Q_PROPERTY(cm::data::StringDecorator* ui_street MEMBER street
CONSTANT)
Q_PROPERTY(cm::data::StringDecorator* ui_city MEMBER city CONSTANT) Q_PROPERTY(cm::data::StringDecorator* ui_postcode MEMBER postcode
CONSTANT) Q_PROPERTY(QString ui_fullAddress READ fullAddress CONSTANT) public: explicit Address(QObject* parent = nullptr); Address(QObject* parent, const QJsonObject& json);
data::StringDecorator* building{nullptr}; data::StringDecorator* street{nullptr}; data::StringDecorator* city{nullptr}; data::StringDecorator* postcode{nullptr}; QString fullAddress() const; };
}}
#endif

We define the properties we designed at the beginning of the chapter, but instead of using regular QString objects, we use our new StringDecorators. To protect the integrity of our data, we should really use the READ keyword and return a StringDecorator* const via an accessor method, but for simplicity, we’ll use MEMBER instead. We also provide an overloaded constructor that we can use to construct an address from a QJsonObject. Finally, we add a helper fullAddress() method and property to concatenate the address elements into a single string for use in the UI.

address.cpp:

#include "address.h"

using namespace cm::data;
namespace cm { namespace models {
Address::Address(QObject* parent) : Entity(parent, "address") { building = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "building", "Building"))); street = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "street", "Street"))); city = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "city", "City"))); postcode = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "postcode", "Post Code"))); }
Address::Address(QObject* parent, const QJsonObject& json) : Address(parent) { update(json); }
QString Address::fullAddress() const { return building->value() + " " + street->value() + "n" + city->value() + "n" + postcode->value(); } }}

This is where all of our hard work starts to come together. We need to do two things with each of our properties. Firstly, we need a pointer to the derived type (StringDecorator), which we can present to the UI in order to display and edit the value. Secondly, we need to make the base Entity class aware of the base type (DataDecorator) so that it can iterate the data items and perform the JSON serialization work for us. We can use the addDataItem() method to achieve both these goals in a one-line statement:

building = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, "building", "Building")));

Breaking this down, we create a new StringDecorator* with the building key and Building UI label. This is immediately passed to addDataItem()which adds it to the dataDecorators collection in the Entity and returns the data item as a DataDecorator*. We can then cast it back to a StringDecorator* before storing it in the building member variable.

The only other piece of implementation here is to take a JSON object, construct the address as normal by calling the default constructor, and then update the model using the update() method.

The Appointment and Contact models follow the same pattern, just with different properties and the appropriate variation of DataDecorator for each of their data types.  Where Contact varies more significantly is in its use of an EnumeratorDecorator for the contactType property. To support this, we first define an enumerator in the header file that contains all the possible values we want:

enum eContactType {
 Unknown = 0,
 Telephone,
 Email,
 Fax
};

Note that we have a default value of Unknown represented by 0. This is important as it allows us to accommodate an initial unset value. Next, we define a mapper container that allows us to map each of the enumerated types to a descriptive string:

std::map<int, QString> Contact::contactTypeMapper = std::map<int, QString> {
 { Contact::eContactType::Unknown, "" }
 , { Contact::eContactType::Telephone, "Telephone" }
 , { Contact::eContactType::Email, "Email" }
 , { Contact::eContactType::Fax, "Fax" }
};

When creating the new EnumeratorDecorator, we supply the default value (0 for eContactType::Unknown) along with the mapper:

contactType = static_cast<EnumeratorDecorator*>(addDataItem(new EnumeratorDecorator(this, "contactType", "Contact Type", 0, contactTypeMapper)));

Our client model is a little more complex, as it not only has data items but has child entities and collections too. However, the way we create and expose these things is very similar to what we have already seen.

client.h:

#ifndef CLIENT_H
#define CLIENT_H
#include <QObject>
#include <QtQml/QQmlListProperty>
#include <cm-lib_global.h> #include <data/string-decorator.h> #include <data/entity.h> #include <data/entity-collection.h> #include <models/address.h> #include <models/appointment.h> #include <models/contact.h>
namespace cm { namespace models {
class CMLIBSHARED_EXPORT Client : public data::Entity { Q_OBJECT Q_PROPERTY( cm::data::StringDecorator* ui_reference MEMBER
reference CONSTANT )
Q_PROPERTY( cm::data::StringDecorator* ui_name MEMBER name CONSTANT ) Q_PROPERTY( cm::models::Address* ui_supplyAddress MEMBER
supplyAddress CONSTANT )
Q_PROPERTY( cm::models::Address* ui_billingAddress MEMBER
billingAddress CONSTANT )
Q_PROPERTY( QQmlListProperty<Appointment> ui_appointments READ
ui_appointments NOTIFY appointmentsChanged )
Q_PROPERTY( QQmlListProperty<Contact> ui_contacts READ ui_contacts
NOTIFY contactsChanged )

public: explicit Client(QObject* parent = nullptr); Client(QObject* parent, const QJsonObject& json);
data::StringDecorator* reference{nullptr}; data::StringDecorator* name{nullptr}; Address* supplyAddress{nullptr}; Address* billingAddress{nullptr}; data::EntityCollection<Appointment>* appointments{nullptr}; data::EntityCollection<Contact>* contacts{nullptr};

QQmlListProperty<cm::models::Appointment> ui_appointments();
QQmlListProperty<cm::models::Contact> ui_contacts();

signals:
void appointmentsChanged();
void contactsChanged(); };
}}
#endif

We expose the child entities as pointers to the derived type and the collections as pointers to a templated EntityCollection.

client.cpp:

#include "client.h"

using namespace cm::data;
namespace cm { namespace models {
Client::Client(QObject* parent) : Entity(parent, "client") { reference = static_cast<StringDecorator*>(addDataItem(new
StringDecorator(this, "reference", "Client Ref")));
name = static_cast<StringDecorator*>(addDataItem(new
StringDecorator(this, "name", "Name")));
supplyAddress = static_cast<Address*>(addChild(new Address(this),
"supplyAddress"));
billingAddress = static_cast<Address*>(addChild(new Address(this),
"billingAddress"));
appointments = static_cast<EntityCollection<Appointment>*>
(addChildCollection(new EntityCollection<Appointment>(this,
"appointments")));
contacts = static_cast<EntityCollection<Contact>*>(addChildCollection(new EntityCollection<Contact>(this, "contacts"))); }
Client::Client(QObject* parent, const QJsonObject& json) : Client(parent) { update(json); }

QQmlListProperty<Appointment> Client::ui_appointments()
{
return QQmlListProperty<Appointment>(this, appointments->derivedEntities()); }

QQmlListProperty<Contact> Client::ui_contacts()
{
return QQmlListProperty<Contact>(this, contacts->derivedEntities());
}

}}

Adding child entities follows the same pattern as data items, but using the addChild() method. Note that we add more than one child of the same address type, but ensure that they have different key values to avoid duplicates and invalid JSON. Entity collections are added with addChildCollection() and other than being templated, they follow the same approach.

While it was a lot of work to create our entities and data items, creating models is really quite straightforward and now they all come packed with features that we wouldn’t otherwise have had.

Before we can use our fancy new models in the UI, we need to register the types in main.cpp in cm-ui, including the data decorators that represent the data items. Remember to add the relevant #include statements first:

qmlRegisterType<cm::data::DateTimeDecorator>("CM", 1, 0, "DateTimeDecorator");
qmlRegisterType<cm::data::EnumeratorDecorator>("CM", 1, 0, "EnumeratorDecorator");
qmlRegisterType<cm::data::IntDecorator>("CM", 1, 0, "IntDecorator");
qmlRegisterType<cm::data::StringDecorator>("CM", 1, 0, "StringDecorator");
qmlRegisterType<cm::models::Address>("CM", 1, 0, "Address"); qmlRegisterType<cm::models::Appointment>("CM", 1, 0, "Appointment"); qmlRegisterType<cm::models::Client>("CM", 1, 0, "Client"); qmlRegisterType<cm::models::Contact>("CM", 1, 0, "Contact");

With that done, we’ll create an instance of a client in MasterControllerwhich we will use to populate data for new clients. This follows exactly the same pattern that we’ve used for adding the other controllers.

First, add the member variable to the private implementation of MasterController:

Client* newClient{nullptr};

Then, initialize it in the Implementation constructor:

newClient = new Client(masterController);

Third, add the accessor method:

Client* MasterController::newClient()
{
 return implementation->newClient;
}

Finally, add Q_PROPERTY:

Q_PROPERTY( cm::models::Client* ui_newClient READ newClient CONSTANT )

We now have an empty instance of a client available for consumption by the UI, specifically CreateClientView, which we will edit next. Begin by adding a shortcut property for the new client instance:

property Client newClient: masterController.ui_newClient

Remember that the properties should all be defined at the root Item level and that you need to import CM 1.0 to get access to the registered types. This just enables us to use newClient as shorthand to access the instance rather than having to type out masterController.ui_newClient every time.

At this point, everything is hooked up ready for use, and you should be able to run the application and navigate to the new client view with no problems. The view isn’t doing anything with the new client instance just yet, but it’s happily sitting there ready for action. Now, let’s look at how we can interact with it.