Как стать автором
Обновить

Сигналы и слоты в Qt

Время на прочтение 10 мин
Количество просмотров 267K
Автор оригинала: Qt Software
Сигналы и слоты используются для коммуникации между объектами. Механизм сигналов и слотов главная особенность Qt и вероятно та часть, которая отличаетcя от особенностей, предоставляемых другими фреймворками.

Введение


В программировании графического интерфейса, когда мы меняем один виджет, мы часто хотим что бы другой виджет получил об этом уведомление. В общем случае, мы хотим что бы объекты любого типа могла общаться с другими. Например, если пользователь нажимает кнопку Закрыть, мы вероятно хотим что бы была вызвана функция окна close().
Другие библиотеки добиваются такого рода общения используя обратный вызов. Обратный вызов это указатель на функцию, таким образом, если мы хотим что бы функция уведомила нас о каких-нибудь событиях, мы передаем указатель на другую функцию (обратновызываемую) этой функции. Функция в таком случае делает обратный вызов когда необходимо. Обратный вызов имеет два основных недостатка. Во-первых, он не является типобезопасным. Мы никогда не можем быть уверены что функция делает обратный вызов с корректными аргументами. Во-вторых, обратный вызов жестко связан с вызывающей его функцией, так как эта функция должна точно знать какой обратный вызов надо делать.

Сигналы и слоты


В Qt используется другая техника — сигналы и слоты. Сигнал вырабатывается когда происходит определенное событие. Слот это функция, которая вызывается в ответ на определенный сигнал. Виджеты Qt имеют много предопределенных сигналов и слотов, но мы всегда можем сделать дочерний класс и добавить наши сигналы и слоты в нем.

Сигналы и слоты

Механизм сигналов и слотов типобезопасен. Сигнатура сигнала должна совпадать с сигнатурой слота-получателя. (Фактически слот может иметь более короткую сигнатуру чем сигнал который он получает, так как он может игнорировать дополнительные аргументы). Так как сигнатуры сравнимы, компилятор может помочь нам обнаружить несовпадение типов. Сигналы и слоты слабо связаны. Класс, который вырабатывает сигнал не знает и не заботится о том, какие слоты его получат. Механизм сигналов и слотов Qt гарантирует, что если мы подключим сигнал к слоту, слот будет вызван с параметрами сигнала в нужное время. Сигналы и слоты могут принимать любое число аргументов любого типа. Они полностью типобезопасны.
Все классы, наследуемые от QObject или его дочерних классов (например, QWidget) могут содержать сигналы и слоты. Сигналы вырабатываются объектами когда они изменяют свое состояние так, что это может заинтересовать другие объекты. При этом он на знает и не заботится о том что у его сигнала может не быть получателя.
Слоты могут быть использованы для получения сигналов, но они так же нормальные функции-члены. Так же как объект не знает ничего о получателях своих сигналов, слот ничего не знает о сигналах, которые к нему подключены. Это гарантирует что полностью независимые компоненты могут быть созданы с помощью Qt.
Мы можем подключать к одному слоту столько сигналов, сколько захотим, также один сигнал может быть подключен к стольким слотам, сколько необходимо. Так же возможно подключать сигнал к другому сигналу (это вызовет выработку второго сигнала немедленно после появления первого).
Сигналы и слоты вместе составляют мощный механизм создания компонентов.

Небольшой пример


Описание класса на C++ может выглядеть вот так:
  1. class Counter
  2. {
  3. public:
  4.    Counter() { m_value = 0; }
  5.    int value() const { return m_value; }
  6.    void setValue(int value);
  7. private:
  8.    int m_value;
  9. };
* This source code was highlighted with Source Code Highlighter.

Класс, наследуемый от QObject будет выглядеть следующим образом:
  1. #include <QObject>
  2. class Counter : public QObject
  3. {
  4.    Q_OBJECT
  5. public:
  6.    Counter() { m_value = 0; }
  7.    int value() const { return m_value; }
  8. public slots:
  9.    void setValue(int value);
  10. signals:
  11.    void valueChanged(int newValue);
  12. private:
  13.    int m_value;
  14. };
* This source code was highlighted with Source Code Highlighter.

Класс, наследованный от QObject имеет то же самое внутреннее состояние и обеспечивает публичные методы для доступа к этому состоянию, но дополнительно у него есть поддержка для использования сигналов и слотов. Этот класс может сообщить внешнему миру что его состояние изменилось выработав сигнал valueChanged() и у него есть слот, в который другие объекты могут посылать сигналы.
Все классы, содержащие сигналы и слоты должны указывать макрос Q_OBJECT в начале их описания. Они также должны быть потомками (прямо или косвенно) QObject.
Слоты реализуются программистом. Возможная реализация слота Counter::setValue() выглядит следующим образом:
  1. void Counter::setValue(int value)
  2. {
  3.    if (value != m_value) {
  4.      m_value = value;
  5.      emit valueChanged(value);
  6.    }
  7. }
* This source code was highlighted with Source Code Highlighter.

Ключевое слово emit вырабатывает сигнал valueChanged() объекта с новым значением в качестве аргумента.
В следующем примере мы создаем два объекта типа Counter и соединяем сигнал valueChanged() первого со слотом setValue() второго используя статическую функцию QObject::connect():
  1.    Counter a, b;
  2.    QObject::connect(&a, SIGNAL(valueChanged(int)),
  3.            &b, SLOT(setValue(int)));
  4.    a.setValue(12);   // a.value() == 12, b.value() == 12
  5.    b.setValue(48);   // a.value() == 12, b.value() == 48
* This source code was highlighted with Source Code Highlighter.

Вызов a.setValue(12) вырабатывает сигнал valueChanged(12), который получит объект b в свой слот setValue() slot, т.е. будет вызвана функция b.setValue(12). Тогда b вырабатывает такой же сигнал valueChanged(), но так как он не подключен ни к одному слоту, это сигнал будет проигнорирован.
Отмечу что функция setValue() устанавливает новое значение и вырабатывает сигнал только есть value != m_value. Это предотвращает бесконечный цикл в случае кругового соединения (например, если бы b.valueChanged() был бы подключен к a.setValue()).
Сигнал вырабатывается для каждого соединения. Если соединение продублировать, два сигнала будут выработаны. Соединение всегда можно разорвать использовав функцию QObject::disconnect().
Приведенный выше пример показывает как объекты могут работать вместе без необходимости знать что-либо друг о друге. Что бы задействовать это, объекты должны быть соединены вместе и это может быть достигнуто простым вызовом функции QObject::connect() или с помощью свойства автоматического соединения программы uic.

Компилирование примера


Мета-объектный компилятор (meta-object compiler, moc) просматривает описание классов в файлах исходных кодов и генерирует код на C++, который инициализирует мета-объекты. Мета-объекты содержат имена все сигналов и слотов, так же как и указатели на эти функции.
Запуская программу moc для описания класса, содержащего сигналы и слоты, мы получаем файл исходных кодов, который должен быть скомпилирован и слинкован с другими объектными файлами приложения. При использовании qmake, правила для автоматического вызова moc будут добавлены в Makefile проекта.

Сигналы


Сигналы вырабатываются объектами когда они изменяют свое состояние так, что это может заинтересовать другие объекты. Только класс, который определяет сигнал или его потомки могут вырабатывать сигнал.
Когда сигнал вырабатывается, слот, к которому он подключен обычно выполняется немедленно, так же как и нормальный вызов процедуры. Когда это происходит, механизм сигналов и сигналов и слотов полностью независим от любого цикла событий графического интерфейса. Выполнение кода, следующего за выпуском сигнала произойдет сразу после выхода из всех слотов. Ситуация слегка отличается когда используются отложенные соединения (queued connections); в этом случае код после ключевого слова emit продолжает выполнение немедленно, а слоты будут выполнены позже.
Если несколько слотов подключены к одному сигналу, слоты будут выполнены один за другим в произвольном порядке после выработки сигнала.
Сигналы автоматически генерируются программой moc и не должны быть реализованы в исходном коде. Они могут не возвращать значение (т. е., используем тип void).
Замечание по поводу аргументов: опыт показывает, что сигналы и слоты легче повторно использовать при написании программ, если они не используют специальных типов. Например, если бы сигнал QScrollBar::valueChanged() использовал бы специальный тип вроде гипотетического QScrollBar::Range, он мог бы быть подключенным только к слотам, спроектированным специально для него.

Слоты


Слот вызывается когда вырабатывается сигнал, с которым он связан. Слот это обычная функция в C++ и может вызываться обычным способом; единственная его особенность, что с ним можно соединсять сигналы.
Так как слоты это нормальные функции-члены, они следуют обычным правилам C++ при прямом вызове. Тем не менее, как слоты, они могут быть вызваны любым компонентом, независимо от их уровней доступа, через соединение сигнал-слот. Это значит, что сигнал, выработаный объектом произвольного класса может вызвать защищенный (private) слот объекта несвязанного с ним класса.
Слоты так же можно объявлять виртуальными, что иногда бывает довольно удобно.
По сравнению с обратными вызовами, сигналы и слоты слегка медленнее из-за увеличенной гибкости, которую они обеспечивают, хотя разница для реальных приложений незаметна. В общем, выработка сигнала, который подключен к некоторым слотам, в среднем в 10 раз медленнее, чем вызов получателя напрямую, при вызове не виртуальной функции. Эти накладные расходы требуются для нахождения объекта, для безопасного перебора всех его соединений (т. е. проверка что последующий получатель не был уничтожен во время выпуска сигнала) и передачи любых параметров в общем виде. Хотя вызов десяти невиртуальных процедур может показаться дорогим, это менее затратно, чем, например, операция создания или удаления объекта. Пока мы создаем строку, вектор или список, что неявно требует создание объекта, затраты сигналов и слотов отвечают за очень маленькую долю в затратах среди всех вызовов процедур.
То же самое верно делаете ли вы системный вызов в слот или косвенно вызываете более десяти функций. На i586-500, мы можем вырабатывать около 2,000,000 сигналов в секунду, соединенных с одним слотом или 1,200,000 в секунду, при соединении в двумя слотами. Простота и гибкость механизма сигналов и слотов окупает дополнительные затраты, которые пользователь программы даже не заметит.
Следует заметить, что библиотеки, которые определяют переменные с именами signal или slot, могут вызывать предупреждения или ошибки компилятора при компиляции вместе с программой, написанной на Qt. Что бы решить данную проблему, необходимо убрать определение мешающегося символа препроцессора с помощью директивы #undef.

Метаобъектная информация


Метаобъект содержит дополнительную информацию, такую как имя объекта. Можно так же проверить наследует ли объект определенный класс, например:
  1.    if (widget->inherits("QAbstractButton")) {
  2.      QAbstractButton *button = static_cast<QAbstractButton *>(widget);
  3.      button->toggle();
  4.    }
* This source code was highlighted with Source Code Highlighter.

Метаобъектная информация также испльзуется qobject_cast<T>(), который похож на QObject::inherits(), но менее предрасположен к ошибкам:
  1.    if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))
  2.      button->toggle();
* This source code was highlighted with Source Code Highlighter.


Реальный пример


Ниже приведен простой пример виджета с комментариями.
  1. #ifndef LCDNUMBER_H
  2. #define LCDNUMBER_H
  3. #include <QFrame>
  4. class LcdNumber : public QFrame
  5. {
  6.    Q_OBJECT
* This source code was highlighted with Source Code Highlighter.

Класс LcdNumber наследует QObject, который обладает большинством информации о сигналах и слотах через классы QFrame и QWidget. Он похож на встроенный виджет QLCDNumber.
Макрос Q_OBJECT указывает препроцессору объявить несколько функций-членов, которые будут реализованы программой moc; если при компилировании среди прочих будет появляется запись «undefined reference to vtable for LcdNumber», то скорее всего забыли запустить moc или добавить результат его работы в команду линковки.
  1. public:
  2.    LcdNumber(QWidget *parent = 0);
* This source code was highlighted with Source Code Highlighter.

Это не явно относится к moc'у, но если мы наследуем класс Qwidget, мы скорее всего захотим иметь аргумент parent (родитель) в конструкторе и передавать его конструктору родительского класса.
Некоторые деструкторы и функции-члены опущены здесь; moc игнорирует функции-члены.
  1. signals:
  2.    void overflow();
* This source code was highlighted with Source Code Highlighter.

LcdNumber вырабатывает сигнал когда его просят показать невозможное значение.
Если мы не заботимся о переполнении или знаем что оно не может произойти, мы может игнорировать этот сигнал, т.е. никуда его не подключать.
С другой стороны, если мы захотим вызвать две разные функции для реакции на эту ошибку, тогда просто подключаем эту функцию к двум разным слотам. Qt вызовет их оба (в произвольном порядке).
  1. public slots:
  2.    void display(int num);
  3.    void display(double num);
  4.    void display(const QString &str);
  5.    void setHexMode();
  6.    void setDecMode();
  7.    void setOctMode();
  8.    void setBinMode();
  9.    void setSmallDecimalPoint(bool point);
  10. };
  11. #endif
* This source code was highlighted with Source Code Highlighter.

Слоты это функции, используемые для получения информации об изменениях состояний других виджетов. LcdNumber использует их, как показано в коде выше, для установки отображаемого числа. Так как функция display() часть интерфейса класса с остальной программой, этот слот публичный.
Стоит отметить что функция display() перегружена. Qt выберет подходящую версию при соединении сигнала и слота. С обратным вызовом нам бы пришлось искать пять разных имен и контролировать типы самостоятельно.
Некоторые незначительные функции-члены были опущены в данном примере.

Продвинутое использование сигналов и слотов


В некоторых случаях может потребоваться информация об отправителе сигнала. Qt предоставляет функцию Qobject::sender(), которая возвращает указатель на объект, пославший сигнал.
Класс QSignalMapper необходим в ситуациях, когда много сигналов подключены к одному и тому же слоту, и этот слот должен реагировать на каждый сигнал по-разному.
Предположим что у нас есть три кнопки, которые определяют, какой файл мы хотим открыть: «Tax File», «Accounts File», or «Report File».
Что бы открыть нужный файл мы соединяем их сигнал QPushButton::clicked() со слотом readFile(). Теперь используем функцию класса QSignalMapper — setMapping() — для преобразования всех сигналов в объект QSignalMapper.
  1.    signalMapper = new QSignalMapper(this);
  2.    signalMapper->setMapping(taxFileButton, QString("taxfile.txt"));
  3.    signalMapper->setMapping(accountFileButton, QString("accountsfile.txt"));
  4.    signalMapper->setMapping(reportFileButton, QString("reportfile.txt"));
  5.    connect(taxFileButton, SIGNAL(clicked()),
  6.      signalMapper, SLOT (map()));
  7.    connect(accountFileButton, SIGNAL(clicked()),
  8.      signalMapper, SLOT (map()));
  9.    connect(reportFileButton, SIGNAL(clicked()),
  10.      signalMapper, SLOT (map()));
* This source code was highlighted with Source Code Highlighter.

Теперь подключаем сигнал mapped() к слоту readFile() в котором разные файлы будут открыты в зависимости от нажатой кнопки.
  1.   connect(signalMapper, SIGNAL(mapped(const QString &)),
  2.      this, SLOT(readFile(const QString &)));
* This source code was highlighted with Source Code Highlighter.


Использование Qt со сторонними сигналами и слотами


Можно использовать Qt со сторонним механизмом сигналов и слотов. Можно использовать несколько механизмов в одном проекте. Для этого надо добавить следующую строку в файл проекта (.pro):
CONFIG += no_keywords

Эта опция говорит Qt не определять ключевые слова moc'a — signals, slots, и emit, так как эти имена будут использованы строронней библиотекой, например, Boost. Что бы использовать сигналы и слоты Qt с установленным флагом no_keywords, надо просто заменить все использования ключевых слов moc'а Qt в исходных файлах на соотствующие макросы — Q_SIGNALS, Q_SLOTS, и Q_EMIT.
Теги:
Хабы:
+38
Комментарии 24
Комментарии Комментарии 24

Публикации

Истории

Работа

QT разработчик
13 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн