9.3. QApplication and the Event Loop
Interactive Qt applications with GUI have a different control flow from console applications and filter applications
because they are
event-based
, and often multithreaded. Objects are frequently sending messages to each other, making a linear hand-trace through the code rather difficult.
|
When writing event-driven programs, GUI views need to respond to changes in the state of data model objects, so that they can display the most recent information possible.
When a particular subject object changes state, it needs an
indirect
way to alert (and perhaps send additional information to) all the other objects that are listening to state-change events, known as observers. A design pattern that enables such a
message-passing
mechanism is called the
Observer pattern
, sometimes also known as the
Publish-Subscribe pattern.
There are many different
implementations
of this pattern. Some common characteristics that tie them together are
-
They all enable concrete subject classes to be decoupled from concrete observer classes.
-
They all support broadcast-style (one to many) communication.
-
The mechanism used to send information from subjects to observers is completely specified in the subject's base class.
Qt's approach is very different from Java's approach, because signals and slots rely on generated code, while Java just renames observer to listener.
|
The Qt class
QEvent
encapsulates the notion of an event.
QEvent
is the base class for several specific event classes such as
QActionEvent
,
QFileOpenEvent
,
QHoverEvent
,
QInputEvent
,
QMouseEvent
, and so forth.
QEvent
objects can be created by the window system in response to actions of the
user
(e.g.,
QMouseEvent
) at specified time intervals (
QTimerEvent
) or explicitly by an application program. The
type()
member function returns an
enum
that has nearly a hundred specific values that can identify the particular kind of event.
A typical Qt program creates objects, connects them, and then
tells
the application to
exec
()
. At that point, the objects can send information to each other in a variety of ways.
QWidgets
send
QEvents
to other
QObjects
in response to user actions such as mouse clicks and keyboard events. A widget can also respond to events from the window manager such as repaints, resizes, or close events. Furthermore,
QObjects
can transmit information to one another by means of signals and slots.
Each
QWidget
can be specialized to handle keyboard and mouse events in its own way. Some widgets will
emit
a signal in response to receiving an event.
An
event loop
is a program structure that
permits
events to be prioritized, enqueued, and dispatched to objects. Writing an event-based application means implementing a
passive interface
of functions that only get called in response to certain events. The event loop
generally
continues running until a terminating event occurs (e.g., the user clicks on the QUIT button).
Example 9.5 shows a simple application that initiates the event loop by calling
exec()
.
Example 9.5. src/eventloop/main.cpp
[ . . . . ]
int main(int argc, char * argv[]) {
QApplication myapp(argc, argv);
<-- 1
QWidget rootWidget;
setGui(&rootWidget);
rootWidget.show();
<-- 2
return myapp.exec();
<-- 3
};
(1)
Every GUI, multithreaded, or event-driven Qt Application must have a QApplication object defined at the top of main().
(2)
Show our widget on the screen.
(3)
Enter the event loop.
|
When we run this app, we first see a widget on the screen as shown in the following figure.
We can type in the
QTextEdit
on the screen, or click on the Shout button. When Shout is clicked, a widget is superimposed on our original widget as shown in the
next
figure.
This message dialog
knows
how to self-destruct, because it has its own
buttons
and actions.
9.3.1. Layouts: A First Look
Whenever more than a single widget needs to be displayed, they must be arranged in some form of a
layout
(see Section 11.5). Layouts are derived from the abstract base class,
QLayout
, which is derived from
QObject
. Layouts are geometry managers that fit into the composition hierarchy of a graphical interface. Typically, we start with a widget that will contain all of the
parts
of our graphical construction. We select one or more suitable layouts to be children of our main widget (or of one another) and then we add widgets to the layouts.
|
It is important to understand that widgets in a layout are not children of the layoutthey are children of the widget that owns the layout. Only a widget can be the parent of another widget. It may be useful to think of the layout as an older sibling acting as the nanny of its widgets.
|
In Example 9.6, we are laying out widgets in a vertical fashion with
QVBoxLayout
.
Example 9.6. src/eventloop/main.cpp
[ . . . . ]
QWidget* setGui(QWidget *box) {
QLayout* layout = new QVBoxLayout;
box->setLayout(layout);
<-- 1
QTextEdit *te = new QTextEdit;
<-- 2
layout->addWidget(te);
<-- 3
te->setHtml("Some <b>text</b> in the <tt>QTextEdit</tt>"
"edit window <i>please</i>?");
QPushButton *quitButton=new QPushButton("Quit");
layout->addWidget(quitButton);
QPushButton *shoutButton = new QPushButton("Shout");
layout->addWidget(shoutButton);
Messager *msgr = new Messager("This dialog will self-
destruct.", box);
QObject::connect(quitButton, SIGNAL(clicked()),
qApp, SLOT(quit()));
<-- 4
qApp->connect(shoutButton, SIGNAL(clicked()), msgr, SLOT(shout()));
return box;
}
(1)
box is the parent of layout.
(2)
This is the window for qDebug messages.
(3)
te is the child of layout.
(4)
qApp is a global variable that points to the current QApplication object.
|
The widgets are arranged vertically in this layout, from top to bottom, in the order that they were added to the layout.
9.3.2. Connecting to Slots
In Example 9.7, we saw the following connections established:
QObject::connect(quitButton, SIGNAL(clicked()), qApp, SLOT(quit()));
qApp->connect(shoutButton, SIGNAL(clicked()), msgr, SLOT(shout()));
connect()
is actually a static member of
QObject
and can be called with any
QObject
or, as we showed, by means of its class scope resolution operator.
qApp
is a global pointer that points to the currently running
QApplication
.
The second
connect
goes to a
slot
that we declare in Example 9.7.
Example 9.7. src/eventloop/messager.h
#ifndef MESSAGER_H
#define MESSAGER_H
#include <QObject>
#include <QString>
#include <QErrorMessage>
class Messager : public QObject {
Q_OBJECT
public:
Messager (QString msg, QWidget* parent=0);
public slots:
void shout();
private:
QWidget* m_Parent;
QErrorMessage* message;
};
#endif
|
Declaring a member function to be a
slot
enables it to be connected to a
signal
so that it can be called
passively
in response to some event. For its definition, shown in Example 9.8, we have kept things quite simple: the
shout()
function simply pops up a message box on the screen.
Example 9.8. src/eventloop/messager.cpp
#include "messager.h"
Messager::Messager(QString msg, QWidget* parent)
: m_Parent(parent) {
message = new QErrorMessage(parent);
setObjectName(msg);
}
void Messager::shout() {
message->showMessage(objectName());
}
|
9.3.3. Signals and Slots
When the main thread of a C++ program calls
qApp->exec()
, it enters into an event loop, where messages are handled and dispatched. While
qApp
is executing its event loop, it is possible for
QObject
s to send messages to one another.
A
signal
is a message that is presented in a class definition like a
void
function declaration. It has a parameter list but no function body. A signal is part of the interface of a class. It looks like a function but it cannot be calledit must be
emitted
by an object of that class. A signal is implicitly
protected
, and so are all the identifiers that follow it in the class definition until another access
specifier
appears.
A
slot
is a
void
member function. It can be called as a normal member function.
A signal of one object can be
connected
to the slots of one or more
other objects, provided the objects exist and the parameter lists are assignment compatible
from the signal to the slot. The syntax of the connect statement is:
bool QObject::connect(senderqobjptr,
SIGNAL(signalname(argtypelist)),
receiverqobjptr,
SLOT(slotname(argtypelist))
optionalConnectionType);
Any
QObject
that has a signal can emit that signal. This will result in an indirect call to all connected slots.
QWidgets already
emit
signals in response to events, so you only need to make the proper connections to receive those signals. Arguments passed in the
emit
statement are accessible as parameters in the slot function, similar to a function call, except that the call is indirect. The argument list is a way to transmit information from one object to another.
Example 9.9 defines a class that uses signals and slots to transmit a single
int
parameter.
Example 9.9. src/widgets/sliderlcd/sliderlcd.h
[ . . . . ]
class QSlider;
class QLCDNumber;
class LogWindow;
class QErrorMessage;
class SliderLCD : public QMainWindow {
Q_OBJECT
public:
SliderLCD(int minval = -273, int maxval = 360);
void initSliderLCD();
public slots:
void checkValue(int newValue);
void showMessage();
signals:
void toomuch();
private:
int m_Minval, m_Maxval;
LogWindow* m_LogWin;
QErrorMessage *m_ErrorMessage;
QLCDNumber* m_LCD;
QSlider* m_Slider;
};
#endif
[ . . . . ]
|
In Example 9.10, we can see how the widgets are initially created and connected.
Example 9.10. src/widgets/sliderlcd/sliderlcd.cpp
[ . . . . ]
SliderLCD::SliderLCD(int min, int max) : m_Minval(min),
m_Maxval(max) {
initSliderLCD();
}
void SliderLCD::initSliderLCD() {
m_LogWin = new LogWindow();
<-- 1
QDockWidget *logDock = new QDockWidget("Debug Log");
logDock->setWidget(m_LogWin);
logDock->setFeatures(0);
<-- 2
setCentralWidget(logDock);
m_LCD = new QLCDNumber();
m_LCD->setSegmentStyle(QLCDNumber::Filled);
QDockWidget *lcdDock = new QDockWidget("LCD");
lcdDock->setFeatures(QDockWidget::DockWidgetClosable);
<-- 3
lcdDock->setWidget(m_LCD);
addDockWidget(Qt::LeftDockWidgetArea, lcdDock);
m_Slider = new QSlider( Qt::Horizontal);
QDockWidget* sliderDock = new QDockWidget("How cold is it
today?");
sliderDock->setWidget(m_Slider);
sliderDock->setFeatures(QDockWidget::DockWidgetMovable);
/* Can be moved between doc areas */
addDockWidget(Qt::BottomDockWidgetArea, sliderDock);
m_Slider->setRange(m_Minval, m_Maxval);
m_Slider->setValue(0);
m_Slider->setFocusPolicy(Qt::StrongFocus);
m_Slider->setSingleStep(1);
<-- 4
m_Slider->setPageStep(20);
<-- 5
m_Slider->setFocus();
<-- 6
connect(m_Slider, SIGNAL(valueChanged(int)), /*SliderLCD is a
QObject so
connect does not need scope resolution. */
this, SLOT(checkValue(int)));
connect(m_Slider, SIGNAL(valueChanged(int)),
m_LCD, SLOT(display(int)));
connect(this, SIGNAL(toomuch()),
this, SLOT(showMessage()));
<-- 7
m_ErrorMessage = NULL;
}
(1)
a class defined in the
utils
library
(2)
cannot be closed, moved, or floated
(3)
can be closed
(4)
Step each time left or right arrow key is pressed.
(5)
Step each time PageUp/PageDown key is pressed.
(6)
Give the slider focus.
(7)
Normally there is no point in connecting a signal to a slot on the same object, but we do it for demonstration purposes.
|
Only the argument types belong in the
connect
statement; for example, the following is not legal:
connect( button, SIGNAL(valueChanged(int)), lcd, SLOT(setValue(3)))
Example 9.11 defines the two slots, one of which conditionally
emits
another signal.
Example 9.11. src/widgets/sliderlcd/sliderlcd.cpp
[ . . . . ]
void SliderLCD::checkValue(int newValue) {
if (newValue> 120) {
emit toomuch();
<-- 1
}
}
/* This slot is called indirectly via emit because
of the connect */
void SliderLCD::showMessage() {
if (m_ErrorMessage == NULL) {
m_ErrorMessage = new QErrorMessage(this);
}
if (!m_ErrorMessage->isVisible()) {
QString message("Too hot outside! Stay in. ");
m_ErrorMessage->showMessage(message);
<-- 2
}
}
(1)
Emit a signal to
anyone
interested.
(2)
This is a direct call to a slot. It's a member function.
|
Example 9.12 contains client code to test this class.
Example 9.12. src/widgets/sliderlcd/sliderlcd-demo.cpp
#include "sliderlcd.h"
#include <QApplication>
#include <QDebug>
int main(int argc, char ** argv) {
QApplication app(argc, argv);
SliderLCD slcd;
slcd.show();
qDebug() << QString("This is a debug message.");
return app.exec();
}
|
Whenever the
slider
produces a new value, that value is transmitted as an argument from the
valueChanged(int) signal
to the
display(int) slot
of the
lcd
.
|
In single-threaded applications, or in multithreaded applications where the emitting and receiving
QObject
s are in the same thread, signals are sent in a
synchronous
manner. This means the thread blocks (suspends execution) until the code for the slots has completed execution (see Section 12.2).
In multi-threaded applications, where signals are emitted by an object in one thread and received by an object in another, it is possible to have signals queued, or executed in an
asynchronous
way, depending on the optional
Qt::ConnectionType
passed to
connect()
.
|
Exercises: Signals and Slots
|
1.
|
Modify the
sliderlcd
program as
follows
:
-
Make the lcd display show the
temperatures
as hexadecimal integers.
-
Make the lcd display
characters
have a different ("flat") style.
-
Give the slider a vertical orientation.
-
Give the slider and the lcd display more interesting colors.
-
Add a push button that the user can click to switch the lcd display from decimal mode to hexadecimal mode.
-
Make the push button into a toggle that allows the user to switch back and forth between decimal and hexadecimal modes.
|
|
2.
|
Write an application, similar to the one in Section 9.3, but that has four buttons. The first one, labeled Advice, should be connected to a slot that
randomly
selects a piece of text (such as a
fortune
cookie) and displays it in the
QTextEdit
window. The second one, labeled Weather, randomly selects a
sentence
about the weather and displays it in the
QTextEdit
window. The third one, labeled Next Meeting, pops up a message dialog with a randomly generated (fictitious) meeting time and descriptive message in it. The fourth one, labeled Quit, terminates the program. Use signals and slots to connect the button clicks with the appropriate functions.
|
|