Qt fundamentals

Qt is a cross-platform application framework that's mostly used to develop UI-driven applications. Qt uses and extends C++ through a code generator called the MOC (meta-object compiler). When you build an app, the MOC reads your C++ header files looking for Qt-specific macros like Q_OBJECT, and produces the appropriate C++ meta-object code. Among other things, the meta-object code is necessary for the signals and slots mechanism.

In addition to the MOC, Qt provides several modules that contain foundational classes that you can use to build a Cascades app, such as QtCore, QtNetwork, QtXml, QtSql, and more. For a closer look at what parts of Qt are supported in Cascades, see Support for Qt in Cascades.

QObject, the Qt base class

All objects in Cascades are derived from the  QObject class. This class is the base class of all Qt objects and provides several important features. Most notably, QObject provides support for signals and slots, which is a powerful mechanism that allows objects to communicate with each other. When something happens to an object in your app (for example, a user clicks a button), the object emits a signal. You can handle that signal and respond to the event by using a function called a slot.

To illustrate some of the features and requirements of a QObject in Cascades, consider the  Button control. This control represents a clickable button that you can use in your apps. The header file (button.h) for this control is located in the folder where you installed the BlackBerry 10 Native SDK, in the target\qnx6\usr\include\bb\cascades\controls subfolder. Let's take a closer look at some of the code in this file and what the code means.

Class declaration

