Много лет назад я написал графонистую игру Life, в которой можно было регулировать различные параметры:
Собственно, целью было не столько реализовать алгоритм Life, сколько потренироваться в создании графических интерфейсов. Я сделал такие элементы, как индикатор вкл/выкл, кнопка и ползунок.
Есть мнение, что делать графический интерфейс это если не самая сложная задача, то как минимум громоздкая и неприятная. Отсюда и использование разнообразных костылей вроде QT, tkinter. Как и следует ожидать, они сильно утяжеляют программу, и скоростью работы тоже не блещут.
В Life уже есть какой-то интерфейс, и он даже переносим, но он всё-таки не делался с целью быть универсальным. А почему игровому интерфейсу противопоказано быть универсальным, я писал здесь:
Сейчас же я поставил другую задачу, а именно сделать максимально универсальный интерфейс. Требования к нему будут такие же, как к стандартному GUI: иметь панель задач, иметь окна (одновременно несколько), окна могут сворачиваться и разворачиваться, в окнах могут быть текст, рисунки, кнопки, чекбоксы, выпадающие списки.
Естественно, не всё сразу. Для начала нужен MVP – Minimum Viable Product, или минимально работоспособный продукт.
Что это будет? Обычный прямоугольник, который должен быть нарисован на экране. По сути, все элементы интерфейса это прямоугольники. Окна, кнопки, панели, списки...
Самый минимум, что нужно иметь, это координаты и размеры прямоугольника. А вот далее мы начинаем ходить по оху очень тонкому льду под названием feature creep. Это когда в проект вместе с основными требованиями прокрадываются дополнительные, которые кажутся необходимыми в будущем или полезными, но приводят к усложнению и замедлению разработки.
Для дизайна графического интерфейса естественно быть монолитным, а не сборной солянкой. Все элементы обычно следуют одному стилю и поэтому проблема того, чтобы каждый из них был уникальным, надуманна.
Но я постарался максимально усложнить задачу, чтобы посмотреть, как можно её решить. Во-первых, элемент может быть разных цветов, иметь рамку или не иметь, сама рамка может быть разной:
Причём цвета должны должны быть минимум в двух вариантах: для активного и пассивного элемента.
Для начала определим нужные параметры. Это во-первых набор цветов: активный фоновый, пассивный фоновый, активный передний, пассивный передний, активный рамки, пассивный рамки и т.д.
Во-вторых это набор размеров рамки: толщина, форма, отступы.
В первой итерации добавим в структуру прямоугольника дополнительные поля со всеми этими параметрами. Пусть их будет с десяток или больше. Теперь можно каждый индивидуальный прямоугольник настроить по-своему, что отвечает поставленной задаче.
Но проблему уже видно: большое количество параметров. Чтобы сделать её ещё выпуклее, конкретизируем элемент интерфейса до окна. Внутри прямогольника окна есть ещё один, в котором написан заголовок:
Так как он может иметь свои цвета и рамки, логично его представить такими же параметрами, как и основной прямоугольник. То есть количество параметров для окна удваивается. Теперь это активный цвет фона, активный цвет фона заголовка, пассивный цвет фона, пассивный цвет фона заголовка, и т.д.
Представим, что нужно создать на экране 10 окон, и для каждого указать все эти параметры. Ведь мы же хотели универсальности и гибкости, ну так вот она. Что не нравится? Огромное количество рутинных действий, которые придётся совершить при создании окна.
Нормализация
Это термин из теории баз данных, и обозначает он удаление избыточных данных и замену их ссылками. Здесь мы тоже можем так поступить.
Сделаем семейство параметров, которое зададим как отдельную структуру.
Далее удалим все параметры из окна и оставим только ссылку на семейство параметров. Теперь мы можем один раз создать семейство параметров, а затем создать сколько угодно окон со ссылкой на этой семейство.
Но однажды почувствовав вкус крови нормализации, становится сложно остановиться.
В самом семействе параметров тоже есть избыточность. Предположим, что два окна отличаются только цветом рамки. Тогда нам нужно сделать по отдельному семейству с полным набором параметров для каждого окна, где будет меняться только один параметр, а остальные просто дублироваться.
Те параметры, которые дублируются, можно было бы выделить в отдельное семейство и также заменить ссылкой. Но проблема в том, что в разных сценариях использования получается разная избыточность и разные схемы деления.
Далее я опущу многочисленные рассуждения и попытки переделать параметры. Вы можете сами потренироваться в этом. Задача в том, чтобы разбить параметры на отдельные группы так, чтобы эти группы не были сильно связаны и каждую из них можно было редактировать независимо для большинства сценариев использования.
В результате у меня получился такой вариант.
Это семейство параметров цвета, которое описывает цвета фона и текста, а также светлый, тёмный цвета для рельефных элементов и цвет рамки.
Это семейство параметров формы, которое описывает размеры рельефных элементов, рамок, скругление и отступы.
Как видим, я разделил параметры на цвет и форму. Таким образом, с помощью комбинации этих параметров я могу задавать цвет и форму независимо друг от друга.
Теперь посмотрим, как этими параметрами будет пользоваться окно:
Ему нужно 3 описания цвета: для самого тела, для заголовка и для рамки. Умножим на два для активного и пассивного варианта. И два описания формы: для самого окна и для заголовка. Здесь активный и пассивный варианты не различаются.
Ну и добавлены ссылки на шрифт заголовка и окна, и название окна.
Предположим, мы создадим 6 структур GUI_Color и 2 структуры GUI_Shape, и тогда все окна будут ими пользоваться. Уже неплохо, но параметров всё равно получается много.
Даже если не надо задавать каждый цвет отдельно, всё равно для каждого окна нужно указать какие-то конкретные ссылки на какие-то конкретные структуры.
Контекст
Проблему можно решить, введя параметры по умолчанию. В нашем случае параметр по умолчанию обозначается отсутствием параметра, то есть ссылка будет равна NULL. Да, это язык C, мы любим жить опасно.
Откуда же брать параметры по умолчанию? Из некоего контекста:
Он содержит описание одного окна по умолчанию и одной кнопки по умолчанию (до кнопок мы тут не добрались, там примерно то же самое, но ещё больше). Кроме того, есть ссылка на шрифт по умолчанию. А также служебные поля для рендеринга SDL, которые по идее лучше отсюда убрать, но я не хочу усложнять повествование.
Теперь смотрите, чего можно добиться с помощью контекста: если создавать все окна с параметрами по умолчанию, то во-первых, мы можем везде писать NULL, не думая о том, какой именно параметр выбрать; во-вторых, просто поменяв один активный контекст на другой , мы полностью поменяем весь визуальный стиль.
Применение
Начнём с создания необходимых стилей цветов и форм:
Это цветовая схема окна, цветовая схема заголовка окна и форма окна. Создадим контекст, который их использует:
Сюда вкралось ещё и объявление кнопки, ну это на будущее. В общем, в контексте создан шаблон окна со всеми необходимыми параметрами.
Теперь создадим окно:
Легче лёгкого, так как все параметры по умолчанию, кроме заголовка.
Функцию рисования окна тут приводить пока не буду из-за её громоздкости, но суть, думаю, понятна:
- взять из параметров цвет фона
- если его там нет, взять из контекста
- нарисовать прямоугольник цветом фона
- взять из параметров цвет фона заголовка
- если его там нет, взять из контекста
- нарисовать прямоугольник цветом фона заголовка
- взять из параметров ссылку на шрифт заголовка
- ... и т.д.
Нудно, длинно, но совершенно несложно и пишется один раз.
В результате можно уже нарисовать несколько вполне приличных окон, использующих один и тот же контекст и не требующих индивидуальной настройки каждого параметра:
MVP получился пока что такой, а дальше будет дорабатываться и изменяться. Если вы были внимательны, то заметите, что не хватает одной детали: а где, собственно, задаются расположение и размеры окон?
Читайте дальше: