В Qt С++ используется два механизма обработки событий, это механизм виртуальных функций и механизм слотов и сигналов. Подробно останавливаться на этом не будем, так как в интернете очень много информации на эту тему. Главное – слот/сигнал, это имя функции с аргументами определенных типов. Мое представление об этом механизме сводится к следующим моментам:
- Фактически в программу встроен простейший интерпретатор на основе статических таблиц.
- Сами таблицы заполняются метакомпилятором на этапе компиляции C++ и содержат символьные имена и типы аргументов слотов и сигналов.
- Непосредственно, уже в программе пользователя, при помощи функции “connect” производится динамическая связка сигналов и слотов.
- При генерации сигнала, интерпретатор на основании статических таблиц и динамической связки ищет пару (цепочку пар) сигнал –> слот и вызывает нужные слоты. Именно таким поиском определяется “медленность” программы, по сравнению с классическими обратными вызовами.
- Преимущество в более полной инкапсуляции объектов друг от друга и в возможности динамической (во время выполнения готовой программы) перепривязки сигналов и слотов, что позволяет делать более гибкие алгоритмы.
Библиотека Qt огромна. Разработчики поставили себе цель сделать лучшее для С++, и как следствие они всё делают ориентируясь только на C++. По этому в Qt используются изощренные технологии, по другому и не скажешь. Главное – Qt написан без учета того, как её адаптировать к другим языкам программирования.
Однако если посмотреть в интернете, то и к другим языкам программирования написаны адаптации (обертки) для Qt. Если рассмотреть некоторые из них, то видно, что все разработчики идут разными путями.
Примеры, из тех, что я смог хоть как то понять и осмыслить:
- PyQt – python. Самая мощная, продвинутая и технически совершенная адаптация.
Для изготовления данной библиотеки, используется многоступенчатая схема. Был разработан специальный язык описания и компилятор для него (общее название SIP). На языке SIP создается промежуточное описание каждого класса Qt. После чего SIP обрабатывает все исходники Qt на C++ и собственные описания на языке SIP и генерирует новые исходники на C++, которые обеспечивают встраивание объектов C++ Qt в объекты python. Как следствие – эта библиотека содержит 99% всех возможностей Qt. Второй принципиальный момент, это то, что python может динамически менять набор слотов/сигналов, что позволяет объявлять любую функцию python слотом/сигналом Qt. - Lazarus – pascal. Довольно развитая библиотека. Основная особенность, полное переопределение многих (не всех) объектов Qt для адаптации к механизму событий по типу VCL Delphi. В рамках pascal уже не использует механизм сигналов/слотов. Внутри очень похож на QtE5, но не позволяет использовать слот/сигнал.
- Qt Jambi – java. Устарела. Основная особенность, использование промежуточной разаработки – smoke.
Smoke – это попытка разобрать C++ объект на состовные части по типу С. То есть, программа на jave обращается к коду smoke, и по частям синтезирует С++ объект в своём объекте java. На мой взгляд очень трудоёмко и главное, smoke перестал развиваться.
QtE5 слоты
В QtE5 также используется механизм сигналов-слотов. Однако в связи с тем, что QtE5 не умеет сама изготавливать слоты и сигналы, то пришлось отказаться от их изготовления на этапе
компиляции, как в С++. Вместо этого в класс QtE5.QAction добавлен набор (массив) уже готовых слотов и сигналов.
У данного решения есть недостатки:
- Так как набор слотов заранее фиксирован, то мы можем использовать только те слоты и сигналы (собственные функции с определенными аргументами), которые у нас есть в QtE5.
- Для добавления новых слотов и сигналов, нам необходимо менять C++ код для QtE5Widget.cpp и компилировать её заново.
Есть и небольшие преимущества:
- Как показала практика, большого разнообразия слотов и сигналов не нужно.
- Работая с QtE5 на D можно не используя C++ писать довольно сложные и объёмные программы.
Давайте рассмотрим типичную схему взаимодействия D и Qt C++. Я буду использовать идентификаторы на русском для лучшего понимания. Все классы QtE5 наследованы от QtE5.QObject (это не объект QObject Qt).
// ------ Начало типичного класса QtE5 ----------
extern (C) {
void on_Слот1_ДляВызоваИзQt(ТипичныйКлассQtE5* uk, int n) {
// Два параметра uk и n передаются всегда из Qt.
// Параметров может быть и больше, но эти два всегда присутствуют
(*uk).Слот1_ДляВызоваИзQt();
// Фактически вызван делегат
}
}
class ТипичныйКлассQtE5 {
QAction ac1; // Этот объект QtE5 содержит в себе набор слотов/сигналов
this() {
ac1 = new QAction(this, &on_Слот1_ДляВызоваИзQt, aThis);
// this - родитель --> parrent в Qt
// &on_Слот1_ДляВызоваИзQt - адрес функции, которую вызовет Qt в ответ на сигнал
// aThis - адрес экземпляра объекта ТипичныйКлассQtE5, см. main()
}
. . .
void Слот1_ДляВызоваИзQt() {
writeln("работа слота");
}
. . .
}
// ------ Конец типичного класса QtE5 ----------
main() {
ТипичныйКлассQtE5 f = new ТипичныйКлассQtE5(); f.saveThis(&f); // Сохраним адрес экземпляра в нем самом
. . .
// Опускаем кусок текста по генерации сигнала.
// Предположим, что виджет кнопка сгенерирует нам сигнал нажатие "click()".
// Связываем сигнал кнопки с нашим слотом
connects(экземплярКнопки, "click()", f.ac1, "Slot_AN()");
// сигнал кнопки "click()" теперь связан с слотом "Slot_AN()" нашего объекта
// Важно!! ПОЧЕМУ слот называется именно так "Slot_AN()"
// Обратите внимание, что имена слотов и сигналов символьные!!!!
}Из этого примера не понятно, почему наш слот имеет такое имя (“Slot_AN()”).
Видно, что при связке используется слот в QAction нашего класса ( f.ac1 ).
Давайте разбираться! Дело в том, что любой экземпляр класса QtE5.QAction содержит в себе три переменных.
- Первая переменная – это указатель на функцию С, которая будет вызвана, если произойдет вызов любого слота в QtE5.QAction.
Не важно как называется слот, важно, что любой слот в QtE5.QAction может вызвать только ОДНУ предопределенную функцию,
В нашем случае это on_Слот1_ДляВызоваИзQt. - Вторая переменная – это указатель на экземпляр класса, который содержит нашу функцию обработки слота.
В нашем случае это ТипичныйКлассQtE5 f; Именно этот указатель и запоминается f.saveThis(&f);
Но вначале он запоминается в самом себе, в f, вернее в он запоминается в любом наследнике QtE5.QObject, фактически в любом классе QtE5, т.к. все они наследованы от QObject. А вот дальше, при создании QAction он передаётся дальше в C++. Передается он с помощью свойства aThis – который просто выдаёт сохраненный адрес. Т.к. в классе может быть десятки обработчиков слотов,
то для их создания необходимо десятки указателей на функцию С (первый пункт) и один указатель на экземпляр класса. - Последняя переменная. Это просто число (int), обычно называемое N. Фактически мы можем при помощи этого числа организовать обработку одним слотом различных однотипных сигналов. Например несколько кнопок, а их все обрабатывает один слот. В Предыдущем нашем примере, это переменная не используется, но её видно в on_Слот1_ДляВызоваИзQt где это int n
Давайте рассмотрим конструктор QAction
// ------ Начало типичного класса QtE5 ----------
extern (C) {
void on_Слот1_ДляВызоваИзQt(ТипичныйКлассQtE5* uk, int n) {
// Два параметра uk и n передаются всегда из Qt.
// Параметров может быть и больше, но эти два всегда присутствуют
(*uk).Слот1_ДляВызоваИзQt();
// Фактически вызван делегат
}
}
class ТипичныйКлассQtE5 {
QAction ac1; // Этот объект QtE5 содержит в себе набор слотов/сигналов
this() {
ac1 = new QAction(this, &on_Слот1_ДляВызоваИзQt, aThis);
// this - родитель --> parrent в Qt
// &on_Слот1_ДляВызоваИзQt - адрес функции, которую вызовет Qt в ответ на сигнал
// aThis - адрес экземпляра объекта ТипичныйКлассQtE5, см. main()
}
. . .
void Слот1_ДляВызоваИзQt() {
writeln("работа слота");
}
. . .
}
// ------ Конец типичного класса QtE5 ----------
main() {
ТипичныйКлассQtE5 f = new ТипичныйКлассQtE5(); f.saveThis(&f); // Сохраним адрес экземпляра в нем самом
. . .
// Опускаем кусок текста по генерации сигнала.
// Предположим, что виджет кнопка сгенерирует нам сигнал нажатие "click()".
// Связываем сигнал кнопки с нашим слотом
connects(экземплярКнопки, "click()", f.ac1, "Slot_AN()");
// сигнал кнопки "click()" теперь связан с слотом "Slot_AN()" нашего объекта
// Важно!! ПОЧЕМУ слот называется именно так "Slot_AN()"
// Обратите внимание, что имена слотов и сигналов символьные!!!!
}Здесь видно, что если n не указана, то она равна нулю. В нашем примере, именно ноль и будет виден в on_Слот1_ДляВызоваИзQt.
А как же имя слота, почему оно такое? А вот тут вмешивается C++ и Qt. Дело в том, что Qt запоминает имена слотов и сигналов вместе с количеством и типом аргументов. Но мы не можем генерировать эту информацию (это чистый C++), по этому в QAction уже определено несколько слотов и сигналов с различными аргументами. Давайте на них посмотрим. Они видны в qte5widgets.h
“Slot_AN()”
Простейший слот. AN – это абревиатура, которая говорит, какие параметры будут переданы в extern C функцию.
В данном случае, будет передано два параметра, АдресЭкземпляраОбъекта и N. Раньше этот слот называется “Slot()”, и это его имя сохранено, но глядя на такое имя не поймешь, какие параметры будут. Новое имя более информативно.
“Slot_ANI(int)”
ANI – это абревиатура, которая говорит, какие параметры будут переданы в extern C функцию.
В данном случае, будет передано три параметра, АдресЭкземпляраОбъекта, N и еще один int.
Да, но откуда возьмется еще один int? А он будет взят из сигнала!!! Дело в том, что сигналы тоже имеют параметры.
Если сигнал имеет параметр int (например QSlider генерирует сигнал изменение с параметром int), то мы можем этот параметр перехватить и обработать у себя в слоте.
Как этого достигнуть? Для этого нам надо рассмотреть функцию connects()Предположим, что нам надо связать слайдер с нашим слотом, который может обработать дополнительный параметр int
connects(слайдер, “valueChanged(int)”, QtE5объект, “Slot_ANI(int)”);
Видно, что сигнал с параметром, но мы то тоже ловим этот сигнал слотом с параметром!! Для нас QAction организует немного другой вызов нашей C функции. Немного изменим предыдущий пример.
extern (C) {
void on_Слот1_ДляВызоваИзQt(ТипичныйКлассQtE5* uk, int n, int ЗначениеСлайдера) {
(*uk).Слот1_ДляВызоваИзQt(ЗначениеСлайдера);
}
}
class ТипичныйКлассQtE5 {
QAction ac1; // Этот объект QtE5 содержит в себе набор слотов/сигналов
this() {
ac1 = new QAction(this, &on_Слот1_ДляВызоваИзQt, aThis);
// this - родитель --> parrent в Qt
// &on_Слот1_ДляВызоваИзQt - адрес функции, которую вызовет Qt в ответ на сигнал
// aThis - адрес экземпляра объекта ТипичныйКлассQtE5, см. main()
}
. . .
void Слот1_ДляВызоваИзQt(int ЗначениеСлайдера) {
writeln("Игнал передал нам число = ", ЗначениеСлайдера);
}
. . .
}Таким образом, мы обработали параметр из сигнала. У нас есть ещё слот для обработки bool параметра сигнала
“Slot_ANB(bool)”
ANB – это абревиатура, которая говорит, какие параметры будут переданы в extern C функцию.
В данном случае, будет передано три параметра, АдресЭкземпляраОбъекта, N и еще один bool.
Пока это и все слоты, которые определены в QAction. Как видно их не много, но уж сколько есть. По крайней мере int и bool из стандартных сигналов мы схватить и обработать можем. А это десятки различных сигналов!
QtE5 сигналы
Как определены слоты более или менее понятно. А как воспользоваться сигналом? Сами сигналы так же определены в QtE5.QActin. Посмотрим на название сигналов и способы их вызова.
“Signal_V()”
V – это абревиатура, которая говорит, что параметров нет (void). Этот сигнал можно связать с любым слотом Qt который не имеет параметров. Например:
connects(QtE5_QAction, “Signal_V()”, QtE5_QApplication, “quit()”);
Связываем наш сигнал с стандартным слотом Qt. Это слот в QApplication::quit() и его вызов приводик к завершению программы.
Хорошо, связку то мы осуществили, а как непосредственно сгенерировать сигнал. Для этого у нас есть специальный метод класса QAction
QAction Signal_V() { //-> Послать сигнал с QAction “Signal_V()”
Есть еще два сигнала и соответсвенно два их вызова:
“Signal_VI(int)” —> void QAction.Signal_VI(int);
“Signal_VS(QString)” —> void QAction.Signal_VS(string);
Итак, мы рассмотрели механизм слотов и сигналов в QtE5. Думаю, что исходный текст ниже будет уже более понятен.
Предлагаю изучить его и найти все те приемы, которые были описаны выше
import std.stdio;
import qte5; // Графическая библиотека QtE5
import core.runtime; // Обработка входных параметров
import std.conv;
extern (C) {
void on_Slot1(CDemo* uk, int n, int value) { (*uk).slot1(value); }
void on_Slot2(CDemo* uk, int n) { (*uk).slot2(n); }
}
class CDemo : QWidget {
QVBoxLayout vbl; // Вертикальный выравниватель
QHBoxLayout hbl; // Горизонтальный выравниватель
QLCDNumber lcd; // Цифровой дисплей
QSlider slider; // Горизонтальный слайдер
QPushButton kn1, kn2; // Кнопки
QAction qa; // Задействуем слот и сигнал QtE5
QAction qb, qc; // Задействуем слот и сигнал QtE5
this() {
super(this); resize(400, 200);
qa = new QAction(this, &on_Slot1, aThis);
// используем доп.переменную N равную 1 и 2
qb = new QAction(this, &on_Slot2, aThis, 1);
qc = new QAction(this, &on_Slot2, aThis, 2);
// Определяем вертикальный выравниватель
vbl = new QVBoxLayout(this); // Неявно, через this вставляем в виджет
hbl = new QHBoxLayout(null);
lcd = new QLCDNumber(this);
slider = new QSlider(this);
kn1 = new QPushButton("Кнопка №1", this);
kn2 = new QPushButton("Кнопка №2", this);
// Вставляем элементы в выравниватели
hbl.addWidget(kn1).addWidget(kn2);
vbl.addWidget(lcd).addWidget(slider).addLayout(hbl);
// Настроим слайдер
slider.setMinimum(0).setMaximum(100);
// Вяжем стандартный сигнал слайдера со стандартным слотом LCD
connects(slider, "valueChanged(int)", lcd, "display(int)");
// Пошлем сигнал слайдеру при помощи нашего QAction
connects(qa, "Signal_VI(int)", slider, "setValue(int)");
qa.Signal_VI(25);
// Вяжем стандартный слот слайдера с нашим в QAction
connects(slider, "valueChanged(int)", qa, "Slot_ANI(int)");
// Вяжем наш сигнал из QAction c стандартным слотом виджета
connects(qa, "Signal_VS(QString)", this, "setWindowTitle(const QString &)");
// Вяжем стандартные сигналы кнопок с нашим вторым слотом
connects(kn1, "clicked()", qb, "Slot_AN()");
connects(kn2, "clicked()", qc, "Slot_AN()");
}
// Наш слот
void slot1(int zn) { // Примем числовое значение
// Пошлем сигнал со строкой
qa.Signal_VS("Значение слайдера: " ~ to!string(zn));
}
// Ещё один слот, обрабатывает вызов от двух разных кнопок
void slot2(int n) {
if(n == 1) {
msgbox("Нажата кнопка №1");
}
if(n ==2) {
msgbox("Нажата кнопка №2");
}
}
}
void main(string[] ards) {
bool fDebug = true;
if (1 == LoadQt(dll.QtE5Widgets, fDebug)) return;
QApplication app = new QApplication(&Runtime.cArgs.argc, Runtime.cArgs.argv, 1);
// ---- код программы
CDemo wd = new CDemo(); wd.saveThis(&wd);
wd.show();
// ----
app.exec();
}Создатель QtE5 Мохов Геннадий Владимирович (MGW)
Если есть пожелания, какие слоты или сигналы добавить в QtE5, то готов выслушать.