class QT_CASCADES_EXPORT Button : public AbstractButton {

All Cascades controls inherit QObject, either directly or indirectly. The Button class inherits several intermediate classes, all of which eventually inherit QObject.

An inheritance tree that extends from Button up to QObject.

Q_OBJECT macro

private:
    Q_OBJECT

To identify a class as a QObject, you need to use the Q_OBJECT macro in the private: section of the class definition. This macro is processed by the Qt meta-object compiler (MOC) and enables features such as properties, signals, and slots. To learn more about the Qt meta-object system and meta-object compiler, see The Meta-Object System on the Qt website.

Property declaration

Q_PROPERTY(QString text READ text WRITE setText RESET resetText 
           NOTIFY textChanged FINAL)

A property is a single piece of information about an object, and is declared by using the Q_PROPERTY macro. This macro allows the property to be accessible in QML. In the Q_PROPERTY, you specify the name and type of the property. You also use keywords (such as READ and WRITE) to specify the names of functions that can manipulate the value of the property. Properties are declared in the private: section of the header file, along with the Q_OBJECT macro.

As you can see above, the Button class includes the text property, which specifies the text that appears on the button. This property is a QString (the Qt representation of a text string) and has functions called text()setText(), and resetText() to get, set, and reset the value of the property, respectively. These functions still need to be declared later; the Q_PROPERTY macro only associates them with the property. You can also specify a signal that's emitted when the value changes by using the keyword NOTIFY. To learn more about properties and the keywords you can use, see The Property System on the Qt website.

Function declaration

public:
    Q_SLOT void setText(const QString & text);

You can create functions for QObject classes just as you do for any other C++ classes. Notice, though, that the Q_SLOT macro is used before the function declaration here. This macro is another special element of Qt and indicates that this function is a slot. You can connect slots to signals so that when a signal is emitted, the connected slot function is called automatically. As an alternative to the Q_SLOT macro, you might see slot functions declared in a public slots: section in a header file. In Cascades, these approaches are identical.

Another useful macro that you might see is Q_INVOKABLE. You can use this macro in front of a function declaration (in the same place as Q_SLOT above) to indicate that you want to be able to call the function from QML. This approach can be useful if you have a complicated operation (for example, a custom transition for your UI control) that you want to implement in C++. You can still keep the UI-related portion (calling the function at the appropriate time) in QML, but you can implement the operation in C++ any way you want. To learn more about Q_INVOKABLE, see QML and C++ integration.

Signal declaration

Q_SIGNALS:
    void textChanged(QString text);

You can use the Q_SIGNALS: section to declare signals that a QObject emits when certain events occur. A Button emits the textChanged() signal when its text changes, and you can handle this event in your app if required. You might also see signals declared in a signals: section in a header file, which means the same thing in Cascades.

Object ownership

Every QObject can be related to other objects in parent/child relationships. An object can have one parent, as well as zero or more children. A parent object owns its child objects. When an object is destroyed, its children are also destroyed, so it's important to remember which objects are related to other objects in your apps.

Scene graphs

Cascades maintains the relationships between objects internally, and transfers ownership between objects automatically when you call functions that change object ownership. To make it easier to visualize the parent/child relationships in your app, you can use a hierarchichal structure called a scene graph. This graph shows the controls (also called nodes) in your app and how they're related to each other. You can't view the scene graph for your app in the Momentics IDE or anywhere else; it's just a concept that makes it easier to visualize how Cascades keeps track of object relationships internally.

A hierarchical structure (such as a scene graph) is commonly used in many types of applications, such as complex UI applications and games, and makes it easier to manage complex arrangements of components. Using a hierarchichal structure can also increase the performance of applications, which is a major reason why it was chosen as a feature for Cascades. Finally, the hierarchical nature of scene graphs fits nicely with the parent/child relationship structure of Qt objects. To learn more about this relationship structure, see Object Trees & Ownership from the Qt documentation.

For example, consider a  Container with two  Button controls.

Container {
    Button {
        text: "Button 1"
    }
    Button {
        text: "Button 2"
    }
}

Here's what the scene graph would look like for this arrangement:


A diagram that shows the scene graph for a container that owns two buttons.

Scene graphs can help you keep track of how object ownership might affect various aspects of your app. For example, you can use a scene graph to visualize how touch events are delivered to different controls on the screen. To learn how scene graphs can help you handle touch events, see Touch propagation.

Setting ownership

Object ownership can be established either when controls are created or when they're added to other controls. Both approaches result in the same parent/child relationships, so you can choose which approach you prefer to use in your apps.

For example, here's how to create a container and make it the parent for two buttons. This approach establishes object ownership when the buttons are created. The parent for each button is specified in the  Button constructor, and each button becomes owned by the specified parent automatically when it's created.

// Create the top-level container
Container *topContainer = new Container;
 
// Create the buttons, specifying the container as the parent.
// The container now owns the buttons.
Button *buttonOne = new Button(topContainer);
Button *buttonTwo = new Button(topContainer);

Here's a second approach that establishes object ownership by adding the buttons to the container explicitly.

// Create the top-level container
Container *topContainer = new Container;
 
// Create the buttons
Button *buttonOne = new Button;
Button *buttonTwo = new Button;
 
// Add the buttons to the container. The container now
// owns the buttons.
topContainer->add(buttonOne);
topContainer->add(buttonTwo);

After object ownership is established, you can change the parent of an object by adding the object to a different parent. The object is removed from the first parent and added to the new parent, and the new parent then owns the object. Here's an example:

// Create two containers
Container *firstContainer = new Container;
Container *secondContainer = new Container;
 
// Create a button
Button *myButton = new Button;
 
// Add the button to the first container. The first container
// now owns the button.
firstContainer->add(myButton);
 
// Add the button to the second container. The second container now
// owns the button, and the first container no longer owns it.
secondContainer->add(myButton);

You can also remove an object from a container without specifying a new parent for the object. However, here's when things get tricky: even though the object was removed from the container, the container is still the object's parent and still owns the object. If the container is deleted, the object is deleted as well.

This behavior can lead to errors in your application that might be difficult to recognize. For example, consider the following example of two buttons that are added to a container. The second button is then removed from the container, and after the button is removed, the container is deleted. Finally, the removed button's  setText() function is called.

// WARNING: This code will generate an error if you try to run it
 
// Create a container
Container *myContainer = new Container;
 
// Create two buttons and add them to the container
Button *buttonOne = new Button;
Button *buttonTwo = new Button;
myContainer->add(buttonOne);
myContainer->add(buttonTwo);
 
// Remove the second button from the container
myContainer->remove(buttonTwo);
 
// Delete the container
delete myContainer;
 
// Attempt to set the text of the removed button
buttonTwo->setText("Button Two");

Because the second button was removed from the container before the container was deleted, you might expect that when the container is deleted, the button isn't deleted and you can still access it. However, the container remains the parent of the button even after  remove() is called. The second button is deleted when the container is deleted (along with the first button), so the call to setText() isn't valid because the button doesn't exist anymore.

When an object is removed from its parent, or when an object is replaced by a new object (in the case of certain setter functions, such as  Page::setContent()), it's removed from the visual hierarchy (that is, it's not displayed on the screen anymore). The object's parent still owns the object, but you can change the parent or delete the object at this point. If you don't change the object's parent, it's still deleted when the parent object is deleted. It's important to note that you can't change the parent of an object if the object is still part of the visual hierarchy of its parent. You need to remove it before you try to change its parent.


A diagram illustrating how object ownership is affected when child controls are removed from their parents.

All  QObject instances include a function called  setParent(), and you can use this function to change the parent of an object (when it's allowed, as described above). When you use setParent() to change an object's parent, the function doesn't add that object to the parent's visual hierarchy; you still need to add it explicitly (for example, by calling  Container::add()).

To prevent the error that occurred in the code sample above, you can add a call to setParent(0) after you remove an object from its parent. This way, the object isn't deleted when its former parent is deleted.

...
 
// Remove the button from the container
myContainer->remove(buttonTwo);
 
// Reparent the button so that it's not deleted when the 
// container is deleted
buttonTwo->setParent(0);
 
// Delete the container
delete myContainer;
 
// Attempt to set the text of the button. This time, this call will
// work as expected because the button hasn't been deleted. You'd
// still need to add the button to the visual hierarchy of another
// container (using add()) for the button to appear on the screen.
buttonTwo->setText("Button Two");

If you have a UIObject that does not have a parent, an application crash can occur if the application quits and the object hasn't been deleted first. Always make sure to set a parent for UIObject instances or delete them manually before your application quits.

Scene ownership

There's one important exception to the rules of object ownership. The top-level scene in your application, which is the  AbstractPane object that you set as the root node using Application::setScene(), behaves differently. The application takes ownership of this AbstractPane only if the pane doesn't already have a parent. If the AbstractPane doesn't have a parent when setScene() is called, the application owns the pane and deletes it if a new scene is set. However, if the AbstractPane already has a parent, the application won't take ownership of the pane when setScene() is called.

This behavior lets you switch between different scenes in your applications but ensures that for the most basic use case (an application with just a single scene), the old scene is deleted when appropriate. If you want to switch between two scenes, you should set the parents of the AbstractPane objects explicitly using setParent() before you call setScene().

Many other functions of Cascades classes change the ownership of objects. You should check the API reference for the classes and functions that you're working with to make sure that they don't change object ownership in unexpected ways.

Object ownership in QML

In QML, object ownership is established automatically according to the structure of the QML code that you write. For example, here's how to create the same example as above, with two Button controls and a Container:

Container {
    Button {
        text: "Button 1"
    }
    Button {
        text: "Button 2"
    }
}

The container is the parent of the buttons, and the buttons are children of the container. When the container is destroyed, both buttons are destroyed as well.

Signals and slots

For handling events and inter-object communication, Qt uses a mechanism called signals and slots. By using this mechanism, objects can communicate with each other by emitting a signal. An object that wants to receive a message from another object can create a slot for that particular signal.

Unlike the callback technique for inter-object communication, signals and slots are type-safe and loosely coupled. An object that implements a slot must match the method signature for the signal that it can receive. This approach allows the compiler to check the type of the subscription. Apart from the data type in the signature, a slot implementation doesn't need to know anything about the signal implementation. The slot doesn't even need to receive the same number of arguments that a signal emits. Similarly, you can send signals to objects if you know which slots they implement. The framework discards signal arguments that are not implemented by the slot.

Events in Cascades are implemented as signals. When a user interacts with your UI, the Cascades framework emits a signal. The framework delivers the signal to each slot that's connected to the signal.

To learn more about signals and slots, see Signals and slots.

Thread support

Qt provides support for threaded applications with the  QThread class. The ability to create new threads is often necessary if your application contains resource-intensive processes that might otherwise block the UI thread. Some Qt classes, like QNetworkAccessManager, are already asynchronous in nature and don't require the creation of a separate thread to operate.

QThread

QThread represents a separate thread of control in the application; it can share data with all the other threads within the application, but runs independently in the same way that a standalone application does. To facilitate communication between threads, signals and slots are fully supported.

Previously, the standard method for creating a QThread was by subclassing it. Now, you should use QThread by creating a worker object that is derived from QObject and implementing a slot on that worker to handle the task. The worker should also specify a signal that is emitted when the task is finished, and an optional signal that is emitted when there's an error.

class Worker : public QObject {
    Q_OBJECT
public:
    Worker();
    virtual ~Worker();
public slots:
    void process();
 
signals:
    void finished();
    void error(QString err);
};

After you create the worker class, you can give QThread ownership of the worker, connect the appropriate signals and slots, and start the thread.

// Create a thread
QThread* thread = new QThread;
Worker* worker = new Worker();
 
// Give QThread ownership of Worker Object
worker->moveToThread(thread);
 
// Connect worker error signal to this errorHandler SLOT
// If any Q_ASSERT statement(s) indicate that the slot failed 
// to connect to the signal, make sure you know exactly why this
// has happened. This is not normal, and will cause your
// app to stop working
bool connectResult;

// Since the variable is not used in the app, this is added 
// to avoid a compiler warning
Q_UNUSED(connectResult);

connectResult = connect(worker, SIGNAL(error(QString)), 
                        this, SLOT(errorHandler(QString)));

// This is only available in Debug builds
Q_ASSERT(connectResult);
                        
// Connects the thread's started() signal to the process() slot
// in the worker, causing it to start
connectResult = connect(thread, SIGNAL(started()),
                        worker,
                        SLOT(process()));

// This is only available in Debug builds
Q_ASSERT(connectResult);

// Connect worker finished signal to trigger thread 
// quit, then delete
connectResult = connect(worker, SIGNAL(finished()),
                        thread,
                        SLOT(quit()));

// This is only available in Debug builds
Q_ASSERT(connectResult);

connectResult = connect(worker, SIGNAL(finished()),
                        worker,
                        SLOT(deleteLater()));
 
// Make sure the thread object is deleted after execution 
// has finished
connectResult = connect(thread, SIGNAL(finished()),
                        thread, 
                        SLOT(deleteLater()));

// This is only available in Debug builds
Q_ASSERT(connectResult);

thread->start();

For more information, see Thread Support in Qt on the Qt website.

QtConcurrentRun

The QtConcurrent::run() function is another way that you can run processes asynchronously. The difference from QThread is that it's designed to run just a single function. For more information on QtConcurrent, see QtConcurrentRun on the Qt website.

Last modified: 2013-12-21

comments powered by Disqus