В прошлом выпуске мы дошли до момента, когда нужно уже добавлять в объекты интерфейса дочерние объекты.
Окно может содержать много элементов совершенно разных типов:
Задача в первую очередь не в том, как их запрограммировать, а в том, как их задать. Сразу видно, что описание подобной гигантской структуры отнимет много времени и сил, но есть ли выход? Ведь эти элементы сами собой не появятся, сами себя не расставят по нужным местам, сами себя не назовут. Значит, неприятную работу всё же придётся сделать. И мы можем лишь подумать над тем, как её немного облегчить.
Для примера возьмём структуру из прошлой части, которая попроще, но в то же время имеет достаточную глубину:
И чтобы было ещё проще, рассмотрим только обведённую красным часть.
Парсинг
Самый лёгкий вариант это использовать вместо императивного языка декларативный. Это может быть XML, JSON или что-то своё. К примеру, опишем структуру в JSON:
Весьма удобно и наглядно, и тем не менее мне пришлось записать часть параметров в одну строку, чтобы листинг хотя бы уместился в страницу, а ведь мы даже ещё не приступали к чему-то серьёзному.
Данный формат надо будет парсить – то есть синтаксически разбирать – и переводить уже в программные структуры для использования. Написать или найти готовый парсер это не такая уж проблема, но ниже мы увидим, что необходимости в этом скорее всего не будет.
Последовательное императивное построение
Я на всякий случай заглянул в документацию к QT и tkinter, ну и конечно никаких чудес там нет. К примеру, сначала создаём окно, что на псевдокоде выглядит так:
win = new Window();
Затем можно настроить параметры окна, например заголовок и геометрию:
win.setTitle("Hello World");
win.setRect(0,0,100,100);
Затем можно добавить в окно кнопку:
btn = new Button();
win.addButton(btn);
ну и т.д. В оригинале там даже больше возни, это я ещё упростил.
Если посчитать количество необходимых инструкций, чтобы построить наше окно, оно опять же окажется довольно большим, зато такую запись выгодно использовать, когда добавляется много одинаковых элементов. Тогда можно устроить цикл и за его счёт сократить количество инструкций.
Инициализация структур при создании
Парсинг приводит к появлению структуры, заполненной данными. Последовательное построение приводит к появлению такой же структуры. Суть в том, чтобы пропустить эти действия и сразу создать структуру с данными в уже готовом виде.
Чтобы построить дерево, нужно описать тип для узла дерева. Так как дерево определяется рекурсивно, этот тип должен ссылаться сам на себя.
Структура ItemTree содержит объект интерфейса item, количество потомков у этого объекта child_cnt, общее количество вложенных потомков subtree_cnt, и указатель на массив потомков subtree.
Обратите внимание, как рекурсивно определить тип, который ещё не объявлен. Структура struct GUI_ItemTree имеет поле типа GUI_ItemTree, который был объявлен ранее с использованием этой же структуры, которая ещё не была объявлена. Но накинуть typedef на несуществующую структуру можно, потому что по сути это просто алиас, подобный #define, который всего лишь говорит, что GUI_ItemTree нужно заменять на struct GUI_ItemTree там, где он встречается. А если не встречается – ну, ничего страшного.
Количество потомков нужно для того, чтобы знать, массив какой длины находится по указателю subtree. А общее количество всех вложенных потомков необходимо знать, чтобы диспетчер мог переходить по своему списку от одного узла дерева к другому, не тратя время на перебор их потомков. Пока что я указываю его руками для простоты, в дальнейшем оно будет считаться автоматически.
Засучим рукава
Внимание, дальше мы очень нудно пройдём по каждому этапу подготовки данных, чтобы понять, как это сделать удобнее.
Так как записи содержат ссылки, надо сначала подготовить те данные, на которые делается ссылка. В нашем случае мы должны начать с чекбоксов. Сразу добавим дополнительные типы UI-объектов:
Теперь опишем структуру чекбокса. У него должен быть id, который отличает его от других, должна быть текстовая метка label, и должно быть значение, которое он возвращает. Так как значение может быть любого типа, я сделал два поля для него: ivalue это числовое значение, а svalue строковое. Логика использования: если строковое не равно NULL, берём его, иначе берём числовое.
Теперь создадим три нужных нам экземпляра чекбокса:
GUI_Checkbox cb1 = { 1, "checkbox 1", 1, NULL };
GUI_Checkbox cb2 = { 2, "checkbox 2", 2, NULL };
GUI_Checkbox cb3 = { 3, "checkbox 3", 3, NULL };
Теперь оформим первый чекбокс в виде элемента дерева:
Здесь внутри GUI_ItemTree инициализируется структура GUI_Item с данными чекбокса, и задаётся количество потомков и указатель на них. Чекбокс это финальный элемент, поэтому там всё по нулям.
Кстати, в стандарте C99 инициализацию структуры можно написать и с именами полей, чтобы было понятнее, какое значение для чего:
И это уже похоже на вышеупомянутый JSON, не так ли?
Аналогичным образом мы должны создать элементы дерева из двух других чекбоксов, они будут называться соответственно it_cb2 и it_cb3.
Все три чекбокса являются потомками родительского элемента – группы. Его тип ещё не описан, поэтому надо описать:
Я назвал его GenericGroup, потому что такая структура может использоваться в разных контекстах, что мы увидим позже. Сама группа имеет только свойства заголовка title и шрифта font. Набор свойств мог бы включать и цветовую схему, и форму рамки, как у окна, но на данный момент это неважно.
Создадим экземпляр группы и уже привычно завернём его в GUI_ItemTree:
Можно видеть, что у этой группы 3 потомка, всего потомков тоже 3, так как вложенных нет, и есть указатель на массив it_cb_list, который должен содержать три ранее описанных чекбокса it_cb1, it_cb2, it_cb3. Обратим внимание на то, как формируется этот массив. Экземпляры чекбоксов уже есть, их надо положить в массив. В таком виде класть нельзя:
GUI_ItemTree it_cb_list[] = { &it_cb1, &it_cb2, &it_cb3 };
потому что это будет массив указателей, а не массив самих чекбоксов. Если же написать так:
GUI_ItemTree it_cb_list[] = { it_cb1, it_cb2, it_cb3 };
тогда всё будет правильно, но чекбоксы будут скопированы в массив. Мы можем избежать лишнего копирования, если проинициализируем чекбоксы сразу в виде массива:
Теперь нам не нужны отдельные экземпляры it_cb1, it_cb2, it_cb3, так как они уже в массиве. Остались указатели на данные чекбоксов cb1, cb2, cb3. Можно ли эти данные тоже создавать не отдельно, а сразу по месту? Посмотрим:
Вместо указателей &cb1, &cb2, &cb3 используем указатели на структуры, которые инициализируются прямо по месту:
&(GUI_Checkbox) { 1, "checkbox 1", 1, NULL }
Пойдём дальше и не будем писать в группе указатель на it_cb_list, а вставим весь массив прямо в описание группы:
Обратите внимание на предпоследнюю строчку, где написано }[0]. Это закончилось описание массива и затем был взят его первый элемент. Указатель GUI_ItemTree* указывает на этот первый элемент.
Если мы сделаем указатель просто на массив, а не на его первый элемент, программа всё равно будет работать, но компилятор выдаст предупреждение из-за несовпадения типа указателя: указатель имеет тип GUI_ItemTree*, а массив имеет тип GUI_ItemTree[], и хотя с точки зрения адресов в памяти это одно и то же, компилятор всё-таки считает, что это непорядок.
Итак, в предыдущем листинге нам удалось создать аж целую группу с тремя потомками-чекбоксами, использовав одно длинное выражение, а не кучу инициализаций отдельных объектов. И такая запись весьма похожа на JSON, только со своими нюансами.
Теперь осталось группу вставить во вкладку, вкладку вставить в группу вкладок, а группу вкладок вставить в окно.
Создадим тип элемента для группы вкладок:
У него нет ничего, кроме номера текущей выбранной вкладки current.
Далее нужен тип для вкладки, но он уже есть, это GUI_GenericGroup, так как вкладка это просто группа.
Создаём вкладку, которая содержит вышеописанную группу с тремя чекбоксами:
У этой вкладки один потомок – группа чекбоксов, а всего потомков 4, потому что группа содержит ещё три потомка.
Создаём группу вкладок, куда добавляем только что созданную вкладку:
Здесь 1 прямой потомок и всего 5 потомков. Естественно, что вместо ссылок на it_group1 и tabgroup мы можем вставить описания структур прямо по месту, как это проделывали раньше. Но сам принцип уже понятен, так что это просто вопрос удобоваримости листинга.
Создаём экземпляр окна и превращаем его в дерево. Единственным потомком окна является it_tabgroup, а всего потомков 6.
Искомое дерево получено. Много ли строк кода для этого написано? Да, много, но сравнимо с тем, что мы бы написали даже в самом минимальном виде. Для порядка можно будет добавить методы последовательного построения, и дело в шляпе.
Разложение дерева
Теперь все элементы дерева нужно добавить как отдельные GUI_Item в список диспетчера. Чтобы соблюсти правильный порядок отрисовки, последовательность элементов в списке должна получиться такая: окно, группа вкладок, вкладка, группа, чекбокс, чекбокс, чекбокс.
Код для добавления дерева в список диспетчера:
Используется рекурсивная функция, которая добавляет элемент, а затем для каждого потомка вызывает сама себя.
Добавляем окно-дерево в список:
GUI_dispatcher_push_tree(&dispatcher, &it_win_front);
Для проверки я сделал диагностическую функцию, которая печатает список и дополнительно печатает свойства чекбоксов, если найдёт их:
Вот что получилось:
Все элементы расположились в списке в нужном порядке и готовы к отрисовке.
Но теперь нарушилось соответствие индексов для массивов uids и items в диспетчере. Произошло это потому, что раньше UID шли в массиве uids друг за другом и элементы в items тоже шли друг за другом, то есть каждый элемент имел свой UID. Теперь после корневого элемента в items вставляются его потомки, у которых нет своих UID, поэтому индексы начинают расходиться.
Эта проблема решается, поэтому поправлю всё в следующем выпуске, где можно будет уже отрисовать всё как положено: