Pull to refresh

Реализация Qt signal/slot на Android

Reading time5 min
Views12K

Предисловие


Недавно портировал довольно большой проект с Qt (C++) на Android (Java), в процессе работы часто приходилось применять динамическое связывание объектов. Беда состояла в том что связывание (binding) в отличие от привычных сигналов и слотов в Qt в Java реализовано через лисенеры (listeners), и сколько я не пытался себя убедить что способ этот равноценен и тоже имеет место быть такого же удобства как при использовании сигналов и слотов достичь не удавалось.
Например, нам нужно связать бегунок (QSlider в Qt или SeekBar в Android) с каким либо действием, хотя бы привязать другой бегунок который будет послушно перемещаться следом за первым. В Qt подобная операция выглядит следующим образом:


Пример 1

// Создаём бегунки
QSlider *primary   = new QSlider(this);
QSlider *secondary = new QSlider(this);
// Размещаем в слое, инициализируем
// ...
// Связываем перемещение первого со вторым
connect(primary, SIGNAL(valueChanged(int)), secondary, SLOT(setValue(int)));


В результате получаем связь сигнала valueChanged() бегунка primary со слотом setValue() бегунка secondary. То же самое на Android:

Пример 2

// Создаём бегунки 
SeekBar firstBar  = (SeekBar)findViewById(R.id.firstSeekBar);
SeekBar secondBar = (SeekBar)findViewById(R.id.secondSeekBar);
// Инициализируем
// ...
// Связываем перемещение первого со вторым
firstBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {	
			@Override
			// ........
			@Override
			public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
				secondBar.setProgress(progress);
			}
		});


И в результате получаем тоже самое, то есть связываем перемещение firstBar и secondBar.
Давайте разберём что здесь происходит. Где-то в недрах firstBar есть переменная типа OnSeekBarChangeListener которая при наступлении перемещения проверится на null и если вдруг окажется ненулевой, а так оно и случится, будет вызван её метод onProgressChanged() с соответствующими параметрами который в свою очередь вызовет secondBar.setProgress(progress) и установит значение второго бегунка.
Всё предельно ясно и понятно, хотя и несколько громоздко. Qt в данном случае более лаконичен, хотя для реализации динамического связывания выходит за рамки C++ догенеривая код в процессе сборки проекта с помощью MOC (Meta Object Compiler). За лаконичность приходится расплачиваться, и это становится очевидным когда в процессе отладки попадаешь в догенереный код. Но к счастью, если следовать простейшим правилам делать это приходится крайне редко.
Но вернёмся к Android. Все классы Android API имеют достаточный набор лисенеров чтобы обеспечить удобство их использования, но что делать если в наличии большой массив кода оперирующий сигналами и слотами? Погуглив, я нашёл несколько реализаций сигналов и слотов на Java, самая достойная из которых, что не удивительно, в составе библиотеки Qt Jambi, несправедливо забытой реализации Qt на Java. Отличная реализация однако не устроила меня по нескольким причинам, самая веская из которых, несоответствие синтаксиса оригиналу, странно что одна и та же технология в составе библиотек под C++ и Java реализована столь различно.
В результате появилась идея реализовать сигналы и слоты под Android на Java самостоятельно.

Задача


Реализовать на Java механизм сигналов и слотов максимально приближенный к синтаксису Qt C++ используя Android API.

Реализация


Что получилось

После нескольких попыток был написан Java класс Connector менее чем о 600 строках имеющий несколько статических методов и статическую карту (Map) сигналов и слотов, по сути являющийся синглтоном (singleton). Весь функционал заключён в четырёх статических методах:

  1. boolean connect(Object sender, String signal, Object receiver, String slot, ConnectionType type)
  2. void disconnect(Object sender, String signal, Object receiver, String slot)
  3. void emit(Object sender, String signalName, Object ...params)
  4. Object sender()

  • connect() позволяет связать сигнал и слот, type в данном контексте экземпляр перечисления который может принимать значения DirectConnection и QueuedConnection по аналогии с Qt. DirectConnection, значение по умолчанию, работает также как лисенер в примере 1. QueuedConnection использует Handler из Android API для реализации асинхронного вызова слота. Остальные виды коннектов остались не реализованными, т.к. перечисленные 2 покрывали 100% случаев встречающихся в портируемом проекте.
  • disconnect() операция обратная коннекту. Разрывет связь сигнала со слотом. Имеет варианты с 4мя, 3мя, 2мя, и 1м параметром. Которые соответственно разрывают связь сигнала с конкретным слотом, связь сигнала со всеми слотами ресивера (receiver), связь сигнала со всеми слотами, и связь сэндера (sender) со всеми слотами.
  • emit() позволяет из произвольного места программы послать сигнал. Первый параметр содержит ссылку на объект (sender) который данный сигнал посылает, обычно this. Объявления сигнала, как метода или переменной, в отличие от других реализаций в данном случае не требуется, сигнал просто произвольная строка которая непременно должна совпадать со строкой переданной в методе connect(). Далее через запятую может следовать произвольное количество параметров произвольных типов, все из которых или хотя бы первые из них должны совпадать с параметрами слота
  • sender() очень полезный метод, также аналог из Qt, вызываемый из тела слота и возвращающий указатель (в Java ссылку типа Object) на объект пославший сигнал.

В примере с бегунками коннект выглядел бы так:

Пример 3

// К сожалению стандартные элементы Android API требуют реализации лиснера
private static class SeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
    	
    	private Object mSender = null;
    	
    	public SeekBarChangeListener(Object sender) {
    		mSender = sender;
    	}
        
    	@Override
    	// ........

    	@Override
    	public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    	    	Connector.emit(mSender, "progressChanged", progress);
    	}
}
// Создаём бегунки 
SeekBar firstBar  = (SeekBar)findViewById(R.id.firstSeekBar);
SeekBar secondBar = (SeekBar)findViewById(R.id.secondSeekBar);
// Инициализируем
// ...
// Учим firsBar посылать сигналы
firstBar.setOnSeekBarChangeListener(new SeekBarChangeListener(firstBar));
// Связываем перемещение первого со вторым
Connector.connect(firstBar, "SIGNAL(progressChanged(int))"
                                 , secondBar, "SLOT(setProgress(int))");

С первого взгляда может показаться что реализация с помощью Connector'а более громоздка чем с помощью лисенера, но не стоит забывать что SeekBar класс заточенный под использование лисенера, как и все другие стандартные классы Android API, потому приходится использовать врапперы (wrappers). Гораздо большую выгоду можно получить используя коннектор при разработке своих классов, или портируя Qt проекты на Android:
  1. не нужно создавать интерфейс для лисенера
  2. не нужно создавать под него переменную
  3. не нужен метод сеттер для лисенера


Более сложные примеры, подробно о деталях реализации, читайте в следующей статье.
(Продолжение следует)
Tags:
Hubs:
Total votes 32: ↑30 and ↓2+28
Comments17

Articles