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

Костылик для сигнал-слот системы в Qt

Время на прочтение 9 мин
Количество просмотров 28K


Привет всем. Я хочу рассказать вот о чём… Недели две назад впервые понадобилось работать с GUI под С++ и, погуглив малость, я решил использовать Qt. Его все жутко хвалили, да и вообще на первый взгляд выглядел он весьма достойно.

На второй взгляд Qt оказался тоже годной штукой, но из-за некоторых ограничений реализации его мето-объектного компилятора пришлось-таки конструировать небольшие костылики. В данном очерке (думаю, слово «очерк» подойдёт лучше, ибо по объёму это на статью не тянет) хочу рассказать о том, как я решал возникающие проблемы.

Добавлено: На основе комментариев добрых людей была сделана правка, повествующая о том, как можно обойтись без MOC и без костыликов вообще.



Как всё начиналось



Началось всё с того, что мне было необходимо сделать шаблонный класс-контроллер в рамках реализации MVC-архитектуры в моей библиотечке. Контроллер должен был взаимодействовать с разнотипным целочисленными данными, связывая с ними QSpinBox в качестве GUI. Если отбросить всякую шелуху, вышло что-то вот такое:

Шаблонный контроллер
template< typename T_IntegralType >
class IntegralController {
private:
	T_IntegralType *_modelField;
	QSpinBox *_view;
	...

public:
	QSpinBox *getView() {
		if (!_view) {
			_view = new QSpinBox();
			/* Подписаться на событие от вьюшечки*/
		}
		return _view;
	}
	. . .

private:
	// Метод, который должен срабатывать по событию от вьюшечки
	void setValue(T_IntegralType inValue) { * modelField = inValue; }
};



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

Примечание про model-view подход
Есть ещё такая вещь, как система делегатов в рамках model-view подхода в Qt, которая позволяет обрабатывать пересылку данных между view и model через реализацию интерфейсов, без системы сигналов-слотов. По определённым причинам, мне не удалось нормально использовать model-view Qt у себя в библиотеке.


Для того, чтобы какой-либо класс мог предоставлять слоты для сигнал-слот системы, необходимо было чтобы этот класс наследовался от класса QObject и включал в себя макрос Q_OBJECT. Зачем? Тогда я не парился с тем, чтобы разобраться. Нужно — значит нужно. Не мудрствуя лукаво, добавил требуемые вещички в свой шаблонный класс и описал реализацию обработки событий GUI:

Шаблонный контроллер с обработкой событий
template< typename T_IntegralType >
class IntegralController : public QObject {
	Q_OBJECT
private:
	T_IntegralType *_modelField;
	QSpinBox *_view;
	...

public:
	QSpinBox *getView() {
		if (!_view) {
			_view = new QSpinBox();
			QObject::connect(_view, SIGNAL(valueChanged(int)),
					this, SLOT(valueChanged(int));
		}
	}
	. . .

private slots:
	void valueChanged(int inValue) { *_modelField = inValue; }
};



Всё собралось. Отлично, подумал я, какой же я молодец! — и продолжил пилить библиотеку, на время позабыв об этом шаблонном контроллере. Конечно, зря поторопился и вообще рано радовался, ибо был нефига не молодцом. Едва я попробовал собрать код, использующий специализацию шаблонного контроллера, как посыпались сообщения линковщика об ошибках, вот в таком духе:

Ошибки линковки
unresolved external symbol "public: virtual struct QMetaObject const * __cdecl TClass < int >::metaObject(void)const " (?metaObject@?$TClass@H@@UEBAPEBUQMetaObject@@XZ)

unresolved external symbol "public: virtual void * __cdecl TClass < int >::qt_metacast(char const *)" (?qt_metacast@?$TClass@H@@UEAAPEAXPEBD@Z)

unresolved external symbol "public: virtual int __cdecl TClass < int >::qt_metacall(enum QMetaObject::Call,int,void * *)" (?qt_metacall@?$TClass@H@@UEAAHW4Call@QMetaObject@@HPEAPEAX@Z)



Было очевидно, что я делаю что-то не так с мета-объектной системой. Пришлось перечитывать про MOC. Дело оказалось в том, что MOC, когда проходится по исходникам, генерирует дополнительные cpp-файлы, в которых создаётся реализация нужных для работы мета-объектой системы методов. С шаблонами эта система умеет работать очень криво, плохо генерируя этот самый метаобъектный код — MOC либо просто игнорирует шаблонные классы при генерации кода, либо воспринимает их как обычные классы, отбрасывая шаблонные аргументы, что по понятным причинам вызывало указанные выше проблемы.

Ещё про шаблоны и Qt
По поводу того, почему не стоит пользоваться шаблонами вместе с сигнал-слот системой есть даже отдельная статья в доке. К сожалению, в моём случае речь шла не об оптимальности, а о приличном сокращении объёма кода и избегании большого объёма копи-пасты — так что для меня пункты из этой статьи не подходили.


При дальнейшем ознакомлении с MOC, обнаружилось существование ещё нескольких ограничений на использование подконтрольных MOC классов. Самое неприятной из них — невозможность описания полноценно управляемых MOC наследников QObject во вложенных классах. А я люблю делать вложенные классы, разбивая области ответственности большого класса между живущими внутри него классиками поменьше. Да, знаю, тру-программисты используют неймспейсы, но при этом, на мой взгляд, загромождается контекст программы, да и семантика вложенных классов отличается от семантики неймспейсов (в первом случае мы строем иерархию отношений между классами, во втором — просто их группируем по каком-то признаку).

В общем, с учётом того, что от MOC-системы мне была нужна лишь возможность подписываться на события через статический метод QObject::connect(...), я решил написать небольшой костыль… Совсем небольшой, маленький костылик.

Примечание: Костылик, как выяснилось, получился для версии Qt младше пятой, где властвовал кривоватый MOC. В версии Qt 5 появилось крутое API, позволяющее сигналы-слоты без этого самого MOC. Добавил эту информацию в конце статьи. Снова мораль для меня, ленивого автора статей, который совсем не молодец: семь раз загугли, один раз напиши статью.

Про костылик



Идея была проста — сделать мини-класс, который наследовал бы QObject и полностью подходил бы для MOC в смысле регистрации класса для корректной работы в рамках сигнал-слот системы. Данный мини-класс предоставлял бы также метод, позволяющий связывать Qt-независимый коллбек с вызовом слота в этом вспомогательном классе.

Звучит сложно, пример будет яснее, я надеюсь. В коде для обработки событий от QSpinBox это выглядело вот так:

Костылик для событий от QSpinBox
class ValueChangedWorkaround : public QObject {
	Q_OBJECT
public:
	// Тип-функтор, реализующий коллбек. В моём случае это
	// был FastDelegate (ссылка на либу в этом спойлере, ниже)
	typedef fastdelegate::FastDelegate1 < int > Callback;

private:
	Callback _callback;

public:
	ValueChangedWorkaround() : _callback() { }

	void bind(QSpinBox *inSpinBox, const Callback &inCallback) {
		_callback = inCallback;
		QObject::connect(inSpinBox, SIGNAL(valueChanged(int)),
				this, SLOT(valueChanged(int));
	}

private slots:
	void valueChanged(int inValue) { _callback(inValue); }
};


Про FastDelegate вообще
FastDelegate на GitHub


Я использовал этот код в контроллере — и всё заработало:

Контроллер с использованием костылика
template< typename T_IntegralType >
class IntegralController {
private:
	typedef IntegralController< IntegralType > OwnType;

	T_IntegralType *_modelField;
	QSpinBox *_view;
	ValueChangedWorkaround _valueChangedWorkaround;
	...

public:
	QSpinBox *getView() {
		if (!_view) {
			_view = new QSpinBox();
			_valueChangedWorkaround.bind(_view,
				ValueChangedWorkaround::Callback(
					&OwnType::valueChanged));
		}
	}
	. . .

private:
	void valueChanged(int inValue) { *_modelField = inValue; }
};



Казалось бы, на этом можно было успокоиться… Но я патологический фанат универсальных решений, поэтому решил сделать макрос, позволяющий создавать костылики для обработки разных событий от Qt-объектов в промышленных масштабах.

Властелин костылей

Генератор костыликов



Казалось бы, новый макрос можно сделать на основе старого, просто заменив некоторые идентификаторы на макросные аргументы и слегка обобщив сам макрос.

Макрос для генерации костыликов. Версия 1.0
Опускаю здесь косые чёрточки (вот такие: "\") — бесят жутко!

define QT_EVENT_WORKAROUND_1_ARG(M_WorkaroundName, M_EventName, M_Arg0Type)
class M_WorkaroundName : public QObject {
	Q_OBJECT
public:
	typedef fastdelegate::FastDelegate1 < M_Arg0Type > Callback;

private:
	Callback _callback;

public:
	M_WorkaroundName() : _callback() { }

	void bind(QObject *inQSignalSource, const Callback &inCallback) {
		_callback = inCallback;
		QObject::connect(inQSignalSource, SIGNAL(M_EventName(M_Arg0Type)),
				this, SLOT(M_EventName(M_Arg0Type));
	}

private slots:
	void M_EventName(M_Arg0Type inValue) { _callback(inValue); }
};



Написав этот макрос, я подумал с уверенностью, что вот теперь уж я точно молодец и вообще властелин костылей (как тот мужик выше). Очень довольный, запустил код и… Да, конечно, ничего не работало. Ошибок компиляции не было, всё собиралось, но коллбек не вызывался, а в лог сыпались сообщения о том, что, дескать, у моего класса TestWorkaround нету нужного слота.

Пришлось копать дальше. Выяснилось, что MOC в Qt не умеет разворачивать макросы. Он проходит по коду до выполнения препроцессора (то бишь, не по тому коду, который, например, можно видеть если выполнить сборку с флагом -E в MinGW, а по никак не обработанному коду).
С другой стороны, MOC должен знать сигнатуры методов, расположенные в блоках декларации класса после слова «slots» — он их читает как строки и дальше использует эти строковые имена при вызовах QObject::connect (макросы SLOT и SIGNAL извлекают эти имена + немного метаданных про место использования). Таким образом, стало ясно, что от пользователя макроса для генерации костыликов придётся требовать-таки писать свою реализацию слота.

Я постарался минимизировать объём и сложность этого кода и окончательное решение выглядит так (уже финальный код, с богомерзкими косыми чёрточками, ага):

#define SIGNAL_WORKAROUND_1_ARG(M_WorkaroundName, M_CallName, M_Arg0Type)\
class M_WorkaroundName : public QObject {\
public:\
	typedef fastdelegate::FastDelegate1< M_Arg0Type > Delegate;\
\
private:\
	Delegate _delegate;\
\
public:\
	void bind(QObject *inQSignalSource, const Delegate &inDelegate) {\
		_delegate = inDelegate;\
		QObject::connect(inQSignalSource, SIGNAL(M_CallName(M_Arg0Type)),\
				this, SLOT(M_CallName(M_Arg0Type)));\
	}\
\
	void CALL(M_Arg0Type inArgument) { _delegate(inArgument); }\
— Как видите, макрос описывает не до конца определённый класс, закончить который пользователю нужно описанием слота для вызова метода CALL(...). Полная инструкция по использованию генератора костыликов ниже…

Полная инструкция:
1. Сгенерировать где-нибудь класс-костылик с использованием макроса. В примере мы будем генерировать костылик с именем YourWorkaroundName, который оборачивает событие qtEventName, принимающее один аргумент типа EventArg1Type). Код для генерации класса:

SIGNAL_WORKAROUND_1_ARG(YourWorkaroundName, qtEventName, EventArg1Type)
	Q_OBJECT private slots: void qtEventName(EventArg1Type a0) { CALL(a0); }
};


2. Использовать новый тип в любом месте кода, где нужно обрабатывать события от каких-либо объектов Qt, которые умеют отсылать событие оборачиваемого типа (в примере — событие qtEventName, отправляющее один аргумент типа EventArg1Type). Пример кода, использующего костылик:

class UserClass {
private:
	QSomeObject *_qTestObject;
	YourWorkaroundName _workaround;

public:
	UsingClass() : _qTestObject(new QSomeObject()) {
		_workaround.bind(_qTestObject,
				YourWorkaroundName::Callback(&UsingClass:: onEvent));
	}

	void onEvent(EventArg1Type inArg) { /* Some actions on callback */ }
}


Всё, готово. Теперь вы можете обрабатывать сообщения от Qt-объектов в любых классах, без ограничений, накладываемых Qt MOC.

В заключение — несколько замечаний:
1. Предложенный макрос годится для событий, принимающих один аргумент на вход. Для обработки другого количества аргументов можно либо сделать копи-пасту этого макроса (олдфаг-стайл), либо использовать variadic macro из С++11.
2. Предложенное решение использует библиотеку FastDelegate для работы с коллбеками. Вы можете легко заменить FastDelefate в макросе на свой тип, если хотите использовать свои функторы.
3. В данном решении нет обработки ошибок, ассертов, и.т.д — в моём случае подобная обработка не требуется. Можете добавить по вкусу.
4. Я готов согласиться с тем, что предложенное решение это адово адище и с удовольствием выслушаю предложения как ещё можно было справиться с ограничениями Qt MOC. Добавлю ваши предложения в статью с указанием авторства решения и с благодарностью от себя лично. Заранее спасибо!

Заключение



Надеюсь, предложенный генератор костыликов поможет сэкономить кому-нибудь время на написание аналогичной жести. Напоследок замечу ещё, что генератор костыликов содержит небольшое количество кода, поэтому, как по мне, нет смысла выкладывать его на GitHub.
Однако, по пожеланию почтенной публики могу сделать маленькую репу с тест-драйвом. Высказывайтесь в комментах по поводу, если желающих будет больше пяти — выложу.

Спасибо за внимание и за то, что дочитали!

П.С.: Если будете находить какие-нибудь ошибки в статье — пишите, буду править.


(Добавлено 11.03.2016)

Спасибо AlexPublic и VioletGiraffe, ткнули носом в новое сигнал-слот API.

Если говорить про генератор костыликов, он будет выглядеть вот так:

Генератор костыликов
#define SIGNAL_WORKAROUND_1ARG(M_WorkaroundName, M_CallName, M_Arg0Type)\
class M_WorkaroundName : public QObject {\
public:\
    typedef fastdelegate::FastDelegate1< M_Arg0Type > Delegate;\
\
private:\
    Delegate _delegate;\
\
public:\
    template< typename T_ListenerType >\
    void bind(T_ListenerType *inQSignalSource, const Delegate &inDelegate) {\
        _delegate = inDelegate;\
        connect(inQSignalSource,\
                    static_cast< void (T_ListenerType::*)(M_Arg0Type) > &T_ListenerType::M_CallName),\
                    this, &M_WorkaroundName::M_CallName);\
    }\
\
    void M_CallName(M_Arg0Type a0) { _delegate(a0); }\
};


Использование стало проще. Теперь тип-обёртку можно создавать вообще где угодно, включая шаблонный контекст. Как-нибудь вот так:

template<typename T_SomeQObject>
class UserClass {
private:
	SIGNAL_WORKAROUND_1ARG(YourWorkaroundName, qtEventName, EventArg1Type);

	T_SomeQObject *_qTestObject;
	YourWorkaroundName _workaround;

public:
	UsingClass() : _qTestObject(new T_SomeQObject()) {
		_workaround.bind(_qTestObject, YourWorkaroundName::Callback(&UsingClass::onEvent));
	}

	void onEvent(EventArg1Type inArg) { /* Some actions on callback */ }
}



Хотя, следует заметить, это новое сигнал-слот Qt5 API позволяет вообще отвязаться от MOC и его ограничений. Поэтому вместо использования костыля легче унаследовать класс-слушатель от QObject (так как больше нет привязки к MOC, наследник-слушатель может быть шаблонным и вообще каким угодно), обложив QObject ifdef-ом если хочется делать библиотеку с поддержкой разный GUI-фреймворков, либо, если использовать С++11 и старше, и воспользоваться лямбдой, как это предложил AlexPublic.
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+19
Комментарии 94
Комментарии Комментарии 94

Публикации

Истории

Работа

QT разработчик
6 вакансий
Программист C++
120 вакансий

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

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн