Signals and slots

The Cascades framework uses signals and slots to allow objects to communicate with each other. This system is based on the signals and slots mechanism that the underlying Qt development framework uses for inter-object communication.

The signals and slots mechanism is how Cascades manages event handling. In other frameworks, you might design your application to listen for a particular event that you're interested in, such as a touch event. When that event occurs, your app processes the event and responds to it. Cascades uses a similar approach but provides an additional layer (signals and slots) on top of the fundamental event system. This layer makes handling different types of events easier and more intuitive. The idea of signals and slots is different than normal event handling approaches, so throughout this documentation you'll notice that "signals and slots", instead of "event handling", is used to describe the system.

In many applications that you create, when a change occurs to one object, you'll want to notify other objects of the change. The notification takes the form of a signal. A signal is emitted by an object when some type of event occurs, such as when a property of the object changes. Other objects can receive these signals by using slots. A slot is a function that is called when an object receives a signal. Slots are member functions of a class, and can either be called directly or be connected to a signal. In Cascades, there are many predefined signals that you can connect to slots in your app, but you're also free to create your own signals and slots if you need them.

For example, the  Button control includes a predefined signal called  clicked(), which (as you'd expect) is emitted when the button is clicked. You can connect this signal to another object in your app to respond to the signal when it's emitted. The  Slider control includes a slot called  resetValue(), which resets the value of the slider to its default value. You could connect the button's  clicked() signal to the slider's  resetValue() slot so that when the button is clicked, the slider's value is reset.


A diagram illustrating the relationship between signals and slots in the given example.

The emission of signals from objects is separate and distinct from the connection of signals to slots; signals are emitted from objects even if no slots are connected to those signals. Also, a signal-to-slot connection isn't necessarily a one-to-one relationship. You can connect many signals to one slot, or one signal to many slots. You can also connect signals to other signals, so that when one signal is emitted, the connected signal is also emitted.

For more detailed information, see Signals & Slots on the Qt website. The use of signals and slots in Cascades is very similar to the implementation in Qt. There are some differences, and the following sections describe how to use the specific implementation in Cascades.

Using predefined signals

Most core UI controls include predefined signals that are emitted when something interesting happens to the control. A button emits a signal when it's clicked, a slider emits a signal when its value changes, and a text area emits a signal when its text changes. You can respond to these signals in either QML or C++.

Responding to predefined signals in QML

In QML, predefined signals have corresponding signal handlers that are created automatically for your use. The names of these signal handlers are based on the signal that they correspond to, and start with the prefix "on". For example, the clicked() signal for a Button has an onClicked signal handler. Notice that the casing of the signal name changes to conform to the lower camel case format (from clicked to onClicked ).

Inside the signal handler, you can use JavaScript to define how the control should respond to the signal. You can change the control's property values, change other controls' property values, call functions, and so on. Here's how to create a button that changes its text when it's clicked:

import bb.cascades 1.0
 
Page {
    content: Button {
        text: "Click me"
             
        onClicked: {
            text = "Clicked!"
        }
    }
}

Some predefined signals include parameters that you can access and use in the signal handlers. These parameters provide additional information about the change that occurred. You can check the API reference to determine whether a signal provides parameters and what they are.

For example, the CheckBox control emits a checkedChanged() signal when its checked state changes (checked or cleared). This signal includes a Boolean parameter,  checked , that contains the new checked state of the check box. Here's how to create a check box that, when its checked state changes, updates the text of an adjacent Label:

import bb.cascades 1.0
 
Page {
    content: Container {
        CheckBox {
            onCheckedChanged: {
                if (checked)
                    checkLabel.text = "Checked";
                else
                    checkLabel.text = "Not checked";
            }
        }
         
        Label {
            id: checkLabel
            text: "Not checked"
        }
    } // end of Container
} // end of Page

Using predefined signals and their associated signal handlers is an easy way to provide simple interactions in your apps. You can respond to signals from core UI controls with minimal effort and code. The signal handlers are provided for you already, and all you need to do is fill in the actions you want to take.

Responding to predefined signals in C++

In C++, you use the  QObject::connect() static function to connect a signal to a slot. This function is overloaded to accept a few different argument combinations, but you typically call the function with the following four arguments:

  • An object representing the sender of the signal
  • The signal to send
  • An object representing the receiver of the signal
  • The slot to receive the signal

As an example, consider a smoke detector and sprinkler system in your home or office. When the smoke detector detects smoke, it sends a signal to the sprinkler to indicate that the sprinkler should start dispensing water to put out a potential fire.

You could model this behavior in Cascades by using signals and slots. You might have an object that represents the smoke detector, emitting a signal when it detects smoke. You might also have an object that represents the sprinkler, with a slot that dispenses the water.

Screen showing a sprinkler system spraying water.

Here's how you could connect the smoke detector's signal to the sprinkler's slot:

connectResult = QObject::connect(smokeDetector,
                                 SIGNAL(smokeDetected()),
                                 sprinkler,
                                 SLOT(dispenseWater()));

When you specify the signal and slot in connect(), you need to use the SIGNAL() and SLOT() macros, respectively. These macros convert the names of the signal and slot that you want to connect to string literals that are used by connect().

It's easy to mistype signal or slot names when using connect(), or put parentheses in the wrong locations, so it can be a good idea to check the return value from connect() to make sure that there aren't any errors. The connect() function returns a Boolean value that indicates whether the connection of the signal and slot was successful. You can use Q_ASSERT() to test the return value and generate a warning message if the connection isn't successful.

Here's how to check the return value from connect() and generate a warning if something's gone wrong with the connection:

// 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 = QObject::connect(smokeDetector, 
                                 SIGNAL(smokeDetected()), 
                                 sprinkler,
                                 SLOT(dispenseWater()));

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

