Найти в Дзене
ZDG

Графический интерфейс программы на языке C #8: Переходы состояний

Оглавление

В предыдущей части сделана работа вкладок и чекбоксов, то есть какой-никакой интерактив.

Я также внёс кучу косметических изменений в код, но в основе всё сохранилось. Добавил элемент "кнопка", элемент "цифровой дисплей", подровнял окно и вкладки:

Нажатиями на кнопки "-" и "+" можно уменьшать и увеличивать значение поля дисплея. Здесь возникает ещё одна задача связи между элементами. Если при нажатии на чекбокс я обновлял данные в модели, то при нажатии на кнопку "-" или "+" требуется обновлять данные и в модели, и в элементе дисплея, чтобы визуально наблюдать, как изменяется число.

И здесь, в отсутствие прямых указателей, ничего не остаётся, кроме как искать элемент по его id. То есть, контроллер, обрабатывающий события для этого окна, знает, что если нажата кнопка с id = ID_BTN2, то нужно увеличить значение элемента с id = ID_PANEL1.

Ну и да, приходится рекурсивно искать элемент с таким id в дереве элементов.

-2

Не рекурсивно, потому что известно количество всех потомков у родительского элемента, и все они расположены подряд за родительским элементом, так что просто перебираем массив.

Но это, скажем так, только половина истории. Я уже упоминал, что нажатие на кнопку мыши это не клик. Кликом считается, когда кнопка была нажата на каком-то элементе, и затем отжата, не покидая его.

Кроме того, есть такая интересная фишка, как ховер. Это когда мы просто навели курсор на элемент, но ещё ничего не нажали. Элементы с ховером могут специальным образом подсвечиваться.

Наконец, есть драг. То есть перетаскивание.

Cтатусы ховера, нажатия, отжатия и драга довольно замысловато переплетаются, что в реализации быстро приводит к путанице. Поэтому нужно бросить писать код и вдумчиво составить таблицу переходов состояний.

-3

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

Я добавлю в диспетчер поле состояния state, индекс последнего найденного элемента last_index, индекс родительского корневого элемента last_uid_index и координаты последнего нажатия origin_x и origin_y.

-4

Всё это понадобится дальше. Также я сделал свою структуру GUI_Event со своими типами GUI_EventType:

-5

Проблема в том, что событие SDL_Event имеет разные поля в зависимости от своего типа. Например, если тип события SDL_MOUSEMOTION, то координаты курсора находятся в полях motion.x и motion.y, а если тип SDL_MOUSEBUTTONDOWN, то они уже в полях button.x и button.y. Из-за этого приходится писать разные варианты кода.

Сама структура SDL_Event устроена как union и возможно, что поля button.* идентичны полям motion.*, но я не хочу рисковать.

Поэтому я сделал свой тип события и функцию, которая переконвертирует события из SDL_Event в мой тип.

-6

Помимо переконвертации функция отсеивает ненужные типы, что помогает в дальнейшем.

Теперь можно написать функцию обработки, реализующую переходы состояний. Будем смотреть её по частям, соответствующим таблице.

Состояние NONE

-7

Код (оптимизированный по длине листинга):

-8

Состояние DOWN

-9

Код:

-10

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

Состояние HOVER

-11

Код:

-12

Указанные манипуляции позволяют полностью автоматически перерисовывать интерфейс. Например, функция, которая рисует кнопку, видит, что она имеет статус GUI_STATUS_HOVER, и перерисовывает её именно для этого статуса. Для этого у структуры кнопки присутствуют соответствующие поля цвета и формы (либо она берёт их из контекста).

-13

Драг

Отдельно рассмотрим обработку драга. Запоминать при нажатии origin_x и origin_y нужно как раз для того, чтобы получить смещения dx, dy от точки нажатия до текущей точки. Есть одна хитрость. По традиции, окно перетаскивается за его заголовок. Кстати да, я добавил ещё элемент "заголовок", потому что окна бывают и без заголовка. Но он не элемент верхнего уровня, поэтому надо перетаскивать не его, а родительское окно. Именно для этого в диспетчере запоминается индекс родительского элемента last_uid_index. Его мы и таскаем.

-14

Кроме самого окна, нужно перетащить все его дочерние элементы, и к счастью рекурсия здесь опять не нужна. Так как известно общее количество потомков, и все они лежат в массиве последовательно после окна, то просто проходим по массиву и к координатам каждого прибавляем dx и dy. После этого текущие координаты курсора запоминаем как последние origin_x, origin_y.

Драг работает отлично, но единственная проблема это выход части окна за пределы экрана. То, что рисуется с помощью SDL_FillRect(), отрабатывает нормально, но вот текст у меня выводится прямым доступом к видеопамяти. Поэтому, как только текст попадает за пределы SDL_Surface, программа падает :)

-15

Решить проблему можно просто: запретить окну перемещаться за край экрана. Ну или доработать вывод текста.

Дополнительная обработка

Некоторые элементы требуют дополнительной обработки. Это чекбоксы, переключение вкладок и счётчик. Да, уже работало в прошлой части, но сейчас были кое-какие изменения.

Напишем функцию GUI_dispatcher_process_item() пока только для чекбокса:

-16

Функция, как и ранее, переключает состояние чекбокса на противоположное. Я также добавил огромный чекбокс на первую вкладку, потому что могу вкладки ещё не переключаются:

-17

Добавим код для переключения вкладок, также из прошлой части, с необходимыми изменениями:

-18

Теперь можно переключать вкладки, и конечно тыкать в другие чекбоксы:

-19

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

Роутинг события в контроллер окна

Функция роутинга будет встроена в основной цикл программы, сразу после стандартной обработки события, но перед перерисовкой:

-20

Флаг dirty сигнализирует, что в результате обработки требуется перерисовка. Значит, что-то изменилось. На это мы и ориентируемся. В принципе сценарии могут быть разные. Скажем, ничего не изменилось, а роутинг нужен. Но пока так.

Также, роутинг в принципе можно вызывать после любого события, в том числе делать разный роутинг по типам. Но сейчас он необходим только для события отжатия кнопки.

Функция route_item() выясняет с помощью last_uid_index, в каком окне произошло событие, и запускает контроллер этого окна, также передавая ему соответствующую модель (пока всё в единственном экземпляре):

-21

Процессинг окна уже происходит по полностью произвольной логике:

-22

Здесь обновляется содержимое элемента со счётчиком с поиском его по ID, либо передаются состояния чекбоксов в модель.

Демонстрационное видео:

Код на гитхабе:

GitHub - nandakoryaaa/gui

Читайте дальше:

Наука
7 млн интересуются