Как вызвать слот из другого виджета
БлогNot. QT: простой обмен сигналами между двумя классами
QT: простой обмен сигналами между двумя классами
Всё основное о сигналах и слотах в QT сказано в этой заметке, и там же приведён пример приложения. Этот пример, наверное, ещё проще и возник как ответ на вопрос о том, как организовать обмен сигналами между виджетом и неким объектом, представленным отдельным классом.
Создадим виджет с формой на основе класса QWidget действуя так же, как в этой статье.
Затем добавим в проект класс MyObject , являющийся потомком базового класса Object . Для этого нажмём правую кнопку мыши на выделенном жирным шрифтом имени проекта, выберем пункт меню Добавить новый. затем слева укажем язык C++, справа — пункт списка C++ Class, нажмём кнопку Выбрать. в новом окне введём имя класса MyObject и выберем из списка базовый класс QObject .
Осталось нажать Далее и Завершить, пустой класс, готовый отправлять и принимать сигналы, создан.
Предусмотрим отправку из виджета сигнала некоторому объекту (другому классу) и слот для приёма информации от объекта. Для этого изменим файл widget.h следующим образом:
#ifndef WIDGET_H #define WIDGET_H #include #include "myobject.h" //заголовочный файл класс объекта namespace Ui < class Widget; >class Widget : public QWidget < Q_OBJECT public: explicit Widget(QWidget *parent = 0); ~Widget(); private: Ui::Widget *ui; MyObject Object; //Объект, с которым будем работать signals: void send_to_object(bool); //будем отправлять величину типа bool private slots: void get_from_object(bool); //и принимать такую же >; #endif // WIDGET_H
Суть механизма сигналов и слотов состоит в том, что виджет может использовать интерфейс сигналов и слотов других виджетов, не вникая в детали их реализации, не заботясь об очередности обработки сигналов и даже «не зная», будет ли сигнал обработан. Объект просто посылает «внешнему миру» сигнал о том, что его состояние изменилось и другие объекты могут это учесть. Заставим наш виджет посылать сигнал классу MyObject , а последнему укажем принимать его в ещё не написанный слот get_from_gui .
Осуществим и обратную связь. Виджет будет принимать в слот get_from_object сигнал send_to_gui , отправленный из объекта Object .
Соединять сигналы со слотами можно в любой точке приложения, для удобства сделаем это в конструкторе виджета, изменив файл widget.cpp следующим образом:
#include «widget.h» #include «ui_widget.h» Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) < ui->setupUi(this); connect(this,SIGNAL(send_to_object(bool)),&Object,SLOT(get_from_gui(bool))); //Соединение 1 — от виджета к объекту connect(&Object,SIGNAL(send_to_gui(bool)),this,SLOT(get_from_object(bool))); //Соединение 2 — от объекта к виджету > Widget::~Widget()
Ниже этого кода обеспечим реализацию слота виджета.
Чтобы отображать величину типа bool , полученную от объекта, откроем двойным щелчком форму widget.ui и перетащим на неё однострочное тектовое поле lineEdit . Также добавим стандартный переключатель checkBox .

форма виджета
Горячей клавишей Ctrl+W закроем форму и подтвердим её сохранение.
Теперь можно добавить в widget.cpp слот, который будет менять текст, выведенный в lineEdit в зависимости от того, истинная или ложная величина ему передана.
void Widget::get_from_object(bool value) < ui->lineEdit->setText(value==true?tr("true"):tr("false")); >
Перейдём к написанию класса объекта. В файле myobject.h опишем сигнал и слот объекта, а также предусмотрим вектор из величин типа bool , хранящий последовательность состояний объекта.
#ifndef MYOBJECT_H #define MYOBJECT_H #include #include class MyObject : public QObject < Q_OBJECT public: explicit MyObject(QObject *parent = 0); signals: void send_to_gui(bool); private slots: void get_from_gui(bool); private: //данные класса QVector state; >; #endif // MYOBJECT_H
В файле myobject.cpp достаточно будет реализовать код слота.
void MyObject::get_from_gui(bool value) < state->append(value); //запомнить новое состояние emit send_to_gui(value); //и отправить виджету сигнал >
Виджет тоже должен когда-то отправлять сигнал, пусть это будет происходить при изменении состояния чекбокса. Когда оно меняется, чекбокс отправляет встроенный в него сигнал clicked()
Дважды щёлкнем по форме widget.ui , затем нажмём правую кнопку на чекбоксе, щёлкнем пункт меню Перейти к слоту. выберем из списка clicked() и нажмём ОК.

создание слота в режиме дизайна формы
В widget.cpp добавился ещё один слот, запрограммируем его:
void Widget::on_checkBox_clicked() < emit send_to_object(ui->checkBox->isChecked()); >
Объекту отправляется сигнал о текущем состоянии чекбокса, которое он заносит в свой список state .
Теперь виджет работает и обменивается сигналами с объектом.
03.03.2018, 10:07 [6871 просмотр]
Отображение одного QWidget над другим
Потребность в расположении одного виджета над другим может возникнуть, если вы хотите использовать нестандартные дизайнерские приемы или пытаетесь сэкономить место в насыщенном графическом интерфейсе. У этого решения есть свои преимущества и недостатки. С одной стороны, это позволит использовать более гибкие способы компоновки виджетов, но может запутать пользователя, который не знаком с ходом ваших мыслей. Также такой интерфейс будет сложнее реализовать и сопровождать, поскольку всю работу, которую иначе проделал бы менеджер компоновки QLayout , придется делать вручную. Поэтому лучше выбрать какой-то компромисс и пользоваться представленным в заметке приемом лишь в редких случаях и только тогда, когда это реально необходимо.
Пример расположения одного виджета поверх другого
В качестве примера мы реализуем приложение, способное выводить подсказки по аналогии с Toasts на Android, которые выглядят следующим образом:
В нижней части скриншота мы видим небольшое поле с текстом. Оно появляется на экране поверх остальных элементов интерфейса на некоторый промежуток времени и само исчезает. Его удобно использовать для вывода коротких сообщений и уведомлений. В отличие от диалоговых окон, пользователь со стандартным Toast взаимодействовать не может. Для Android предусмотрен специальный класс, который позволяет выводить такие уведомления. Но в QtSDK на момент написания заметки подобный функционал не предусмотрен. Попробуем исправить это.
Сразу скажу, что расположить виджет в произвольной позиции на другом виджете очень легко. Для того, чтобы это сделать, нам достаточно не добавлять дочерний виджет в layout родительского виджета, а просто установить отношение между ними, передав в конструктор дочернего виджета указатель на родительский:
QWidget parent; QWidget* child = new QWidget( &parent );
В этом случае дочерний виджет будет отображаться на родительском, но за его размерами и позицией придется следить нам самим.
Таким образом, мы создадим простое приложение, которое будет выводить уведомления в нижней части главного окна. Чтобы приложение не вышло слишком скучным, раскрасим уведомления в разные цвета. Для этого создадим три кнопки, при нажатии на которые будут выводиться разные уведомления со своим стилем.
Реализация компоновки одного виджета над другим
Начнем с объявления класса:
#include #include class QTextEdit; class QLabel; class ToastDemoWidget : public QWidget < Q_OBJECT public: ToastDemoWidget( QWidget* parent = 0 ); ~ToastDemoWidget(); protected: void resizeEvent( QResizeEvent* ); private slots: void onOk(); void onError(); void onWait(); private: void showToast( const QString& message, const char* const styleClass ); private: QTextEdit* m_edit; QLabel* m_lblToast; QTimer m_timer; >;
Здесь все достаточно очевидно. Создаем обычный виджет, который наследует класс QWidget . Для него переопределяем виртуальную функцию resizeEvent() , чтобы контролировать положение подсказки при изменении размеров окна.
Для отображения самой подсказки будем использовать функцию-член showToast() . Она принимает два параметра: текст сообщения подсказки message и класс стиля styleClass , с которым подсказка должна отобразиться. Пока что на стилях останавливаться не будем, но скоро мы к ним вернемся.
Чтобы наш виджет не вышел совсем пустым, в добавок к кнопкам мы разместим на нем текстовое поле QTextEdit . При этом саму подсказку мы будем отображать с помощью обычного QLabel , который будет исчезать по сигналу таймера QTimer .
А теперь пройдемся по реализации каждой функции и слота. Но, как я и обещал, начнем со стилей:
static const char* const TOAST_STYLESHEET = "QLabel " "[class=Info] " "[class=Ok] " "[class=Error] " "[class=Wait] ";
Конечно, лучше было бы вынести содержимое константы TOAST_STYLESHEET в отдельный файл, но я решил не усложнять реализацию и сделать по-простому. В Qt реализована очень мощная и удобная система управления стилями. Вы можете поменять внешний вид любого стандартного виджета. Есть несколько вариантов добиться этого, но одним из самых удобных является применение языка QSS, который приходится близким родственником CSS. Если вы знакомы с CSS, то особых сложностей с пониманием QSS, возникнуть не должно. В представленном выше фрагменте мы сначала задаем стиль для объектов класса QLabel . Устанавливаем:
- Жирное начертание шрифта;
- Внутренние отступы по 10 пикселей сверху и снизу, и по 15 пикселей слева и справа;
- Сплошную границу толщиной в 2 пикселя;
- Скругленные края для границы с радиусом 10 пикселей.
Далее мы определяем четыре набора стилей для разных значений свойства class . Указать свойство можно для любого объекта QObject с помощью функции-члена setProperty() . Например:
m_lblToast->setProperty( "class", "Info" );
Мы вынуждены идти таким путем, поскольку в QSS не предусмотрены аналоги классов из CSS. Сами же объявления свойств для соответствующих стилей не вызывают затруднений. Мы просто определяем цвет фона, указывая значение rgba , где последняя компонента ( alpha ) указывает на степень прозрачности, а также цвет границы, который лучше подходит выбранному фону. Мы могли бы поставить в качестве фона какие-нибудь подходящие изображения, но это уже выходит за рамки рассматриваемой темы. Возможно, в одной из следующих заметок мы к ней еще вернемся.
Теперь переходим к конструктору родительского виджета:
ToastDemoWidget::ToastDemoWidget( QWidget* parent ) : QWidget( parent ) < QVBoxLayout* mainLayout = new QVBoxLayout; setLayout( mainLayout ); m_edit = new QTextEdit; mainLayout->addWidget( m_edit ); m_lblToast = new QLabel( this ); m_lblToast->setText( trUtf8( "Hello, world!" ) ); m_lblToast->setAlignment( Qt::AlignCenter ); m_lblToast->setWordWrap( true ); m_lblToast->setProperty( "class", "Info" ); m_lblToast->setStyleSheet( TOAST_STYLESHEET ); m_lblToast->adjustSize(); connect( m_edit, SIGNAL( textChanged() ), m_lblToast, SLOT( hide() ) ); connect( m_edit, SIGNAL( textChanged() ), &m_timer, SLOT( stop() ) ); QHBoxLayout* panelLayout = new QHBoxLayout; mainLayout->addLayout( panelLayout ); QPushButton* btnOk = new QPushButton( trUtf8( "Ок" ) ); connect( btnOk, SIGNAL( clicked() ), SLOT( onOk() ) ); panelLayout->addWidget( btnOk ); QPushButton* btnError = new QPushButton( trUtf8( "Ошибка" ) ); connect( btnError, SIGNAL( clicked() ), SLOT( onError() ) ); panelLayout->addWidget( btnError ); QPushButton* btnWait = new QPushButton( trUtf8( "Ожидание" ) ); connect( btnWait, SIGNAL( clicked() ), SLOT( onWait() ) ); panelLayout->addWidget( btnWait ); panelLayout->addStretch( 1 ); m_timer.setSingleShot( true ); connect( &m_timer, SIGNAL( timeout() ), m_lblToast, SLOT( hide() ) ); m_lblToast->raise(); resize( 420, 540 ); >
Важным моментом здесь является определение и инициализация m_lblToast . В его конструктор мы передаем указатель this , чтобы обеспечить привязку к родительскому виджету, но ни в какие layout -ы мы его не добавляем. Далее мы:
- Устанавливаем текст «Hello, world!» ;
- Делаем выравнивание текста по центру;
- Задаем перенос слов для случая длинных строк;
- Определяем значение свойства class , равным Info ;
- Не забываем назначить стиль, который мы заранее подготовили чуть выше;
- Вручную просим подогнать размер под содержимое, иначе виджет окажется слишком маленьким для текста.
Сигнал textChanged() поля m_edit мы подключаем к слотам hide() для m_lblToast и stop() для m_timer . Первое соединение обеспечит скрытие виджета уведомления, если оно было видимым, а второе остановит таймер, если он был запущен.
Затем мы создаем три кнопки и соединяем их последовательно со слотами onOk() , onError() и onWait() .
Таймер мы переводим в режим singleShot , то есть он будет срабатывать после старта всего один раз. При этом, когда он сработает, для m_lblToast будет вызван слот hide() и он исчезнет с экрана.
Строка m_lblToast->raise() нужна для того, чтобы виджет подсказки отображался поверх всех остальных виджетов.
Теперь посмотрим на реализацию функции resizeEvent() , которая будет вызываться при любом изменении размера родительского виджета:
static const double TOAST_CENTER_POSITION_FROM_PARENT_TOP = 0.7; void ToastDemoWidget::resizeEvent( QResizeEvent* ) < int x = ( width() - m_lblToast->width() ) / 2; int y = TOAST_CENTER_POSITION_FROM_PARENT_TOP * height() - m_lblToast->height() / 2; m_lblToast->move( x, y ); >
Координату x мы рассчитываем таким образом, чтобы виджет подсказки был размещен в середине родительского виджета по горизонтали. Для координаты y мы используем относительную позиции, для чего определили константу TOAST_CENTER_POSITION_FROM_PARENT_TOP . Она указывает, насколько середина виджета подсказки должна быть удалена от верха родительского виджета по вертикали. Значение должно быть определено в долях единицы, иначе для слишком больших или маленьких величин виджет подсказки вылезет за пределы родительского виджета. Мы задали значение 0.7 , поэтому он появится в нижней части. А, например, для значения 0.5 мы бы увидели, что подсказка появляется точно в середине родительского виджета.
На этом этапе мы уже можем запустить приложение. И вот что получится:
Слева на скриншоте мы видим наше уведомление «Hello, world» . Но если мы начнем печатать в текстовом поле, то оно сразу же исчезнет, что представлено в правой части скриншота.
Теперь пришло время написать реализацию функции showToast() . Вот что получилось:
static const int TOAST_SHOW_TIME_MSEC = 1500; void ToastDemoWidget::showToast( const QString& message, const char* const styleClass ) < if( m_timer.isActive() ) < return; >m_lblToast->setText( message ); m_lblToast->setProperty( "class", styleClass ); m_lblToast->style()->unpolish( m_lblToast ); m_lblToast->style()->polish( m_lblToast ); m_lblToast->update(); m_lblToast->adjustSize(); m_lblToast->show(); m_timer.start( TOAST_SHOW_TIME_MSEC ); resizeEvent( NULL ); >
Сначала мы проверяем активность статуса. Если он запущен, то какое-то уведомление уже отображается и мы выходим из функции. Иначе мы:
- Устанавливаем переданный текст message в m_lblToast ;
- Задаем значение свойства class для m_lblToast , чтобы применить нужный стиль styleClass , который был передан в функцию;
- Следующие несколько строк заставляет принудительно перерисовать m_lblToast с учетом изменившихся значений свойств;
- Затем мы вновь подгоняем размер под содержимое;
- Отображаем виджет подсказки m_lblToast ;
- Запускаем таймер;
- Вручную вызываем resizeEvent() , чтобы обновить позицию, поскольку размер дочернего виджета изменился для соответствия новому содержимому.
При наличии функции showToast() реализация слотов, привязанных к кнопкам, остается тривиальной:
void ToastDemoWidget::onOk() < showToast( trUtf8( "Все хорошо!" ), "Ok" ); >void ToastDemoWidget::onError() < showToast( trUtf8( "Что-то пошло не так!" ), "Error" ); >void ToastDemoWidget::onWait()
Думаю, что комментарии здесь не требуются. Поэтому приведу окончательную версию кода реализации целиком:
#include #include #include #include #include #include static const char* const TOAST_STYLESHEET = "QLabel " "[class=Info] " "[class=Ok] " "[class=Error] " "[class=Wait] "; static const int TOAST_SHOW_TIME_MSEC = 1500; static const double TOAST_CENTER_POSITION_FROM_PARENT_TOP = 0.7; ToastDemoWidget::ToastDemoWidget( QWidget* parent ) : QWidget( parent ) < QVBoxLayout* mainLayout = new QVBoxLayout; setLayout( mainLayout ); m_edit = new QTextEdit; mainLayout->addWidget( m_edit ); m_lblToast = new QLabel( this ); m_lblToast->setText( trUtf8( "Hello, world!" ) ); m_lblToast->setAlignment( Qt::AlignCenter ); m_lblToast->setWordWrap( true ); m_lblToast->setProperty( "class", "Info" ); m_lblToast->setStyleSheet( TOAST_STYLESHEET ); m_lblToast->adjustSize(); connect( m_edit, SIGNAL( textChanged() ), m_lblToast, SLOT( hide() ) ); connect( m_edit, SIGNAL( textChanged() ), &m_timer, SLOT( stop() ) ); QHBoxLayout* panelLayout = new QHBoxLayout; mainLayout->addLayout( panelLayout ); QPushButton* btnOk = new QPushButton( trUtf8( "Ок" ) ); connect( btnOk, SIGNAL( clicked() ), SLOT( onOk() ) ); panelLayout->addWidget( btnOk ); QPushButton* btnError = new QPushButton( trUtf8( "Ошибка" ) ); connect( btnError, SIGNAL( clicked() ), SLOT( onError() ) ); panelLayout->addWidget( btnError ); QPushButton* btnWait = new QPushButton( trUtf8( "Ожидание" ) ); connect( btnWait, SIGNAL( clicked() ), SLOT( onWait() ) ); panelLayout->addWidget( btnWait ); panelLayout->addStretch( 1 ); m_timer.setSingleShot( true ); connect( &m_timer, SIGNAL( timeout() ), m_lblToast, SLOT( hide() ) ); m_lblToast->raise(); resize( 420, 540 ); > ToastDemoWidget::~ToastDemoWidget() < >void ToastDemoWidget::resizeEvent( QResizeEvent* ) < int x = ( width() - m_lblToast->width() ) / 2; int y = TOAST_CENTER_POSITION_FROM_PARENT_TOP * height() - m_lblToast->height() / 2; m_lblToast->move( x, y ); > void ToastDemoWidget::onOk() < showToast( trUtf8( "Все хорошо!" ), "Ok" ); >void ToastDemoWidget::onError() < showToast( trUtf8( "Что-то пошло не так!" ), "Error" ); >void ToastDemoWidget::onWait() < showToast( trUtf8( "Немного подождите…" ), "Wait" ); >void ToastDemoWidget::showToast( const QString& message, const char* const styleClass ) < if( m_timer.isActive() ) < return; >m_lblToast->setText( message ); m_lblToast->setProperty( "class", styleClass ); m_lblToast->style()->unpolish( m_lblToast ); m_lblToast->style()->polish( m_lblToast ); m_lblToast->update(); m_lblToast->adjustSize(); m_lblToast->show(); m_timer.start( TOAST_SHOW_TIME_MSEC ); resizeEvent( NULL ); >
А вот так выглядит приложение после нажатия разных кнопок, когда в текстовом поле находится копия его же исходного кода:
Заключение
Вот мы и разобрались с тем, как разместить один QWidget над другим. Мы рассмотрели вполне реальный пример использования такой техники, в рамках которого реализовали простой аналог системы уведомлений Toasts , которая есть в Android. Но прежде чем использовать представленную методику, еще раз подумайте. Вполне вероятно, что в вашем случае вполне достаточно стандартных методов компоновки на основе QLayout .
Как вызвать слот из другого виджета
![]()

Просмотр профиля
17.12.2014, 16:44
Группа: Участник
Сообщений: 21
Регистрация: 26.5.2013
Пользователь №: 3841
Репутация: 0
Доброго времени суток.
Подскажите как вызвать слот из класса class MainWindow1, после нажатия кнопки в классе class Addnomenklatura.
#ifndef MAINWINDOW1_H
#define MAINWINDOW1_H
#include
#include
#include "mainwindow.h"
#include "ui_mainwindow.h"
namespace Ui class MainWindow1;
>
class MainWindow1 : public QMainWindow
Q_OBJECT
public:
explicit MainWindow1(QWidget *parent = 0);
~MainWindow1();
private slots:
void on_pushButton_clicked();
void prostoSlot();
void on_pushButton_2_clicked();
private:
Ui::MainWindow1 *ui;
QSqlQueryModel *nomenclatura;
>;
#include "mainwindow1.h"
#include "ui_mainwindow1.h"
#include "Addnomenklatura.h"
#include "ui_Addnomenklatura.h"
#include
#include
#include
#include
#include
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
MainWindow1::MainWindow1(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow1)
ui->setupUi(this);
QSqlQueryModel *nomenclatura = new QSqlQueryModel;
QSqlQuery query;
query.exec("SELECT * FROM nomenklatura");
nomenclatura->setQuery(query);
ui->tableView->setModel(nomenclatura);
>
MainWindow1::~MainWindow1()
delete ui;
>
void MainWindow1::on_pushButton_clicked()
Addnomenklatura *n1 = new Addnomenklatura;
n1->show();
>
void MainWindow1::on_pushButton_2_clicked()
>
void MainWindow1:: prostoSlot() Этот слот нужно вызвать.
QMessageBox::information(NULL,QObject::tr("Информация"),tr("Слот вызван"));
QSqlQueryModel *nomenclatura = new QSqlQueryModel;
QSqlQuery query;
query.exec("SELECT * FROM nomenklatura");
nomenclatura->setQuery(query);
ui->tableView->setModel(nomenclatura);
>
#ifndef ADDNOMENKLATURA_H
#define ADDNOMENKLATURA_H
#include
#include
#include "mainwindow1.h"
#include "ui_mainwindow1.h"
#include "mainwindow.h"
#include "ui_mainwindow.h"
namespace Ui class Addnomenklatura;
>
class Addnomenklatura : public QMainWindow
Q_OBJECT
public:
explicit Addnomenklatura(QWidget *parent = 0);
~Addnomenklatura();
private slots:
void on_pushButton_clicked();
void on_pushButton_2_clicked();
private:
Ui::Addnomenklatura *ui;
>;
#endif // ADDNOMENKLATURA_H
#include "addnomenklatura.h"
#include "ui_addnomenklatura.h"
#include
#include "mainwindow1.h"
#include "ui_mainwindow1.h"
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include
Addnomenklatura::Addnomenklatura(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::Addnomenklatura)
ui->setupUi(this);
connect(ui->pushButton,SIGNAL(clicked()). SLOT(prostoSlot())); думал как соединить сигнал со слотом. но не пришел к правильному варианту
>
Addnomenklatura::~Addnomenklatura()
delete ui;
>
void Addnomenklatura::on_pushButton_clicked() После нажатия этой кнопки должен вызваться слот.
QSqlQuery query;
query.prepare("INSERT INTO nomenklatura (id_n,kod_pozicii,name_pozicii,cena) VALUES ( :id_n,:kod_pozicii,:name_pozicii,:cena)");
query.bindValue(":id_n", ui->lineEdit->text());
query.bindValue(":kod_pozicii", ui->lineEdit->text());
query.bindValue(":name_pozicii", ui->lineEdit->text());
query.bindValue(":cena", ui->lineEdit->text());
query.exec();
this->close();
>
void Addnomenklatura::on_pushButton_2_clicked()
this->close();
>
Сигналы и слоты в Qt
Сигналы и слоты используются для коммуникации между объектами. Механизм сигналов и слотов главная особенность Qt и вероятно та часть, которая отличаетcя от особенностей, предоставляемых другими фреймворками.
Введение
В программировании графического интерфейса, когда мы меняем один виджет, мы часто хотим что бы другой виджет получил об этом уведомление. В общем случае, мы хотим что бы объекты любого типа могла общаться с другими. Например, если пользователь нажимает кнопку Закрыть, мы вероятно хотим что бы была вызвана функция окна close().
Другие библиотеки добиваются такого рода общения используя обратный вызов. Обратный вызов это указатель на функцию, таким образом, если мы хотим что бы функция уведомила нас о каких-нибудь событиях, мы передаем указатель на другую функцию (обратновызываемую) этой функции. Функция в таком случае делает обратный вызов когда необходимо. Обратный вызов имеет два основных недостатка. Во-первых, он не является типобезопасным. Мы никогда не можем быть уверены что функция делает обратный вызов с корректными аргументами. Во-вторых, обратный вызов жестко связан с вызывающей его функцией, так как эта функция должна точно знать какой обратный вызов надо делать.
Сигналы и слоты
В Qt используется другая техника — сигналы и слоты. Сигнал вырабатывается когда происходит определенное событие. Слот это функция, которая вызывается в ответ на определенный сигнал. Виджеты Qt имеют много предопределенных сигналов и слотов, но мы всегда можем сделать дочерний класс и добавить наши сигналы и слоты в нем.

Механизм сигналов и слотов типобезопасен. Сигнатура сигнала должна совпадать с сигнатурой слота-получателя. (Фактически слот может иметь более короткую сигнатуру чем сигнал который он получает, так как он может игнорировать дополнительные аргументы). Так как сигнатуры сравнимы, компилятор может помочь нам обнаружить несовпадение типов. Сигналы и слоты слабо связаны. Класс, который вырабатывает сигнал не знает и не заботится о том, какие слоты его получат. Механизм сигналов и слотов Qt гарантирует, что если мы подключим сигнал к слоту, слот будет вызван с параметрами сигнала в нужное время. Сигналы и слоты могут принимать любое число аргументов любого типа. Они полностью типобезопасны.
Все классы, наследуемые от QObject или его дочерних классов (например, QWidget) могут содержать сигналы и слоты. Сигналы вырабатываются объектами когда они изменяют свое состояние так, что это может заинтересовать другие объекты. При этом он на знает и не заботится о том что у его сигнала может не быть получателя.
Слоты могут быть использованы для получения сигналов, но они так же нормальные функции-члены. Так же как объект не знает ничего о получателях своих сигналов, слот ничего не знает о сигналах, которые к нему подключены. Это гарантирует что полностью независимые компоненты могут быть созданы с помощью Qt.
Мы можем подключать к одному слоту столько сигналов, сколько захотим, также один сигнал может быть подключен к стольким слотам, сколько необходимо. Так же возможно подключать сигнал к другому сигналу (это вызовет выработку второго сигнала немедленно после появления первого).
Сигналы и слоты вместе составляют мощный механизм создания компонентов.
Небольшой пример
Описание класса на C++ может выглядеть вот так:
- class Counter
- public :
- Counter()
- int value () const
- void setValue( int value );
- private :
- int m_value;
- >;
Класс, наследуемый от QObject будет выглядеть следующим образом:
- #include
- class Counter : public QObject
- Q_OBJECT
- public :
- Counter()
- int value () const
- public slots:
- void setValue( int value );
- signals:
- void valueChanged( int newValue);
- private :
- int m_value;
- >;
Класс, наследованный от QObject имеет то же самое внутреннее состояние и обеспечивает публичные методы для доступа к этому состоянию, но дополнительно у него есть поддержка для использования сигналов и слотов. Этот класс может сообщить внешнему миру что его состояние изменилось выработав сигнал valueChanged() и у него есть слот, в который другие объекты могут посылать сигналы.
Все классы, содержащие сигналы и слоты должны указывать макрос Q_OBJECT в начале их описания. Они также должны быть потомками (прямо или косвенно) QObject.
Слоты реализуются программистом. Возможная реализация слота Counter::setValue() выглядит следующим образом:
- void Counter::setValue( int value )
- if ( value != m_value)
- m_value = value ;
- emit valueChanged( value );
- >
- >
Ключевое слово emit вырабатывает сигнал valueChanged() объекта с новым значением в качестве аргумента.
В следующем примере мы создаем два объекта типа Counter и соединяем сигнал valueChanged() первого со слотом setValue() второго используя статическую функцию QObject::connect():
- Counter a, b;
- QObject::connect(&a, SIGNAL(valueChanged( int )),
- &b, SLOT(setValue( int )));
- a.setValue(12); // a.value() == 12, b.value() == 12
- b.setValue(48); // a.value() == 12, b.value() == 48
Вызов 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.
Метаобъектная информация
Метаобъект содержит дополнительную информацию, такую как имя объекта. Можно так же проверить наследует ли объект определенный класс, например:
- if (widget->inherits( «QAbstractButton» ))
- QAbstractButton *button = static_cast(widget);
- button->toggle();
- >
Метаобъектная информация также испльзуется qobject_cast(), который похож на QObject::inherits(), но менее предрасположен к ошибкам:
- if (QAbstractButton *button = qobject_cast(widget))
- button->toggle();
Реальный пример
Ниже приведен простой пример виджета с комментариями.
- #ifndef LCDNUMBER_H
- #define LCDNUMBER_H
- #include
- class LcdNumber : public QFrame
- Q_OBJECT
Класс LcdNumber наследует QObject, который обладает большинством информации о сигналах и слотах через классы QFrame и QWidget. Он похож на встроенный виджет QLCDNumber.
Макрос Q_OBJECT указывает препроцессору объявить несколько функций-членов, которые будут реализованы программой moc; если при компилировании среди прочих будет появляется запись «undefined reference to vtable for LcdNumber», то скорее всего забыли запустить moc или добавить результат его работы в команду линковки.
- public :
- LcdNumber(QWidget *parent = 0);
Это не явно относится к moc’у, но если мы наследуем класс Qwidget, мы скорее всего захотим иметь аргумент parent (родитель) в конструкторе и передавать его конструктору родительского класса.
Некоторые деструкторы и функции-члены опущены здесь; moc игнорирует функции-члены.
- signals:
- void overflow();
LcdNumber вырабатывает сигнал когда его просят показать невозможное значение.
Если мы не заботимся о переполнении или знаем что оно не может произойти, мы может игнорировать этот сигнал, т.е. никуда его не подключать.
С другой стороны, если мы захотим вызвать две разные функции для реакции на эту ошибку, тогда просто подключаем эту функцию к двум разным слотам. Qt вызовет их оба (в произвольном порядке).
- public slots:
- void display( int num);
- void display( double num);
- void display( const QString &str);
- void setHexMode();
- void setDecMode();
- void setOctMode();
- void setBinMode();
- void setSmallDecimalPoint( bool point);
- >;
- #endif
Слоты это функции, используемые для получения информации об изменениях состояний других виджетов. LcdNumber использует их, как показано в коде выше, для установки отображаемого числа. Так как функция display() часть интерфейса класса с остальной программой, этот слот публичный.
Стоит отметить что функция display() перегружена. Qt выберет подходящую версию при соединении сигнала и слота. С обратным вызовом нам бы пришлось искать пять разных имен и контролировать типы самостоятельно.
Некоторые незначительные функции-члены были опущены в данном примере.
Продвинутое использование сигналов и слотов
В некоторых случаях может потребоваться информация об отправителе сигнала. Qt предоставляет функцию Qobject::sender(), которая возвращает указатель на объект, пославший сигнал.
Класс QSignalMapper необходим в ситуациях, когда много сигналов подключены к одному и тому же слоту, и этот слот должен реагировать на каждый сигнал по-разному.
Предположим что у нас есть три кнопки, которые определяют, какой файл мы хотим открыть: «Tax File», «Accounts File», or «Report File».
Что бы открыть нужный файл мы соединяем их сигнал QPushButton::clicked() со слотом readFile(). Теперь используем функцию класса QSignalMapper — setMapping() — для преобразования всех сигналов в объект QSignalMapper.
- signalMapper = new QSignalMapper( this );
- signalMapper->setMapping(taxFileButton, QString( «taxfile.txt» ));
- signalMapper->setMapping(accountFileButton, QString( «accountsfile.txt» ));
- signalMapper->setMapping(reportFileButton, QString( «reportfile.txt» ));
- connect(taxFileButton, SIGNAL(clicked()),
- signalMapper, SLOT (map()));
- connect(accountFileButton, SIGNAL(clicked()),
- signalMapper, SLOT (map()));
- connect(reportFileButton, SIGNAL(clicked()),
- signalMapper, SLOT (map()));
Теперь подключаем сигнал mapped() к слоту readFile() в котором разные файлы будут открыты в зависимости от нажатой кнопки.
- connect(signalMapper, SIGNAL(mapped( const QString &)),
- this , SLOT(readFile( const QString &)));
Использование Qt со сторонними сигналами и слотами
Можно использовать Qt со сторонним механизмом сигналов и слотов. Можно использовать несколько механизмов в одном проекте. Для этого надо добавить следующую строку в файл проекта (.pro):
CONFIG += no_keywords
Эта опция говорит Qt не определять ключевые слова moc’a — signals, slots, и emit, так как эти имена будут использованы строронней библиотекой, например, Boost. Что бы использовать сигналы и слоты Qt с установленным флагом no_keywords, надо просто заменить все использования ключевых слов moc’а Qt в исходных файлах на соотствующие макросы — Q_SIGNALS, Q_SLOTS, и Q_EMIT.