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

Графический интерфейс программы на языке C #6: Управление событиями

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

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

Пора сделать элементы интерфейса интерактивными. Допустим, чтобы они реагировали на клик.

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

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

Но есть целых три нюанса.

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

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

В-третьих, сам по себе такой клик смысла не имеет. Да, мы можем нарисовать, как нажалась кнопка, но это не принесёт никакой пользы. У каждого действия в интерфейсе должна быть цель. К примеру, нажатие на кнопку увеличивает счётчик. Где находится этот счётчик и как его привязать к именно этой кнопке? Вот главная задача.

Модели

Интерфейс типично строится из набора окон, а если говорить точнее, форм (по аналогии с HTML form), где каждая форма по-своему уникальна. Она содержит определённое количество кнопок (будем для краткости упоминать только кнопки), и каждая кнопка предназначена для чего-то.

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

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

-2

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

Мы можем связать эти данные с GUI, сделав в типе GUI_Item дополнительное поле с указателем на данные. Назовём его, к примеру, data_ptr. Тогда одна кнопка будет иметь указатель data_ptr = &model.cnt1, а другая &model.cnt2.

Для того чтобы понять, что конкретно делать при нажатии кнопки, нужно иметь ещё одно поле handler – указатель на функцию-обработчик. К примеру, сделаем функцию button_cnt_handler():

-3

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

Нормально ли то, что мы должны расширить структуру элемента GUI_Item, добавив туда указатели на обработчик и на данные? Вообще да, потому что именно так работает, к примеру, JavaScript:

item.onсlick = function()...

То есть у объекта item есть поле onclick, которому присваивается указатель на некую функцию. Если нам покажется, что мы чрезмерно раздуваем структуру GUI_Item, то можно ткнуть палкой в тот же JS, где у объекта одних только on-полей... несколько десятков, не считая всего остального.

-4

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

Например, нужно сделать так, чтобы при увеличении одного счётчика другой уменьшался, и наоборот. Имея указатель на cnt1, мы увеличим его, но у нас не будет указателя на cnt2, чтобы его уменьшить. Тогда можно сделать указатель на всю модель, и если раньше функция-обработчик была одна, а указатели на данные разные, то теперь функции будут разные, а указатель на данные один:

-5

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

Но меня по-прежнему беспокоят указатели, которые создают опасную зависимость между объектами GUI и данными. Это всё из-за Rust!

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

Тогда указатели не нужны. Вместо этого достаточно опознавать элементы по какому-либо идентификатору. Так, первой кнопке в форме можно назначить идентификатор 1, а второй 2. Тогда функция обработки будет иметь следующий вид:

-6

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

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

Шаблоны проектирования #2: MVC
ZDG27 августа 2020

Контуры будущего обработчика уже обрисовываются, но нужно учесть ещё одну вещь.

Рассмотрим клик на неактивной вкладке. Что при этом происходит? Неактивная вкладка должна стать активной, а активная – неактивной. Изменится и их внешний вид. Кто должен этим заниматься? Уж точно не наш контроллер, ведь ему без разницы, где элементы расположены на экране и как они выглядят, он знает только про модель.

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

  • Клик по заголовку вкладки
  • Находим целевой элемент – группу вкладок
  • Вызываем для группы вкладок стандартный GUI-обработчик
  • GUI-обработчик изменяет статусы GUI-элементов, чтобы они перерисовались правильно
  • Передаём событие дальше в контроллер

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

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

-7

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

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

typedef enum { GUI_EVT_WINDOW_CLOSE } GUI_Event;

Теперь мы можем расширить структуру GUI_Item и сделать в ней поле event_type. Это поле мы можем оставлять пустым, либо назначать ему конкретный тип. В нашей ситуации мы назначим кнопке, которая должна закрывать окно, event_type = GUI_EVT_WINDOW_CLOSE.

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

Данные, которые вернулись, уже ни к чему не привязаны. Мы не обязаны сразу же вызывать какой-то обработчик. У нас есть структура, которую можно использовать в любой момент.

И когда такой момент наступит, мы для начала используем общий контроллер, который выполняет универсальные действия. Так, если этот контроллер видит событие GUI_EVT_WINDOW_CLOSE, то у него есть вся необходимая информация, чтобы закрыть целевое окно и больше ничего не выяснять.

Аналогичным образом для инкремента счётчика мы можем сделать собственное логическое событие GUI_EVT_COUNTER_INCREMENT, и обработать его общим контроллером.

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

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

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

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

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