Иерархия объектов Qt часть 1 - Механизм сигналов и слотов, приведение типов и свойства объектов.
Вступление.
В Qt все построено на объектах, главным базовым классом является QObject. Все классы, имеющие сигналы и слоты в Qt унаследованы от него. При множественном наследовании, если в иерархии планируется наличие класса QObject, его имя должно стоять первым в списке базовых классов, иначе метаобъектный компилятор Qt moc (Meta object compiler) будет ругаться. Например:
class Foo : public QObject, public Bar {
//…
};
Так же нужно проследить за тем, что бы только один класс в иерархии наследовал QObject.
QObject содержит в себе поддержку сигналов и слотов, таймера, механизма фильтрации событий, мета-информации, приведений типов и объединения объектов в иерархии.
Поддержка таймера позволяет каждому из производных от QObject классов создавать свои таймеры. Объединение в иерархии позволяет упростить управление памятью, так как родительские объекты автоматически удаляют дочерние, механизм слотов и сигналов позволяет наладить взаимодействие объектов с внешним миром.
Если нужно что бы класс мог посылать сигналы и иметь слоты, в его объявлении должен находится макрос Q_OBJECT, после макроса не нужно ставить точку с запятой. Его наличие сообщает moc о том, что в тело класса нужно включить мета-информацию необходимую для функциональности сигналов и слотов.
Например:
class Foo : public QObject {
Q_OBJECT
public:
//…
//…
};
Препроцессор moc анализирует объявления классов содержащих макрос Q_OBJECT и добавляет в них нужную мета-информацию. Механизм сигналов и слотов объектно ориентирован и является главным при программировании в Qt.
Сигналы.
Начнем с них. Сигнал может иметь произвольное количество аргументов. Определять сигнал нельзя, это делает moc. Описать сигнал можно следующим образом:
class Foo : public QObject {
Q_OBJECT
//…
//…
signals:
void error(const QString& message);
};
В примере выше мы объявили сигнал error имеющий аргумент message. Послать сигнал можно оператором emit. Вообще говоря, называть emit оператором неверно, это ключевое слово сообщает moc о том, что нужно послать сигнал error. Мы можем послать сигнал и передать ему аргумент так:
//….
emit error(“Ошибка”);
//….
Напоминаю, что сигналы реализовывать не надо, это делает moc, он порождает код на стандартном С++, который и заставляет это работать.
Слоты.
Это обработчики сигналов. Объявить слот можно так:
class Bar : public QObject {
Q_OBJECT
//….
public slots:
void handleError(const QString& message)
{
qDebug()<<message;
}
//….
};
Примечание для пользователей Windows: Для того, что бы видеть в консоли вывод функции qDebug() вам нужно добавить в файл проекта следующую строку: CONFIG += console.
В коде выше мы реализовали слот handleError с аргументом message. В отличие от сигналов слоты нужно реализовывать самому. Слоты могут быть public, protected и private. Они так же могут быть вызваны как обычные функции, вообще говоря, с точки зрения программирования слоты это обычные методы класса. Для того, что бы наладить взаимодействие между сигналами и слотами их нужно соединить. Для этого в классе QObject существует функция connect.
bool connect(const QObject* sender,
const char* signal,
const QObject* receiver,
const char* slot,
Qt::ConnectionType type = Qt::AutoConnection);
Где sender это объект который посылает сигнал, signal это сигнал, receiver это объект обработчик, slot это слот, type это тип соединения.
Тип соединения может принимать три значения:
Qt::DirectConnection – сигнал обрабатывается сразу соответствующим слотом.
Qt::QueuedConnection – сигнал преобразуется в событие и ставится в очередь.
Qt::AutoConnection – если sender и receiver в одном потоке то Qt::QueuedConnection, если нет, то Qt::DirectConnection.
Вообще, вряд ли вам придется изменять значение этого параметра, но знать, что это возможно будет полезно.
И так соединение может выглядеть так:
//…
connect(mySender,SIGNAL(error(const QString&)),myReceiver,SLOT(handleError(const QString&)));
//…
Как видно в коде, сигнал должен обрамляться макросом SIGNAL, а слот макросом SLOT. В списке параметров нужно указывать только типы аргументов, имена должны отсутствовать, иначе соединение может не состоятся, о чем Qt выведет предупреждение на консоль. Если на месте myReceiver должно стоять слово this, его можно опустить и передать функции connect три аргумента. Так же существует возможность отменить соединение сигнала и слота путем вызова QObject::disconnect.
bool QObject::disconnect ( const QObject * sender, const char * signal, const QObject *receiver, const char * method )
Она полностью аналогична функции connect, за исключением того, что вместо соединения эта функция выполняет разъединение. И так пора уже написать пример.
У нас будет класс со слотом showMessage() который будет выводить сообщение по нажатию на кнопку.
Файл Foo.h
#ifndef FOO_H_
#define FOO_H_
#include <QObject>
//Для поддержки сигналов и слотов наследуем QObject
class Foo: public QObject {
//Наличе макроса обязательно
Q_OBJECT
public:
//Конструктор
Foo(QObject* parent = 0);
public slots:
//Наш слот
void showMessage();
};
#endif /* FOO_H_ */
Файл Foo.cpp
#include "Foo.h"
#include <QMessageBox>
Foo::Foo(QObject* parent)
:QObject(parent)
{
}
void Foo::showMessage()
{
//О том, как работает класс QMessageBox см. QtAssistaint
QMessageBox::information(0,"Message","You press button",QMessageBox::Yes);
}
#include <QtGui>
#include <QApplication>
#include <QPushButton>
#include "Foo.h"
Файл main.cpp
int main(int argc, char *argv[]) {
//Инициализируем каркас приложения,это нужно сделать
//ДО манипуляций с виджетами
QApplication a(argc, argv);
//Создаем кнопку
QPushButton button("Press me!");
//Наш класс
Foo foo;
//Соединяем сигнал кнопки clicked(),который посылатся кнопкой
//когда на нее нажимают
QObject::connect(&button,SIGNAL(clicked()),&foo,SLOT(showMessage()));
//Отображаем кнопку
button.show();
return a.exec();
}
Разобраться в том, что тут происходит должны помочь комментарии. При запуске приложения вы должны увидеть окно с кнопкой, нажав на нее вы увидите сообщение.
Свойства.
Так же QObject обеспечивает поддержку свойств. Свойства это поля объекта которые можно читать и писать. Этот механизм используется QtDesigner и QtScript. Свойство можно определить при помощи макроса Q_PROPERTY();
Например:
// Как обычно в квадратных скобках указаны необязательные поля.
Q_PROPERTY (
// Тип и имя свойства
Type name
// Функция чтения
READ getFunc
// Функция записи
[WRITE setFunc]
// Функция сброса свойства
[RESET resetFunc]
// Указывает отображается ли это свойство в QtDesigner
[DESIGNABLE bool]
// Указывает доступно ли это свойство из QtScript
[SCRIPTABLE bool]
// Указывает запоминается ли свойство при сохранении объекта
[STORED bool]
)
Теперь напишем класс обладающий поддержкой сигналов и слотов, и имеющий так же свойство good – хороший.
class Foo : public QObject {
Q_OBJECT
Q_PROPERTY(bool good, READ getName(), WRITE setName())
// После макросов НЕТ точки с запятой
public:
Foo (QObject* parent = 0) : QObject(parent),
good(true)
{
}
void setGood(bool flag)
{
good = flag;
}
bool getGood() const
{
return good;
}
private:
bool good;
};
Выше мы определили класс имеющий свойство good. Как видно из кода функция чтения свойства это getGood и поэтому она объявлена в макросе Q_PROPERTY с квалификатором READ, функция записи setGood и соответственно она объявляется с квалификатором WRITE. Теперь мы программно можем изменять свойства объекта например так:
//….
if (!foo->getProperty(“good”).toBool())
foo->setProperty(“good”,true);
Заключение.
Приведения типов и объединение объектов в иерархии мы рассмотрим в следующей статье. Остальные темы слишком объемны и будут рассматриваться отдельно.