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

Графический интерфейс программы на языке C #5: Рекурсивное рисование

В предыдущей части были рассмотрены различные способы описания элементов GUI.

Приведу финальный вариант, где все элементы описаны в виде одной большой древесной структуры, похожей на JSON:

На деле я добавил туда ещё две вкладки, что должно привести к рисованию примерно такого окна:

-2

Я также переделал добавление дерева в список объектов диспетчера. Раньше каждый элемент дерева содержал количество прямых потомков и общее количество всех вложенных потомков. Сейчас он содержит только количество прямых потомков, а общее считается во время добавления. Так как изменилось соответствие индексов между UID и объектами, я ввёл две новые структуры для записи в массивы диспетчера:

-3

Структура UIDRecord записывается в массив uids и содержит UID объекта и его индекс в массиве items. Структура ItemRecord записывается в массив items и содержит объект item, количество его прямых потомков child_cnt и общее количество потомков subtree_cnt, которое и высчитывается автоматически.

Само собой, изменились функции добавления:

-4

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

Рекурсивная функция добавления:

-5

Делается ранний прогноз возможности добавления по количеству прямых потомков. Если он прокатывает, то добавляется сам объект, затем рекурсивно его потомки. После каждого рекурсивного вызова накапливается счётчик общего количества потомков, он и кладётся в массив вместе с объектом.

Для проверки распечатал получившийся список диспетчера:

-6

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

Функции удаления аналогично усложнились, но я их даже пока не делал.

Как рисовать?

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

На данный момент каждый объект интерфейса прибит гвоздями. Задав координаты элемента, можно рисовать его автономно и независимо от других. А что это значит? Если, к примеру, сдвинуть какой-то элемент ниже, то остальные останутся на своих местах. То есть если я хочу перенести всю группу из трёх чекбоксов вниз, нужно изменить координаты каждого из них.

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

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

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

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

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

Рассмотрим возможные комбинации трёх вкладок:

-7
-8
-9

Вопрос: кто должен рисовать заголовки вкладок? Допустим, это делает сама вкладка. В таком случае она должна знать, сколько ещё есть вкладок, и какое место среди них она занимает.

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

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

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

Итак, для рисования элемента нужно иметь следующие данные:

  • Сам элемент
  • Его индекс
  • Количество потомков
  • Ссылка на родительский элемент с такими же данными

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

Сделаем для этого струтуру типа GUI_ItemNode:

-10

Зачем здесь ещё участвует GUI_Rect (алиас для SDL_Rect)? Это как раз базовый прямоугольник для рисования потомков относительно него. Координаты потомков будут складываться с координатами этого прямоугольника.

Именно это приводит нас к тому, что рисование должно быть рекурсивным. Каждый родительский контекст задаёт свои базовые координаты.

Рисование начинается с верхнего уровня:

-11

Здесь создаётся первичный родительский контекст, у которого нет ссылки на родителя. Элементы берутся по индексам из списка uids, и передаются на рисование в рекурсивную функцию. Она длинная, поэтому разберём её по частям:

-12

Из node мы получаем индекс элемента item_index. Затем по этому индексу получаем запись об элементе ir. Если элемент невидим, то ничего не делаем. Заметим, что это делает автоматически невидимыми всех его детей – погружения в рекурсию не происходит.

Затем, в зависимости от типа элемента, вызывается специфическая функция рисования. Это нормально, так как каждый тип выглядит по-разному и следовательно рисовать его надо по-разному.

После отрисовки элемента проверяем, есть ли у него дети:

-13

Если есть, формируем очередную запись GUI_ItemNode о родителе, вставляя туда указатель на предыдущую запись, формируем для неё новые координаты прямоугольника, и для каждого потомка вызываем функцию рекурсивно.

Индекс каждого следующего потомка получаем, прибавляя к текущему индексу 1 (это сам элемент) и subtree_cnt (это количество всех вложенных потомков).

Ну и в целом это всё. Можно рассмотреть рисование группы вкладок, так как оно нетривиально. Само рисование я заменю на printf(), чтобы не загромождать:

-14

Используя собственный индекс, группа проходит по индексам своих детей, которые расположены после неё. Из детей она берёт их названия. Она выводит сначала те вкладки, которые расположены до активной, затем активную, затем те, которые после активной. Они все рисуются по-разному, поэтому так. Вот лог рисования:

-15

Ну и для примера тривиальное рисование чекбокса:

-16

Всё вместе получается вот так:

-17

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

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

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