The Problem
You've used onClicked on MouseArea — that's a signal handler. But signals in QML go far beyond mouse clicks. Any object can emit signals when anything happens: data arrives, state changes, a timer fires. Signals are the primary way objects communicate in a QML application.
The Naive Approach
Without signals, you'd poll objects for changes or couple them directly:
// Tight coupling: one object reaches into another
Timer {
interval: 100
running: true
repeat: true
onTriggered: {
if (sensor.temperature !== lastTemp) {
display.temperature = sensor.temperature
lastTemp = sensor.temperature
}
}
}This is wasteful (polling), fragile (depends on sensor and display IDs), and doesn't scale.
Signals are like a PA system. The announcer (the signal emitter) doesn't need to know who's listening. Anyone interested tunes in and responds. New listeners can join without the announcer changing anything. This decoupling is what makes signals powerful.
The Idea
A signal is an event that an object emits. Other objects can connect to it with handlers. QML provides two syntaxes:
on<SignalName>— inline handler for signals on a specific object:qmlButton { onClicked: console.log("clicked") }connect()— programmatic connection from anywhere:qmlsomeObject.signalName.connect(handlerFunction)
Let's Build It
First, create a custom component with its own signals:
// ClickBox.qml
import QtQuick
Rectangle {
id: root
// Custom signals
signal clicked(int x, int y)
signal doubleClicked()
signal hoveredChanged(bool hovering)
property string label: "Box"
property color defaultColor: "#4ecdc4"
property color hoverColor: "#45b7d1"
width: 150
height: 100
radius: 8
color: mouseArea.containsMouse ? hoverColor : defaultColor
Text {
text: root.label
anchors.centerIn: parent
color: "white"
font.pixelSize: 16
font.bold: true
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: (mouse) => {
root.clicked(mouse.x, mouse.y)
}
onDoubleClicked: {
root.doubleClicked()
}
onContainsMouseChanged: {
root.hoveredChanged(containsMouse)
}
}
}Now use it in a main file:
// main.qml
import QtQuick
import QtQuick.Window
Window {
width: 500
height: 300
visible: true
title: "Signals"
property int clickCount: 0
Row {
anchors.centerIn: parent
spacing: 20
ClickBox {
id: box1
label: "Box 1"
// Inline signal handlers
onClicked: (x, y) => {
clickCount++
status.text = "Box 1 clicked at (" + x + ", " + y + ")"
}
onDoubleClicked: {
status.text = "Box 1 double-clicked!"
}
}
ClickBox {
id: box2
label: "Box 2"
defaultColor: "#ff6b6b"
hoverColor: "#e05555"
onClicked: {
clickCount++
status.text = "Box 2 clicked"
}
onHoveredChanged: (hovering) => {
if (hovering) status.text = "Hovering over Box 2"
}
}
}
// Status label
Text {
id: status
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
bottomMargin: 20
}
text: "Click a box"
color: "#888"
font.pixelSize: 14
}
// Counter connected via the signal approach
Text {
anchors {
top: parent.top
right: parent.right
topMargin: 10
rightMargin: 10
}
text: "Clicks: " + clickCount
color: "#333"
font.pixelSize: 14
}
}Custom signals are declared with signal:
signal clicked(int x, int y)
signal doubleClicked()
signal hoveredChanged(bool hovering)Each signal can carry typed parameters. The handler name follows the pattern on<SignalName> with the first letter capitalized:
onClicked: (x, y) => { ... }
onDoubleClicked: { ... }
onHoveredChanged: (hovering) => { ... }Signal parameters become handler function parameters. You can name them anything, but the types must match the signal declaration.
Connecting from JavaScript
For dynamic connections, use the connect() method:
// Connnect to a signal programmatically
Component.onCompleted: {
box1.clicked.connect(function(x, y) {
console.log("Dynamic handler: clicked at", x, y)
})
}To disconnect:
var handler = function(x, y) { console.log(x, y) }
box1.clicked.connect(handler)
// Later:
box1.clicked.disconnect(handler)Let's Improve It
Signals can also carry any type using var:
signal dataReceived(var payload)
// Emit with any JavaScript value:
dataReceived({ temperature: 22, humidity: 60 })
// Handle it:
onDataReceived: (payload) => {
tempDisplay.text = payload.temperature
humDisplay.text = payload.humidity
}Signal name clashes. QML types come with built-in signals (clicked, pressed, released on MouseArea, etc.). If you declare a custom signal with the same name as a built-in one, you shadow the built-in. Check the type's documentation before naming custom signals.
Forgetting to emit signals. A signal declaration doesn't fire automatically — you must call signalName() somewhere. If a handler never fires, check that the signal is being emitted.
Case sensitivity. Signal names are case-sensitive. signal DataReady() creates a handler onDataReady, not OnDataready.
Under the Hood
Signals in QML are backed by Qt's meta-object system (QMetaObject). When you declare signal clicked(int x, int y), Qt generates:
- A
clicked(int, int)method that you call to emit the signal - A
clickedsignal in the meta-object system - Automatic generation of
onClickedhandler property
The connect() method wires the signal to any compatible callable (a JavaScript function, another QML signal, or a C++ slot). The meta-object system handles type checking and conversion.
Exercises
- Signals are events that objects emit; handlers respond to them
- Custom signals are declared with the signal keyword, optionally with typed parameters
- Inline handlers use on<SignalName> syntax; dynamic connections use connect()
- Emit a signal by calling it like a function: mySignal(args)