- Game Programming Using Qt Beginner's Guide
- Witold Wysota Lorenz Haas
- 1273字
- 2025-04-04 20:19:16
Qt meta-objects
Most of the special functionality that Qt offers revolves around the QObject
class and the meta-object paradigm that we will take a closer look at now. The paradigm says that with every QObject
subclass, there is a special object associated that contains information about that class. It allows us to make runtime queries to learn useful things about the class—the class name, superclass, constructors, methods, fields, enumerations, and so on. The meta-object is generated for the class at compile time when three conditions are met:
- The class is a descendant of
QObject
- It contains a special
Q_OBJECT
macro in a private section of its definition - Code of the class is preprocessed by a special Meta-Object Compiler (moc) tool
We can comply to the first two conditions ourselves by writing proper code for the class just like Qt Creator does when we create a class derived from QObject
. The last condition is met automatically when you use a tool chain that comes with Qt (and Qt Creator) to build your project. Then, it is enough to make sure that the file containing the class definition is added to the HEADERS
variable of the project file and Qt will take care of the rest. What really happens is that moc generates some code for us that is later compiled in the main program.
All features discussed in this section of the chapter require a meta-object for the class. Therefore, it is essential to make sure that the three conditions I mentioned are met if you want a class to use any of those features.
Signals and slots
To trigger functionality as a response to something that happens in an application, Qt uses a mechanism of signals and slots. This is based on connecting a notification (which we call a signal) about a change of state in some object with a function or method (called a slot) that is executed when such a notification arises.
Signals and slots can be used with all classes that inherit QObject
. A signal can be connected to a slot, member function, or functor (which includes a regular global function). When an object emits a signal, any of these entities that are connected to that signal will be called. A signal can also be connected to another signal in which case, emitting the first signal will make the other signal be emitted as well. You can connect any number of slots to a single signal and any number of signals to a single slot.
A signal slot connection is defined by the following four attributes:
- An object that changes its state (sender)
- A signal in the sender object
- An object that contains the function to be called (receiver)
- A slot in the receiver
To declare a signal, we put its declaration, that is, a regular member function declaration in a special class scope called signals. However, we don't implement such a function—this will be done automatically by moc. To declare a slot, we put the declaration in the class scope of either public slots, protected slots, or private slots. Slots are regular methods and can be called directly in code just like any other method. Contrary to signals, we need to provide bodies for slot methods.
A sample class implementing some signals and slots looks like as shown in the following code:
class ObjectWithSignalsAndSlots : public QObject { Q_OBJECT public: ObjectWithSignalsAndSlots(QObject *parent = 0) : QObject(parent) { } public slots: void setValue(int v) { … } void setColor(QColor c) { … } private slots: void doSomethingPrivate(); signals: void valueChanged(int); void colorChanged(QColor); }; void ObjectWithSignalsAndSlots::doSomethingPrivate() { // … }
Signals and slots can be connected and disconnected dynamically using the connect()
and disconnect()
statements.
The classic connect
statement looks as follows:
connect(spinBox, SIGNAL(valueChanged(int)), dial, SLOT(setValue(int)));
This statement establishes a connection between SIGNAL
of the spinBox
object called valueChanged
that carries an int
parameter and a setValue
slot in the dial
object that accepts an int
parameter. It is forbidden to put variable names or values in a connect
statement. You can only make a connection between a signal and slot that have matching signatures, which means that they accept the same types of arguments (any type casts are not allowed, and type names have to match exactly) with the exception that the slot can omit an arbitrary number of last arguments. Therefore, the following connect
statement is valid:
connect(spinBox, SIGNAL(valueChanged(int)), lineEdit, SLOT(clear()));
This is because the parameter of the valueChanged
signal can be discarded before clear
is called. However, the following statement is invalid:
connect(button, SIGNAL(clicked()), lineEdit, SLOT(setText(QString)));
There is nowhere to get the value that is to be passed to setText
, so such a connection will fail.
Tip
It is important that you wrap signal and slot signatures into the SIGNAL
and SLOT
macros and that when you specify signatures, you only pass argument types and not values or variable names. Otherwise, the connection will fail.
Since Qt 5, there are a couple of different connect syntax available that don't require a meta-object for the class implementing the slot. The QObject
legacy is still a requirement though, and the meta-object is still required for the class that emits the signal.
The first additional syntax that we can use is the one where we pass a pointer to the signal method and a pointer to the slot method instead of wrapping signatures in the SIGNAL
and SLOT
macros:
connect(button, &QPushButton::clicked, lineEdit, &QLineEdit::clear);
In this situation, the slot can be any member function of any QObject
subclass that has argument types that match the signal or such that can be converted to match the signal. This means that you can, for example, connect a signal carrying a double value with a slot taking an int parameter:
class MyClass : public QObject {
Q_OBJECT
public:
MyClass(QObject *parent = 0) : QObject(parent) {
connect(this, &MyClass::somethingHappened, this, &MyClass::setValue);
}
void setValue(int v) { … }
signals:
void somethingHappened(double);
};
Tip
An important aspect is that you cannot freely mix meta-object-based and function-pointer-based approaches. If you decide to use pointers to member methods in a particular connection, you have to do that for both the signal and the slot.
We can even go a step further and have a signal connected to a standalone function:
connect(button, &QPushButton::clicked, &someFunction);
If you use C++11, the function can also be a lambda expression in which case, it is possible to write the body of the slot directly in the connect
statement:
connect(pushButton, SIGNAL(clicked()), []() { std::cout << "clicked!" << std::endl; });
It is especially useful if you want to invoke a slot with a fixed argument value that can't be carried by a signal because it has less arguments. A solution is to invoke the slot from a lambda function (or a standalone function):
connect(pushButton, SIGNAL(clicked()), [label]() { label->setText("button was clicked"); });
A function can even be replaced with a function object (functor). To do this, we create a class for which we overload the call operator that is compatible with the signal that we wish to connect to, as shown in the following snippet:
class Functor { public: Functor(Object *object, const QString &str) : m_object(object), m_str(str) {} void operator()(int x, int y) const { m_object->set(x, y, m_str); } private: Object *m_object; QString m_str; }; connect(obj1, SIGNAL(coordChanged(int, int)), Functor("Some Text"));
This is often a nice way to execute a slot with an additional parameter that is not carried by the signal, as this is much cleaner than using a lambda expression.
There are some aspects of signals and slots that we have not covered here. We will come back to them later when we deal with multithreading.
Pop quiz – making signal-slot connections
Q1. For which of the following do you have to provide your own implementation?
- A signal
- A slot
- Both
Q2. Which of the following statements are valid?
connect(sender, SIGNAL(textEdited(QString)), receiver, SLOT(setText("foo")))
connect(sender, SIGNAL(toggled(bool)), receiver, SLOT(clear()));
connect(sender, SIGNAL(valueChanged(7)), receiver, SLOT(setValue(int)));
connect(sender, &QPushButton::clicked, receiver, &QLineEdit::clear);