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

Графический интерфейс программы на языке C #7: Обработчики событий

Оглавление

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

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

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

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

Теперь после создания дерева объектов я сразу пересчитываю прямоугольники в абсолютные координаты. Это и так делалось при рекурсивном рисовании, просто результаты не сохранялись. Теперь можно не делать это при каждой перерисовке. Но менять исходный прямоугольник структуры GUI_Item нельзя, потому что может понадобиться пересчитать всё заново. Поэтому дополнительный прямоугольник, который я буду называть актуальным, хранится в структуре GUI_ItemRecord.

Для расчёта используется рекурсивная функция, схожая с функцией рисования, но попроще.

-2

Обратим внимание, что если ширина или высота прямоугольника равны нулю, то они наследуются от родителя. Так удобно.

После создания или изменения дерева нужно вручную вызывать перерасчёт:

GUI_dispatcher_push_tree(&dispatcher, &layout);
GUI_dispatcher_fix_rects(&dispatcher);

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

Структура GUI_ItemNode, которая содержала родительский прямоугольник и передавалась в рекурсивные вызовы, больше его не содержит, но сохранила свою роль связи с родителями. В таком виде она оказалась невостребована, но я оставил её для возможных будущих сценариев.

Также в структуру GUI_Item было добавлено поле id, по которому можно отличать элементы друг от друга.

-3

Таким ID мог бы служить порядковый номер элемента в дереве, но для логики приложения это неудобно, так как мы должны всегда следить за тем, на каком месте какой элемент стоит, а если поменять их местами, то изменятся и их идентификаторы. Поэтому вводятся собственные константы идентификаторов:

-4

Идентификаторы должны быть уникальны только в пределах корневой родительской формы, поэтому их можно переиспользовать в разных формах. Массив item_labels я добавил для вывода отладочной информации в читаемом виде. Каждая строка в массиве соответствует своему id.

Вот эти константы в инициализациях структур описания дерева:

-5

Итак, определим, куда попал клик мышью. Получим от SDL2 событие с типом SDL_MOUSEBUTTONDOWN (технически это не клик, а просто нажатие кнопки, ну неважно). Из события добудем координаты клика и отправимся в поиск.

-6

Структура GUI_TargetResult, аналогично структуре GUI_IndexResult, содержит код результата и индексы, во-первых, корневого родительского элемента (этот индекс берётся из массива uids в диспетчере), и во-вторых, самого дальнего потомка, в которого удалось попасть кликом.

-7

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

-8

Поиск начинается с корневых элементов из массива uids. А вот и рекурсивная часть:

-9

Первым делом проверяем статус элемента, чтобы он был видимым и активным (это битовая маска GUI_STATUS_VA). К примеру, видимы и активны только элементы в активной вкладке, а в неактивных вкладках мы ничего не видим и не проверяем.

Если произошло попадание в прямоугольник элемента, то запоминаем его индекс, и дальше рекурсивно проверяем его детей. Если попадём в кого-то из детей, запомним его индекс, и т.д. Функция вернёт последний найденный индекс.

Обработка события диспетчером

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

-10

Особый случай обработки это переключение активной вкладки, что мы сейчас и рассмотрим.

-11

Любой кликнутый элемент поступает в единую функцию GUI_process_item(). Там они разделяются по типам, и каждый тип обрабатывается по-своему. На данный момент есть только обработка GUI_TabGroup. У этой структуры есть параметр высоты заголовка вкладки head_h. Дело в том, что сама группа вкладок имеет прямоугольник максимально доступного размера, чтобы служить контейнером для детей. Но попадание в этот прямоугольник требует специальной обработки только тогда, когда мы попали именно в заголовок вкладки. То есть локальная координата клика local_y не должна превышать head_h. При попадании в область заголовков нужно далее узнать конкретный заголовок. У группы вкладок есть ширина прямоугольника и количество детей, которые и есть вкладки. Таким образом, можно вычислить ширину одного заголовка как общую ширину, поделённую на количество детей. Далее разделить local_x на эту ширину, и мы получим порядковый номер заголовка, в который попали.

