В предыдущей части рассматривалось решение проблемы мульти-обработки события несколькими элементами:
Именно благодаря ему можно теперь осуществить реализацию составных, или композитных элементов.
Как можно было заметить, до сих пор все элементы интерфейса были простыми, монолитными: кнопка это просто кнопка, ползунок это просто ползунок.
Но что, если надо сделать ползунок с двумя кнопками по краям? Можно пойти двумя путями:
Монолитный
Это будет по-прежнему один элемент, но со сложным поведением. Во-первых, он должен рисовать фиктивные кнопки, которых нет как отдельных объектов. Во-вторых, он должен определять, куда попадает указатель. Если в область, где находится фиктивная кнопка, то будет действовать как кнопка, иначе как ползунок.
Плюс здесь один – логика поведения элемента полностью инкапсулирована в нём, ни от чего не зависит и может быть какой угодно сложной.
Минусы же следующие. Я попытался сделать такой ползунок, и выяснил, что для рисования и обработки фиктивных кнопок приходится прилагать слишком много усилий. Рисовать это полбеды, но нужно ещё и правильно поддерживать их состояния (нажата, отжата, ховер и т.д.), то есть фактически дублировать достаточно громоздкую обработку состояний, которую стандартно делает диспетчер.
Чтобы понять бесперспективность такого подхода, можно сразу взять элемент "выпадающий список":
Здесь есть поле результата, список опций с собственным ползунком и кнопками, кнопка открытия и закрытия. И делать это всё как монолитный компонент окажется в разы сложнее.
Поэтому выбор оказался безальтернативным:
Композитный
Здесь всё просто: делаем обычный ползунок, а ему в потомки добавляем две отдельные кнопки. Все эти объекты существуют в дереве диспетчера и поэтому штатно отрисовываются и обрабатывают события.
Проблема же возникает, когда надо координировать эти события между частями составного элемента. Например, когда нажата кнопка "+", нужно увеличить значение ползунка, но кнопка не знает, где оно находится (она вообще не знает, что должна увеличивать что-то).
Так как кнопка является потомком ползунка, она может, по принципу из прошлой части, передать событие нажатия своему родителю, и тот уже может самостоятельно увеличить своё значение.
Но у ползунка есть собственная обработка нажатия, которая работает отлично... от кнопки. Мы понимаем, что это была кнопка, по набору условий: если пришло событие нажатия, и оно не попадает в область ползунка, значит это кнопка, потому что у ползунка в детях есть только кнопки.
Но такая логика может быть не вполне прозрачной или недостаточно надёжной, так как мы не знаем, какие там ещё дети могут появиться у ползунка в будущем.
Команды
Где-то в первых частях этого цикла я писал, что на уровне логики приложения совсем необязательно и даже противоестественно интерпретировать чисто механические события перемещения мыши, нажатия кнопок и т.п. Вместо них можно сделать внутренние, контекстно-понятные типы событий.
Например, кнопка может возвращать событие с типом "Увеличить значение". Получив такой тип, ползунок уже не должен проверять, куда попал курсор, от кого пришло событие и т.д. Уже ясно, что нужно сделать: увеличить значение.
На сколько увеличить? Величина приращения может быть просто жёстко прописанной константой, что не очень удобно. Или она может храниться в структуре самого ползунка, тогда он будет знать, на сколько. Или она может храниться в кнопке и передаваться вместе с событием.
Таким образом, событие превращается в команду, которую надо выполнить. Команда имеет тип и параметры.
Всё бы ничего, но события и так уже возвращаются от элементов к родителям. Теперь будут возвращаться ещё и команды.
Опуская длинную цепочку рассуждений, как я к этому пришёл (вкратце: через пробы и ошибки), озвучу финальные требования:
- Элемент всегда должен вернуть результат типа GUI_Result. Этот результат никак не связан с передачей событий и говорит лишь о том, что в элементе визуально что-то изменилось и его нужно перерисовать.
- Независимо от п.1, элемент возвращает команду или событие. В случае, когда элемент ничего специально не делает, он возвращает то же событие, которое получил.
- Диспетчер смотрит, что делать с тем событием, которое вернул элемент. Если у элемента указано, что он передаёт событие родителю, то диспетчер находит родителя и вызывает обработку рекурсивно с этим событием. Если у элемента ничего не указано, то диспетчер передаёт событие сразу на верхний уровень приложения, где оно может пройти по какой-то логике и обработаться в контроллере.
Комбинированное событие
Для того, чтобы представить событие и команду в виде единого принимаемого и возвращаемого параметра, нужен какой-то вид полиморфизма.
В C есть объединения union как полиморфизм для бедных. Я сделаю структуру команды:
В качестве типа команды можно набросать чего-нибудь пока абстрактного:
И объединю её и структуру GUI_Event в union внутри нового типа – комбинированного события GUI_ComboEvent:
Здесь находится всё утверждённое ранее: тип события, результат, и сама команда/событие.
Различать события и команды можно будет по полю type. Я введу новый тип события GUI_EVENT_CMD:
И если тип равен GUI_EVENT_CMD, то это команда. Элементы будут обрабатывать и события, и команды, руководствуясь этим типом. Некоторые элементы могут обрабатывать только события, а некоторые – только команды, поэтому они могут не проверять тип, а сразу брать нужные им данные. Ну, на деле можно проверить, чтобы была возможность обнаружить какие-то случайные ошибки и сообщить о них.
Я изменил обработчики элементов, чтобы они теперь и принимали, и возвращали тип GUI_ComboEvent. Посмотрим на примере кнопки. Во-первых, она может теперь возвращать команду, поэтому дополню структуру кнопки полями command_type и value:
Обработчик кнопки меняется так:
Если кнопка была отжата, и в её свойствах указана команда, кнопка переделывает поступившее событие в команду путём изменения его типа, устанавливает value и возвращает эту команду.
Диспетчер проверяет, что вернул элемент:
Если это валидный тип события, и он совпадает с полем элемента return_state, то диспетчер отправляет событие на обработку родителю. Проверка поля parent_offset делается из соображений паранойи, так как у корневого элемента оно равно нулю, и если по ошибке назначить корневой элемент возвращающим событие, то он начнёт бесконечно возвращать его сам себе.
Если же событие не ушло к родителю, диспетчер просто возвращает его, и теперь его будет обрабатывать роутер верхнего уровня.
Реализация
Назначу кнопке слева от ползунка команду GUI_CMD_INCVAL со значением -10:
А элементу GUI_Item, который владеет кнопкой, назначу поле return_state = GUI_EVENT_CMD. Это значит, что если кнопка породит команду, то эта команда будет передана родителю кнопки.
Теперь обработка кнопки:
Если у кнопки задана команда, она переделывает событие в команду, присваивает value и возвращает. Далее диспетчер увидит эту команду и передаст родителю-ползунку.
Ползунок должен обработать команду:
После обработки он устанавливает событию тип GUI_EVENT_NONE, чтобы оно больше никем не обработалось. Возможно, оно и так никем больше не обработается, но так надёжнее.
И, собственно, всё. Далее я аналогичным образом оформляю кнопку справа от ползунка, а затем и сам ползунок. Так как он участвует в установке RGB-цвета заголовка, то будет в свою очередь порождать команду GUI_CMD_SETCOLOR. Но поле return_state у него будет пустое, поэтому команда вернётся сразу в контроллер окна.
Логика самого контроллера стала проще. Ему больше не надо проверять, от какого ползунка пришла команда. Он уже и так знает, что делать.
Я также избавился от отдельных роутингов на разные события и сделал единую функцию route_item().
Функция в свою очередь не будет вызывать разные обработчики окна, а вызовет единый обработчик:
А единый обработчик окна уже разберётся с командами и событиями:
Так что всё более-менее причесалось и появились, как я сейчас вижу, уже достаточные механизмы для реализации более сложных взаимодействий между элементами интерфейса. Поэтому дальше я не буду это всё описывать в подробностях, а просто сделаю выпадающие списки и прочие нужные вещи.
После этого нужно будет перейти к следующим задачам:
- Несколько окон
- Активные и неактивные окна
- Сворачивание и разворачивание окон
И тогда уже можно будет считать GUI более-менее рабочим.
Код на гитхабе:
Читайте дальше: