В 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, то готов выслушать.