В прошлом выпуске я сделал бесполезное окно, которое не содержит ни текста, ни кнопок.
Но торопиться добавлять дополнительные элементы в окно пока не будем, потому что нависает задача гораздо более важная.
Очевидно, что графический интерфейс это не просто где-то нарисованные квадратики, а целый комплекс объектов, у которого есть свой управляющий диспетчер. Можно вообще забыть про сам вывод графики и оставить только "мысленное" представление интерфейса. Для диспетчера должна быть важна следующая информация об объектах:
- Какого типа этот объект
- Где он расположен и какой у него размер
- Какой у него статус
- В каких отношениях он находится с другими объектами
Эти свойства одинаковы для любого объекта, и становится возможным выделить их в отдельную структуру, которой будет оперировать диспетчер.
Сделаем структуру GUI_Item с сопутствующими перечисляемыми типами для типа и статуса объекта:
Она описывает тип и статус экранного объекта, его обрамляющий прямоугольник, а данные объекта хранит как ссылку void* data.
Создав окно со своим наборов свойств, для диспетчера обернём его в структуру GUI_Item:
Можно сделать ещё две обёртки с тем же самым окном и с разными прямоугольниками:
И в результате это будет выглядеть так:
Это один и тот же экземпляр окна, отрисованный в разных обёртках. Если же нам нужны именно разные, независимые окна, мы просто создадим каждое отдельно. Игра ссылками на экземпляры окон открывает разные интересные возможности, но главное – не заиграться.
Вернёмся к проблеме. Как централизованно управлять всеми объектами интерфейса?
Очевидно, они должны храниться в виде списка, к которому имеет доступ диспетчер. Сделаем в качестве временной меры в контексте GUI_Context массив items на 1024, допустим, элемента, и счётчик item_cnt:
Положим в массив три элемента GUI_Item с окнами, которые ранее выводились на экран. Теперь надо просто в цикле пройти по массиву и нарисовать каждый элемент, поэтому не расписываю этот процесс.
На что стоит обратить внимание: как именно будут храниться элементы в массиве? Напишем минимально необходимый код добавления элемента в массив.
В функцию передаётся указатель на GUI_Item, но в массив помещаем не указатель, а копию содержимого указателя. Но ведь можно помещать в массив просто переданный указатель? Да, но какие проблемы это может вызвать?
- Путаница в указателях. Один и тот же указатель можно добавить в массив много раз, что противоречит логике интерфейса и неминуемо приведёт к его некорректному поведению.
- Проблема некорректных указателей. Ведь
мынерадивый программист может создать элемент на стеке внутри какой-нибудь функции, передать указатель на него, а затем выйти из функции и уничтожить элемент на стеке. При этом указатель на уже недействительную область памяти останется добавленным в массив. - Даже имея в распоряжении полностью корректный указатель,
мынерадивый программист может потом перезаписать в нём данные на совсем другие, в то время как диспетчер будет считать, что это структура GUI_Item.
А как поступил бы Rust?
Я иду немножечко путём Rust и делаю так, чтобы в массив добавлялся гарантированно уникальный новый элемент, не зависящий от внешних указателей. Защитить его от перезаписи я всё же не могу, так как это C, детка, но хотя бы на логическом уровне произошло разделение.
Проблемы, однако, только начинаются.
Допустим, мы создали элемент, добавили его в массив, а затем модифицировали его. Мы ожидаем, что изменения отобразятся на экране, но этого не происходит, так как в массив попал не сам элемент, а его копия. Чтобы увидеть изменения, нужно модифицировать копию в массиве.
Небольшое мысленное путешествие
Представим,что мы получили указатель на объект от самого диспетчера и теперь можем модифицировать объект. Этот указатель мы можем держать у себя неограниченно долго.
В то же время остальные объекты интерфейса могут рождаться (добавляться в список) или умирать (удаляться из списка). Следовательно, в списке диспетчера, где эти объекты хранятся, будут образовываться разрывы, с которыми надо что-то делать.
- Можно сдвигать элементы списка, убирая разрывы, но тогда у них поменяются адреса. И следовательно, тот указатель, который мы держим у себя, станет недействительным, но мы об этом не узнаем.
- Можно не трогать разрывы, чтобы не сдвигать элементы, но создавать новые элементы не в конце списка, а в этих разрывах. Что тоже не ведёт ни к чему хорошему, так как если элемент по указателю был удалён, а затем вместо него добавлен другой элемент, то теперь по указателю лежат совсем другие данные.
- Можно вообще ничего не трогать, но тогда банальное создание и удаление окна в бесконечном цикле сразу же исчерпает весь ресурс списка диспетчера.
Как видим, эта проблема в лоб не решается. Главное зло – указатель, но как от него избавиться? Указатель нам обязательно нужен, чтобы делать что-то с объектом. В то же время он в любой момент может оказаться некорректным.
По сути, указатели должны быть одноразовыми. Получили – внесли изменения – и после этого считаем, что указатель уже некорректный. Для следующих изменений его надо будет получить снова.
Принудить нас к этому язык C не может. Может быть лишь правило, контракт, которому мы должны осознанно следовать, а если не следуем – то сами дураки.
Но на деле решение есть, и его механизм такой же, как при добавлении элемента в список. Если при добавлении создаётся копия, то и при получении можно создавать копию и менять её. Но ведь уже было сказано, что изменение копии ни к чему не приведёт? Да, поэтому изменённую копию нужно снова добавить в массив, скопировав её при этом ещё раз!
Жизненный цикл объекта
- Объект создан
- Копия объекта добавлена в список диспетчера
- Объект запрошен у диспетчера для модификации. Диспетчер возращает его копию.
- Копия изменена
- Копия снова добавлена в список диспетчера, но не как новый объект, а на место старого
И вот мы подобрались к главному выводу всей этой истории. Чтобы можно было однозначно выбирать элемент из списка, а затем изменять его, нужно, чтобы его всегда можно было уникально идентифицировать. Указатель сам по себе уникален, но пользоваться им мы не можем из-за вышеописанных причин. Значит, у элемента должен быть некий уникальный ID, который никогда не поменяется и никогда не совпадёт с ID других элементов.
Уточним жизненный цикл объекта с использованием ID:
- Объект создан
- Копия объекта добавлена в список диспетчера, в ответ диспетчер вернул уникальный ID
- Для модификации запрашиваем у диспетчера объект с указанным ID. Диспетчер возвращает его копию.
- Копия изменена
- Копия снова добавлена в список диспетчера, но с тем же ID. Диспетчер находит объект с таким ID и заменяет его содержимое.
Таким образом, мы избавились от владения указателями. Все изменения происходят только в копиях, а непосредственно замену содержимого делает только и исключительно диспетчер по указанному ID. При этом, если ID не существует, или элемент с таким ID был уже удалён, или элемент с таким ID имеет не тот тип, диспетчер вернёт ошибку. Мы уже не сможем использовать никакие некорректные указатели.
Ещё раз уточним жизненный цикл объекта для более краткого варианта использования:
- Объект A создан
- Копия объекта A добавлена в список диспетчера, в ответ диспетчер вернул уникальный ID
- Меняем объект A и обновляем его копию в списке диспетчера сколько угодно раз, используя уже известный ID
Само собой, у диспетчера должен быть особый механизм хранения для того, чтобы генерировать уникальные ID и поддерживать порядок в массиве.
К счастью, это уже целиком проблема самого диспетчера, так что её мы рассмотрим дальше: