В предыдущей части сделана работа вкладок и чекбоксов, то есть какой-никакой интерактив.
Я также внёс кучу косметических изменений в код, но в основе всё сохранилось. Добавил элемент "кнопка", элемент "цифровой дисплей", подровнял окно и вкладки:
Нажатиями на кнопки "-" и "+" можно уменьшать и увеличивать значение поля дисплея. Здесь возникает ещё одна задача связи между элементами. Если при нажатии на чекбокс я обновлял данные в модели, то при нажатии на кнопку "-" или "+" требуется обновлять данные и в модели, и в элементе дисплея, чтобы визуально наблюдать, как изменяется число.
И здесь, в отсутствие прямых указателей, ничего не остаётся, кроме как искать элемент по его id. То есть, контроллер, обрабатывающий события для этого окна, знает, что если нажата кнопка с id = ID_BTN2, то нужно увеличить значение элемента с id = ID_PANEL1.
Ну и да, приходится рекурсивно искать элемент с таким id в дереве элементов.
Не рекурсивно, потому что известно количество всех потомков у родительского элемента, и все они расположены подряд за родительским элементом, так что просто перебираем массив.
Но это, скажем так, только половина истории. Я уже упоминал, что нажатие на кнопку мыши это не клик. Кликом считается, когда кнопка была нажата на каком-то элементе, и затем отжата, не покидая его.
Кроме того, есть такая интересная фишка, как ховер. Это когда мы просто навели курсор на элемент, но ещё ничего не нажали. Элементы с ховером могут специальным образом подсвечиваться.
Наконец, есть драг. То есть перетаскивание.
Cтатусы ховера, нажатия, отжатия и драга довольно замысловато переплетаются, что в реализации быстро приводит к путанице. Поэтому нужно бросить писать код и вдумчиво составить таблицу переходов состояний.
Логика может показаться избыточно громоздкой, но попробуйте учесть все возможные варианты, и проще не получится. Либо получится проще, но с некоторыми ограничениями и редко воспроизводимыми глюками.
Я добавлю в диспетчер поле состояния state, индекс последнего найденного элемента last_index, индекс родительского корневого элемента last_uid_index и координаты последнего нажатия origin_x и origin_y.
Всё это понадобится дальше. Также я сделал свою структуру GUI_Event со своими типами GUI_EventType:
Проблема в том, что событие SDL_Event имеет разные поля в зависимости от своего типа. Например, если тип события SDL_MOUSEMOTION, то координаты курсора находятся в полях motion.x и motion.y, а если тип SDL_MOUSEBUTTONDOWN, то они уже в полях button.x и button.y. Из-за этого приходится писать разные варианты кода.
Сама структура SDL_Event устроена как union и возможно, что поля button.* идентичны полям motion.*, но я не хочу рисковать.
Поэтому я сделал свой тип события и функцию, которая переконвертирует события из SDL_Event в мой тип.
Помимо переконвертации функция отсеивает ненужные типы, что помогает в дальнейшем.
Теперь можно написать функцию обработки, реализующую переходы состояний. Будем смотреть её по частям, соответствующим таблице.
Состояние NONE
Код (оптимизированный по длине листинга):
Состояние DOWN
Код:
Дополнительная обработка элемента происходит в функции GUI_dispatcher_process_item(). Она вставлена именно после события отпускания кнопки, так как это и есть завершённый клик. Туда не передаётся ничего лишнего, потому что всё необходимое уже сохранено в диспетчере. Сам код посмотрим позже. Но это ещё не обработка в контроллере, как указано в таблице. Она будет позже, так как требует роутинга.
Состояние HOVER
Код:
Указанные манипуляции позволяют полностью автоматически перерисовывать интерфейс. Например, функция, которая рисует кнопку, видит, что она имеет статус GUI_STATUS_HOVER, и перерисовывает её именно для этого статуса. Для этого у структуры кнопки присутствуют соответствующие поля цвета и формы (либо она берёт их из контекста).
Драг
Отдельно рассмотрим обработку драга. Запоминать при нажатии origin_x и origin_y нужно как раз для того, чтобы получить смещения dx, dy от точки нажатия до текущей точки. Есть одна хитрость. По традиции, окно перетаскивается за его заголовок. Кстати да, я добавил ещё элемент "заголовок", потому что окна бывают и без заголовка. Но он не элемент верхнего уровня, поэтому надо перетаскивать не его, а родительское окно. Именно для этого в диспетчере запоминается индекс родительского элемента last_uid_index. Его мы и таскаем.
Кроме самого окна, нужно перетащить все его дочерние элементы, и к счастью рекурсия здесь опять не нужна. Так как известно общее количество потомков, и все они лежат в массиве последовательно после окна, то просто проходим по массиву и к координатам каждого прибавляем dx и dy. После этого текущие координаты курсора запоминаем как последние origin_x, origin_y.
Драг работает отлично, но единственная проблема это выход части окна за пределы экрана. То, что рисуется с помощью SDL_FillRect(), отрабатывает нормально, но вот текст у меня выводится прямым доступом к видеопамяти. Поэтому, как только текст попадает за пределы SDL_Surface, программа падает :)
Решить проблему можно просто: запретить окну перемещаться за край экрана. Ну или доработать вывод текста.
Дополнительная обработка
Некоторые элементы требуют дополнительной обработки. Это чекбоксы, переключение вкладок и счётчик. Да, уже работало в прошлой части, но сейчас были кое-какие изменения.
Напишем функцию GUI_dispatcher_process_item() пока только для чекбокса:
Функция, как и ранее, переключает состояние чекбокса на противоположное. Я также добавил огромный чекбокс на первую вкладку, потому что могу вкладки ещё не переключаются:
Добавим код для переключения вкладок, также из прошлой части, с необходимыми изменениями:
Теперь можно переключать вкладки, и конечно тыкать в другие чекбоксы:
Осталось обработать изменение счётчика. Делать мы это будем в другой функции, потому что та, которая выше, занимается общим видом элементов интерфейса и работает всегда стандартно. Нам же нужна особая логика изменения определённого элемента в определённом окне по определённому событию, то есть
Роутинг события в контроллер окна
Функция роутинга будет встроена в основной цикл программы, сразу после стандартной обработки события, но перед перерисовкой:
Флаг dirty сигнализирует, что в результате обработки требуется перерисовка. Значит, что-то изменилось. На это мы и ориентируемся. В принципе сценарии могут быть разные. Скажем, ничего не изменилось, а роутинг нужен. Но пока так.
Также, роутинг в принципе можно вызывать после любого события, в том числе делать разный роутинг по типам. Но сейчас он необходим только для события отжатия кнопки.
Функция route_item() выясняет с помощью last_uid_index, в каком окне произошло событие, и запускает контроллер этого окна, также передавая ему соответствующую модель (пока всё в единственном экземпляре):
Процессинг окна уже происходит по полностью произвольной логике:
Здесь обновляется содержимое элемента со счётчиком с поиском его по ID, либо передаются состояния чекбоксов в модель.
Демонстрационное видео:
Код на гитхабе:
Читайте дальше: