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

В 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 содержит в себе три переменных.

  1. Первая переменная — это указатель на функцию С, которая будет вызвана, если произойдет вызов любого слота в QtE5.QAction.
    Не важно как называется слот, важно, что любой слот в QtE5.QAction может вызвать только ОДНУ предопределенную функцию,
    В нашем случае это on_Слот1_ДляВызоваИзQt.
  2. Вторая переменная — это указатель на экземпляр класса, который содержит нашу функцию обработки слота.
    В нашем случае это ТипичныйКлассQtE5 f; Именно этот указатель и запоминается f.saveThis(&f);
    Но вначале он запоминается в самом себе, в f, вернее в он запоминается в любом наследнике QtE5.QObject, фактически в любом классе QtE5, т.к. все они наследованы от QObject. А вот дальше, при создании QAction он передаётся дальше в C++. Передается он с помощью свойства aThis — который просто выдаёт сохраненный адрес. Т.к. в классе может быть десятки обработчиков слотов,
    то для их создания необходимо десятки указателей на функцию С (первый пункт) и один указатель на экземпляр класса.
  3. Последняя переменная. Это просто число (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”

  1. Если есть пожелания, какие слоты или сигналы добавить в QtE5, то готов выслушать.

Добавить комментарий