Files:
The Repeater example demonstrates how to use QDeclarativeComponent to extend Cascades with a powerful mechanism to generate a couple of parameterized controls depending on a template and a model.
In this example we'll learn how to use the QDeclarativeComponent class to extend Cascades with a custom element named Repeater that allows the user to load and unload a set of parameterized Controls at runtime. This element can be used to create a calendar control for example, where you have a couple of identical subcontrols (one box for each day) that differ slightly (the day number).
The Repeater takes a model and a delegate as parameters. The delegate is a QML snippet that is used as template for the controls to generate. For each entry of the model, the Repeater will generate one control and add it to the Container the Repeater is located in.
The UI of this sample application consists of a TabbedPane with five pages:
Container { horizontalAlignment: HorizontalAlignment.Fill verticalAlignment: VerticalAlignment.Fill leftPadding: 30 topPadding: 30 rightPadding: 30 Repeater { // Use a simple number (N) as model -> the delegate will be repeated N times model: 5 Label { text: qsTr ("Hello World") textStyle { base: SystemDefaults.TextStyles.BodyText color: Color.White } } } }
In this case the Repeater takes a numeric value (5) as model which means that it will create the given delegate (the 'Hello World' label) 5 times. This makes the declaration of a larger number of identical objects easy, but the power of the Repeater will become visible on the next page.
Container { horizontalAlignment: HorizontalAlignment.Fill verticalAlignment: VerticalAlignment.Fill leftPadding: 30 topPadding: 30 rightPadding: 30 Repeater { // Use a simple number (N) as model -> the delegate will be repeated N times model: 5 Label { // The current index can be accessed via the 'index' context property text: qsTr ("Hello World (%1)").arg(index) textStyle { base: SystemDefaults.TextStyles.BodyText color: Color.White } } } }
On this page the Repeater create 5 Labels again, however this time the delegate accesses the 'index' property which is injected by the Repeater into the context of the delegate (the Label). The value of the 'index' property will be different for each instantiation of the delegate, so we will create 5 different Labels with the content 'Hello World (1)' to 'Hello World (5)' here.
Container { horizontalAlignment: HorizontalAlignment.Fill verticalAlignment: VerticalAlignment.Fill leftPadding: 30 topPadding: 30 rightPadding: 30 Slider { id: slider fromValue: 1 toValue: 5 value: 1 } Repeater { // The number, that is used as model, can be a property binding as well model: slider.immediateValue Label { text: qsTr ("Hello World (%1)").arg(index) textStyle { base: SystemDefaults.TextStyles.BodyText color: Color.White } } } }
On this page we show that the model of the Repeater does not have to be a static value, we can bind the 'value' property of a Slider against the 'model' property of the Repeater. Now whenever the user changes the Slider, the number of Labels will be adapted automatically.
Container { horizontalAlignment: HorizontalAlignment.Fill verticalAlignment: VerticalAlignment.Fill leftPadding: 30 topPadding: 30 rightPadding: 30 Repeater { // Use an array of arbitrary values as model model: [Color.Red, Color.Yellow, Color.Green, Color.Blue, Color.White] Label { text: qsTr ("Hello World (%1)").arg(index) // The current value of the model can be accessed via the 'modelData' context property textStyle { base: SystemDefaults.TextStyles.BodyText color: modelData } } } }
This pages demonstrates that the model of the Repeater does not have to be a numeric value but can also be a list of objects, in this case we use a list of color objects. The 'index' property will be in the range of the size of the list now and the content of the list is accessible within the delegate via the 'modelData' property. So we will get 5 Labels with a numbered 'Hello World' text again, but this time each Label has a different color.
Container { horizontalAlignment: HorizontalAlignment.Fill verticalAlignment: VerticalAlignment.Fill leftPadding: 30 topPadding: 30 rightPadding: 30 ScrollView { scrollViewProperties { scrollMode: ScrollMode.Vertical } Container { Repeater { // Use a DataModel object as model model: _sqlModel Container { topPadding: 30 // The values of the current model entry can be accessed by their key names (here: title, firstname, surname) Label { text: title textStyle.base: SystemDefaults.TextStyles.TitleText textStyle.color: Color.White } Label { text: qsTr ("[%1 %2]").arg(firstname).arg(surname) textStyle.base: SystemDefaults.TextStyles.BodyText textStyle.color: Color.White } Divider {} } } } } }
Finally we show that the model of the Repeater can be an actual DataModel as provided by Cascades. Here we have created a C++ DataSet model, that loads its data from a SQLite database file, and exported it under the name '_sqlModel'. Each entry of this model contains of a list of key/value pairs. The Repeater will make these pairs available to the delegate under the key names, so we can use 'firstname' and 'surname' to access the actual values for that entry.
The Repeater has a 'delegate' property of type QDeclarativeComponent*, which specifies the component used for creating the QML elements. The 'model' property specifies how often the element should be repeated. Apart from using an integer constant here, it is also possible to specify a QVariantList or a DataModel. For the later case, the variant and the map content of the QVariantList and the DataModel are made available in the created QML elements by setting them as their context property.
The fact that writing "delegate: Label { ... }" in QML creates a QDeclarativeComponent* and not a Label* is a special behavior of the QML engine. Internally, the QML engine checks if the property (in this case 'delegate') is of type QDeclarativeComponent*, and if so, treats the assignment as a component assignment, not a item assignment. The Repeater itself just sees a QDeclarativeComponent*, as that is what it gets passed from the QML engine.
class Repeater : public bb::cascades::CustomControl { Q_OBJECT /** * The property that represents the model. * We use QVariant as type here so that the user can pass different type of models here. */ Q_PROPERTY(QVariant model READ model WRITE setModel NOTIFY modelChanged) // The declarative component that is used as 'template' for the repeated elements Q_PROPERTY(QDeclarativeComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) // Mark the 'delegate' property as default property Q_CLASSINFO("DefaultProperty", "delegate") public: Repeater(bb::cascades::Container *parent = 0); ~Repeater(); // The accessor methods for the properties QVariant model() const; void setModel(const QVariant &model); QDeclarativeComponent *delegate() const; void setDelegate(QDeclarativeComponent *delegate); Q_SIGNALS: // The change notification signals of the properties void modelChanged(); void delegateChanged(); private: // This method clears the previsouly generated controls void clear(); // This method regenerates new controls according to the current model data void regenerate(); // The model data QVariant m_model; // The declarative component that is used as 'template' for the repeated elements QDeclarativeComponent *m_delegate; // The list of generated controls QList<bb::cascades::Control*> m_controls; };
The Repeater inherits from bb::cascades::CustomControl, so it can be placed inside a Container. However the newly created Controls are not placed inside the Repeater, but inside its parent Container.
The type of its 'model' property is QVariant. That allows us to assign models of different types (numeric values, arrays, DataModels) to it.
Repeater::Repeater(Container *parent) : CustomControl(parent) , m_model(0) , m_delegate(0) { // The Repeater itself is invisible setVisible(false); }
Inside the constructor we initialize the member variables and hide the Repeater object.
void Repeater::setModel(const QVariant &model) { if (m_model == model) return; // If the model has changed, clear all previously generated controls and regenerate them m_model = model; clear(); regenerate(); emit modelChanged(); } void Repeater::setDelegate(QDeclarativeComponent *delegate) { if (m_delegate == delegate) return; // If the delegate has changed, clear all previously generated controls and regenerate them m_delegate = delegate; clear(); regenerate(); emit delegateChanged(); }
Whenever the 'model' or the 'delegate' property changes, we first delete all previously generated controls by calling clear() and then regenerate the controls with the new model and delegate data by calling regenerate().
void Repeater::clear() { // First remove all generated controls from their parent container ... Container *container = qobject_cast<Container*>(parent()); foreach (Control *control, m_controls) { container->remove(control); } // ... and delete them afterwards. qDeleteAll(m_controls); m_controls.clear(); }
The clear() method retrieves the pointer to the parent Container and then removes all generated controls from it. Afterwards it deletes all generated controls.
void Repeater::regenerate() { // Sanity check if (!m_delegate) return; /** * Find the position of the Repeater inside its parent container, so that we can insert * the newly generated items there. */ Container *container = qobject_cast<Container*>(parent()); const int repeaterPosition = container->indexOf(this); /** * Now we check what the type of the model is and generate the controls. */ if (m_model.type() == QVariant::Int || m_model.type() == QVariant::Double) { /** * If a numeric value (N) was passed as model, we create a new control for each entry * in the sequence [1 - N] and make the index available as context property. */ const int counter = m_model.toInt(); for (int i = 1; i <= counter; ++i) { // Create a child context that is specific for this new control QDeclarativeContext *context = new QDeclarativeContext(m_delegate->creationContext(), this); // Make the current index available as context properties context->setContextProperty("index", i); context->setContextProperty("modelData", i); // Create the control from the delegate component QObject *object = m_delegate->create(context); Control *control = qobject_cast<Control*>(object); // Insert the new control into the parent container container->insert(repeaterPosition + i, control); // Keep a reference to the control, so that we can delete it later on m_controls.append(control); } } else if (m_model.type() == QVariant::List) { /** * If a QVariantList was passed as model, we create a new control for each entry of the list * and make the value at that position available as context property. */ const QVariantList list = m_model.toList(); for (int i = 0; i < list.count(); ++i) { // Create a child context that is specific for this new control QDeclarativeContext *context = new QDeclarativeContext(m_delegate->creationContext(), this); // Make the current index and value available as context properties context->setContextProperty("index", i + 1); context->setContextProperty("modelData", list.at(i)); // Create the control from the delegate component QObject *object = m_delegate->create(context); Control *control = qobject_cast<Control*>(object); // Insert the new control into the parent container container->insert(repeaterPosition + i + 1, control); // Keep a reference to the control, so that we can delete it later on m_controls.append(control); } } else { QObject *model = m_model.value<QObject*>(); bb::cascades::DataModel *dataModel = qobject_cast<bb::cascades::DataModel*>(model); if (dataModel) { /** * If a DataModel was passed as model, we create a new control for each entry of the model * and make all key/value pairs of the entry available as context properties. */ for (int i = 0; i < dataModel->childCount(QVariantList()); ++i) { // Create a child context that is specific for this new control QDeclarativeContext *context = new QDeclarativeContext(m_delegate->creationContext(), this); // Make the current index available as context properties context->setContextProperty("index", i + 1); // Make all key/value pairs of the current model entry available as context properties const QVariantMap map = dataModel->data(QVariantList() << i).toMap(); foreach (const QString &key, map.keys()) { context->setContextProperty(key, map.value(key)); } // Create the control from the delegate component QObject *object = m_delegate->create(context); Control *control = qobject_cast<Control*>(object); // Insert the new control into the parent container container->insert(repeaterPosition + i + 1, control); // Keep a reference to the control, so that we can delete it later on m_controls.append(control); } } } }
Inside the regenerate() method the actual magic happens. The first step is to retrieve the pointer to the parent Container of the Repeater and to find out at which position the new controls should be inserted.
Then we check the type of the model, which can by done by evaluating the return value of type() of the model value.
If the QML file specified a numeric value (either a static value or a bound numeric property), this value is used for iteration to create the new controls. To make the 'index' and 'modelData' properties available to the delegate we need a new QDeclarativeContext object, that is only used by one control instance that we'll create. After we set the values for the 'index' and 'modelData' properties on this context, we pass it to the create() method of the delegate object. This will create a new C++ object that represents the QML snippet which we specified for that delegate. Now we simply insert this object (Control) in the parent Container and append it to an internal list so that we can delete it in clear() later on.
If the model is not a numeric value but a list of objects, the QML runtime will assign a QVariantList to the 'model' property. In this case we create the new controls like described above, just that we use the content of the QVariantList for setting the 'modelData' property now.
In all other cases we check whether a DataModel has been assigned to the 'model' property. If that's the case, we iterate over the number of entries of the model (calling childCount() with empty index path) and retrieve the data for each entry. We expect the model to return a QVariantMap for each entry here. The QVariantMap is basically a list of key/value pairs, so instead of a 'modelData' property, we publish the key names and values to the context of the delegate here.