Этот номер мы назначим как текущую вкладку в поле tg->current. После чего пройдёмся по детям и назначим им видимые или невидимые статусы в соответствии с номером.

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

-12
-13
-14

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

Допишем прямо сейчас обработку чекбокса. Ему нужен статус состояния "вкл-выкл". Поэтому добавим такую маску в типы масок GUI-статусов:

-15

Сама обработка заключается в том, чтобы инвертировать текущий статус чекбокса (для этого используем XOR по маске):

-16

Обратим внимание, что изменяется не item.status, а irec->item.status, потому что item это копия irec->item для удобства. Изменение копии не приведёт к изменению оригинального элемента.

Вот и вся обработка. Дополнительно надо учесть это при рисовании, так как раньше не учитывалось:

-17

И вот мы уже можем наслаждаться кликами по чекбоксам в разных вкладках:

-18
-19
-20

Осталось привязать элементы к логике приложения.

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

-21

Я более не использую приставку GUI_, так как теперь мяч на стороне приложения.

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

Роутер может опознать окно по ID корневого элемента.

-22

И опознав, он в соответствии со своими правилами маршрутизации (которые сейчас реализуются тупо через if) направит обработку в назначенный этому окну контроллер process_win1(). Туда опять же передаётся вся информация, плюс указатель на модель model_win1, связанную с этим окном.

Ранее мы говорили про модели, что они содержат данные формы. Сделаем такую модель для окна ID_WIN1:

-23

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

Первый вариант обработки:

-24

Значения ivalue, которые хранятся в элементах GUI_Checkbox, записываются в соответствующие поля модели. Таким образом, мы успешно завершили обработку события и теперь наша модель отражает некое состояние программы. При этом, если чекбокс выключен, из него возвращается нулевое значение. В определённых случаях мы можем фильтровать данные по нулевому значению (или вообще по любому специфическому значению), поэтому такой вариант допустим, но не универсален.

Можно добавить в модель флаги включённых чекбоксов:

-25

И в обработчике их включать или выключать, при этом передавая значение чекбокса как есть:

-26

Следующим шагом можно задуматься, а нужны ли нам вообще значения чекбокса, хранимые в GUI_Checkbox? Ведь они прекрасно могут изначально храниться в модели, а чекбокс будет их только включать и выключать.

Да, в каких-то случаях можно и так. В целом это вопрос удобства или целесообразности, так что задерживаться здесь не будем.

А вот на что стоит взглянуть, это на сам перебор чекбоксов. В текущем виде он состоит из трёх if-ов, но есть ещё две вкладки, в каждой ещё по 3 чекбокса, то есть всего понадобится 9 условий. Код станет громоздким.

Можно ли с этим бороться? Да, в этом случае прямые указатели из GUI_Item на данные модели были бы спасением. Но так как я сознательно отказался от указателей, есть только один вариант:

Терпеть

Что ж, для начала немного поменяю структуру модели:

-27

Я заменил отдельные поля на массивы из трёх элементов. Теперь обработчик:

-28

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

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

-29

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

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

Однако можно сделать некий промежуточный вариант: хранить ID и указатели на данные, как ранее предполагалось, но внутри самой модели. Тогда указатели будут жить ровно столько, сколько живёт сама модель (спасибо, Rust).

-30

А в обработчике банально перебирать ID в цикле и искать совпадения. Это формально тоже хэш-таблица с уровнем коллизий в 100%, но при относительно малом количестве элементов и отсутствии лишнего обвеса работать будет наверно даже быстрее.

-31

Проверим, что получилось. При клике на чекбоксы в первой вкладке должны меняться данные модели. Вывод значений и состояний:

-32

Результат:

-33

Короче, работает.

Текущий код выложен на гитхаб:

GitHub - nandakoryaaa/gui

Также можно посмотреть видео работы:

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

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