In C++, you need to be explicit when handling signals. You can't use the signal handlers for predefined signals the way you can in QML. Instead, to handle a predefined signal that you're interested in, you have two choices: you can connect the signal to an existing slot of another class, or define your own slot to handle the signal and then connect the signal to that slot.

When you connect a signal to an existing slot of another class, you usually pass some kind of information directly from the signal object to the slot object. This information is passed using signal parameters. For example, the Slider control has a signal called immediateValueChanged() that's emitted when the value of the slider changes. This signal includes a  value  parameter that contains the new value of the slider. You could connect this signal to the setPreferredWidth() slot of a Container, and use the parameter to have the width of the container change as the value of the slider changes. Here's how to do that:

// Create the slider and container
Slider* mySlider = Slider::create()
                    .from(100)
                    .to(500);
Container* mySizedContainer = Container::create()
                               .preferredWidth(100)
                               .preferredHeight(100)
                               .background(Color::DarkGreen);
                                
// Connect the slider's immediateValueChanged() signal to
// the container's setPreferredWidth() slot function. Make sure to
// test the return value to detect any errors
bool res = QObject::connect(mySlider, 
                            SIGNAL(immediateValueChanged(float)),
                            mySizedContainer, 
                            SLOT(setPreferredWidth(float)));

// This is only available in Debug builds
Q_ASSERT(res);
 
// Since the variable is not used in the app, this is added 
// to avoid a compiler warning
Q_UNUSED(res);

To use this approach to pass information between controls, the signal and the slot need to use the same types of parameters. You can't connect a signal with a float parameter to a slot that expects a QString parameter. Even though the types of parameters must match, the number of parameters doesn't need to. The slot can have the same number of parameters (or fewer) as the signal that it's connected to. For example, you can connect a signal with two parameters (a float and a bool) to a slot with only a float parameter; the additional bool parameter from the signal is ignored.

It's important to note that if a parameter for a signal or slot is of a type that is specific to Cascades, you must use the full namespace when you specify the parameter. For most controls, this namespace is bb::cascades . For example, the ImageView control contains a signal called imageChanged(), which is emitted when the image that the ImageView represents is changed. This signal includes an Image parameter that represents the new image. To connect this signal to a slot, use the following form:
QObject::connect(myImageView,
                 SIGNAL(imageChanged(bb::cascades::Image*)),
                 myHandler,
                 SLOT(onImageChanged(bb::cascades::Image*)));

Connecting signals to existing slots works well for simple connections, but what if the signal and slot parameters don't match, or you need to perform a more complex task in response to the signal? In these cases, you can define your own slot to handle the signal.

When you create a slot, you're simply creating a member function for the class you're working with. To allow the function to act as a slot, you place the function declaration in a public slots: section in the class header file:

public slots:
    void myCustomSlot();

You can then define the function in the corresponding source file, and connect a signal to the slot using QObject::connect(). Slot functions almost always have a return type of void. You could create a slot that returns a value or an object, but the signals and slots mechanism wouldn't allow you to access this return value.

In one of the code samples above, QML was used to change the text of a Label when the associated check box changed its state. Here's how to achieve the same result using a custom slot in C++:

// The slot declaration, in a header file called App.hpp
 
public slots:
    void handleCheckedChange(bool newCheckedState);
// The slot definition, in the corresponding 
// source file, App.cpp. The variable checkLabel is a private 
// member variable of type Label*
 
void App::handleCheckedChange(bool newCheckedState)
{
    if (newCheckedState)
        checkLabel->setText("Checked");
    else
        checkLabel->setText("Not checked");
}
// The signal/slot connection, in App.cpp
 
// Create the check box and label
CheckBox* myCheckBox = new CheckBox;
checkLabel = Label::create().text("Not checked");
 
// Connect the check box's checkedChanged() signal to
// the handleCheckedChanged() slot function. Make sure to test 
// the return value to detect any errors
bool res = QObject::connect(myCheckBox, 
                            SIGNAL(checkedChanged(bool)), 
                            this,
                            SLOT(handleCheckedChange(bool)));
Q_ASSERT(res);
 
// Since the variable is not used in the app, this is added to avoid a 
// compiler warning
Q_UNUSED(res);

Although using C++ to create your own slots involves more code, it's also more flexible. You can do more complex tasks in your slot implementations, and you can use the full range of C++ functionality instead of being limited only to JavaScript.

Creating your own signals

If the predefined signals in Cascades aren't sufficient for your app, or if you want to emit signals from custom objects that you create, you can create your own signals and connect them to slots in either QML or C++.

Creating signals in QML

In QML, you create a custom signal for a control by using the keyword signal and providing any parameters that the signal includes when it's emitted:

signal mySignal(float value, bool enabled)

Similar to predefined signals, Cascades creates signal handlers for custom signals automatically. The custom signal mySignal() above has a signal handler called onMySignal, and the parameters value and enabled are available for you to use in the body of the signal handler.

To emit a custom signal, you simply call the signal the same way you call a regular function. Here's how to create a button that, when it's clicked, emits a custom signal called clickedWithText. This signal includes a string parameter called currentText, which contains the text of the button:

import bb.cascades 1.0
 
Page {
    content: Button {
        id: myButton
        signal clickedWithText(string currentText)
        text: "Hello world"
         
        onClicked: {
            clickedWithText(text);
        }
    }
}

In addition to using the signal handler that Cascades creates for you, you can create your own slot and connect a custom signal to it. You define a slot as a JavaScript function, and connect your signal to that slot. Here's how to create a button that, when it's clicked, displays its text in a text area:

import bb.cascades 1.0
 
Page {
    content: Container {
        Button {
            id: myButton
            signal sendText(string currentText)
            text: "Hello world"
             
            onClicked: {
                // Connect the sendText() signal to the custom
                // JavaScript function called handleSendText(), and
                // then emit the sendText() signal
                myButton.sendText.connect(myTextArea.handleSendText)
                myButton.sendText(text);
            }
        }
         
        TextArea {
            id: myTextArea
             
            // A custom JavaScript function to handle the 
            // sendText() signal
            function handleSendText(message) {
                myTextArea.text = message;
            }
        }
    }
}

There are other approaches you could use to achieve this result. You could decide to create your button and text area as separate QML components, and define each in their own .qml file. For more information about creating custom components, see Custom QML components.

Creating signals in C++

In C++, you can create a custom signal by placing the declaration in a signals: section in the class header file. You can also include any parameters that the signal includes when it's emitted:

signals:
    void myFirstSignal();
    void mySecondSignal
        (bool enabled, bb::cascades::Button* myButton);

Like slots, signals almost always have a return type of void. When you want to emit a signal, you use the keyword emit:

emit myFirstSignal();

You can connect a custom signal to a slot by using QObject::connect(), in the same way that you connect a predefined signal to a slot:

QObject::connect(signalObject, SIGNAL(myCustomSignal()), slotObject,
                 SLOT(mySlot());

Here's one way to create the same custom signal from one of the QML code samples above, in C++:

// The signal and slot declarations, in a header file called App.hpp
 
signals:
    void sendText(QString currentText);
 
public slots:
    void emitSendText();
    void handleSendText(QString message);
// The slot definitions, in the corresponding source
// file called App.cpp. The variables myButton and myTextArea are
// private member variables of type Button* and TextArea*, respectively.
 
void App::emitSendText()
{
    emit sendText(myButton->text());
}
 
void App::handleSendText(QString message)
{
    myTextArea->setText(message);
}
// The main code of the app, in App.cpp
 
// Create the UI elements
Page* root = new Page;
Container* myContainer = new Container;
myButton = Button::create().text("Hello world");
myTextArea = TextArea::create();
 
// Connect the signals to the appropriate slots. Make sure
// to test the return values to detect any errors.
bool res = QObject::connect(myButton, SIGNAL(clicked()), this,
                            SLOT(emitSendText()));
Q_ASSERT(res);
res = QObject::connect(this, SIGNAL(sendText(QString)), this,
                       SLOT(handleSendText(QString)));
Q_ASSERT(res);
 
// Indicate that the variable res isn't used in the rest
// of the app, to prevent a compiler warning.
Q_UNUSED(res);
 
// Add the button and text area to the container
myContainer->add(myButton);
myContainer->add(myTextArea);
 
// Set the content of the page and display it
root->setContent(myContainer);
app->setScene(root);

As with the equivalent QML code sample, there are several other ways that you could achieve this result, such as creating custom components for each control.

Related resources

Web-based training

 
 

Last modified: 2013-12-21



Got questions about leaving a comment? Get answers from our Disqus FAQ.

comments powered by Disqus