qt-items — новый фреймворк, или попытка найти Теорию Всего
Как известно, физики давно пытаются найти Теорию Всего, в рамках которой можно было бы объяснять все известные взаимодействия в природе. Склонность к обобщениям присуща не только физикам, но и математикам, и программистам. Способность меньшим количеством сущностей объяснять и предсказывать большой спектр явлений очень ценна. Для программистов в роли теорий выступают различные API и фреймворки. Некоторые из них решают узкоспециализированные проблемы, а какие-то претендуют на роль универсальных теорий. Примером последних может выступать Qt — универсальный фреймворк, предназначенный, в основном, для разработки GUI.Далее я расскажу, что мне не нравится в Qt и как его можно сделать ещё более универсальным, мощным и удобным для работы.
Демо видео (лучше смотреть в HD).[embedded content]Qt, как и многие другие GUI фреймворки развивался от простого к сложному. Сначала создавались простые виджеты, потом более сложные и составные. Появился Model/View framework, для отображения данных в табличном или древовидном виде. Появился Graphics Items framework для отображения набора графических элементов. Все эти фреймворки имеют различные API и несовместимы друг с другом. По сути у нас есть три независимых и почти не пересекающихся теории в рамках одной большой. Когда мне нужно разработать какой-либо новый визуальный элемент, то я должен выбрать, в каком из трёх фреймворков я собираюсь его использовать и применять соответствующее API. Таким образом я не могу создать элемент, который можно было бы использовать и в качестве отдельного виджета, и внедрить в ячейки таблицы, и использовать в узлах графической сцены.
Qt развивается под лозунгом — Write once, run anythere. Для написания конечных приложений это может быть и правда, но для расширения и кастомизации самой библиотеки это не так.
Давайте подумаем, как должны быть устроены виджеты, что бы библиотека Qt стала по-настоящему единой и мощной.Рассмотрим разные виджеты (чекбокс, таблица, дерево и графическая сцена) и постараемся найти в них что-то общее. Информация в них сгруппирована в ячейки (Items). Чекбокс состоит из одной ячейки, таблица — из рядов и столбцов ячеек, в сцене ячейками являются узлы. Таким образом можно сказать, что все виджеты отображают ячейки, только их количество и расположение в пространстве специфичны для разных типов виджетов. Давайте скажем, что виджет отображает некоторое пространство ячеек (Space). Для простых виджетов пространство ячеек тривиально SpaceItem, и состоит из единственной ячейки. Для таблицы можно придумать SpaceGrid, которое описывает, как ячейки организованы в строки и столбцы. Для графической сцены имеем SpaceScene, где ячейки могут располагаться как угодно.
Что есть общего у всех пространств, что можно выделить в базовый класс? Пока что, можно выделить две вещи:
Возвращать общий размер пространства (обычно это bounding box всех ячеек) Возвращать расположение ячейки по её координате ItemID class Space { virtual QSize size () const = 0; virtual QRect itemRect (ItemID item) const = 0; }; Давайте теперь внимательно рассмотрим сами ячейки. Для наглядности будем изучать такую таблицу:
Ячейки тоже имеют некоторую структуру. Например, чекбокс состоит из квадратика с галочкой и текста. В таблице ячейки могут быть очень сложными (содержать текст, картинки, ссылки, как в моём видео-примере). Заметим, что для таблицы у нас, как правило, ячейки в одном столбце имеют одинаковую структуру. Поэтому нам легче описывать не каждую ячейку, а целый набор. Наборы ячеек (Range) могут быть разными, например, все ячейки RangeAll, ячейки из колонки RangeColumn, ячейки из строки RangeRow, ячейки из четных строк RangeOddRow и т.п. Какой же интерфейс можно выделить для базового класса Range? Интерфейс простой и лаконичный — отвечать на вопрос, входит какая-то ячейка в Range или нет:
class Range { virtual bool hasItem (ItemID item) const = 0; }; После того, как мы определились с подмножеством ячеек, нам надо указать, какой тип информации в этих ячейках мы хотим отобразить. За отображение самого маленького и неделимого кусочка информации будет отвечать класс View. Например, ViewCheck умеет отображать значок чекбокса, ViewText — отображает строку текста и т.п.Пока что базовый класс View должен уметь лишь рисовать информацию в ячейке: class View { virtual void draw (QPainter* painter, ItemID item, QRect rect) const = 0; }; Возникает вопрос, откуда ViewCheck знает, что ему надо рисовать значок слева в ячейке, а ViewText знает, что ему нужно рисовать текст после значка чекбокса? Для этого заведем ещё один «карликовый» класс Layout. Этот класс умеет размещать View внутри ячейки. Например, LayoutLeft разместит View у левого края ячейки, LayoutRight — у правого, а LayoutClient — займёт всё пространство ячейки. Вот базовый интерфейс: class Layout { virtual void doLayout (ItemID item, View view, QRect& itemRect, QRect& viewRect) const = 0; }; Функция doLayout изменяет параметры itemRect и viewRect так, что бы расположить view внутри ячейки item. Например, LayoutLeft запрашивает размер, необходимый view для отображения информации в ячейке, и «откусывает» необходимое пространство от itemRect. Как видно, от интерфейса View требуется еще одна функция — size: class View { virtual void draw (QPainter* painter, ItemID item, QRect rect) const = 0; virtual QSize size (ItemID item) const = 0; }; В итоге, чтобы описать что и как мы хотим отображать в ячейках некоторого пространства, нам надо перечислять тройки объектов tuple. Такую тройку я назвал ItemSchema. Полностью наш класс Space выглядит примерно так: class Space { virtual QSize size () const = 0; virtual QRect itemRect (ItemID item) const = 0;
QVector
Данная архитектура располагает к сотрудничеству программистов. Кто-то может написать свой тип пространства ячеек, который укладывает ячейки каким-то специальным образом. Кто-то напишет View, который отображает специфичные данные. И эти программисты могу воспользоваться результатом работы друг друга. Вот не полный список моих реализаций класса View, их легко создавать и использовать (реализация буквально несколько строк кода):
ViewButton — рисует кнопку ViewCheck — рисует значок чекбокса ViewColor — заливает область определенным цветом ViewEnumText — рисует текст из ограниченного списка ViewImage, ViewPixmap, ViewStyleStandardPixmap — рисуют изображения ViewLink — рисует текстовые ссылки ViewAlternateBackground — рисует через-полосицу ViewProgressLabel, ViewProgressBox — рисуют прогрессбар или проценты ViewRadio — рисует значок радиобаттона ViewRating — рисует значки оценки ViewSelection — рисует выделенные ячейки ViewText — рисует текст ViewTextFont — меняет шрифт последующего текста ViewVisible — показывает или скрывает другой View Идём дальше. Как правило, виджет отображает не всё пространство ячеек, а только видимую часть. Класс Space удобен для описания пространства ячеек, но плох для отрисовки ячеек в некоторой ограниченной видимой области. Давайте определим специальный класс для отображения под-области пространства CacheSpace:
class CacheSpace { // reference to items space Space space; // visible area QRect window; // draw cached items void draw (QPainter* painter) const; // visit all cached items virtual void visit (Visitor visitor) = 0; }; Каждый конкретный наследник от CacheSpace (CacheGrid, CacheScene и др.) хранит набор кешированных ячеек CacheItem по-разному (но оптимально для данного типа пространства). Поэтому мы выделим в базовом классе функцию visit, которая посещает все кешированные ячейки. С помощью неё легко реализовать функцию draw — просто нужно посетить все кешированные ячейки и вызвать у них свою функцию draw.Как понятно из названия, CacheItem хранит всю информацию, нужную для отображения конкретной ячейки:
class CacheItem {
ItemID item;
QRect itemRect;
QVector
void draw (QPainter* painter) const; }; Здесь функция draw устроена тоже очень просто — в цикле вызвать draw у класса CacheView, который отвечает за отрисовку самого маленького и неделимого кусочка информации внутри ячейки. class CacheView { View view; QRect viewRect;
void draw (QPainter* painter, ItemID item) const; }; Таким образом, виджету необходимо иметь CacheSpace и с помощью него рисовать содержимое своего пространства ячеек:
class Widget { // space of items Space space; // cache of visible area of space CacheSpace cacheSpace;
void paintEvent (QPaintEvent *event) override; void resizeEvent (QResizeEvent *event) override; }; В обработчике resizeEvent мы меняем видимую область объекта cacheSpace.window, а в обработчике paintEvent — рисуем его содержимое cacheSpace.draw ().Как видно, иерархия объектов CacheSpace→CacheItem→CacheView позволяет нам «видеть» всю визуальную структуру виджета с максимальными подробностями. Мы можем доступиться к любому самому маленькому и неделимому кусочку информации, спускаясь с уровня CacheSpace на уровень отдельной ячейки CacheItem и, далее, внутри ячейки перебирая отдельные CacheView.
Эта возможность, представить любой виджет, как иерархию CacheSpace→CacheItem→CacheView, даёт нам большие возможности по управлению и интроспекции виджета.
Например, мы можем реализовать единый интерфейс доступа к любому нашему виджету из системы автоматического тестирования. Система автоматического тестирования GUI обычно запрашивает необходимую область в виджете и потом воздействует на эту область мышью, имитируя действия пользователя. Мы можем предоставить такой системе самую подробную «карту» областей, на которые можно воздействовать.
Другой пример — анимации, которые представлены в видео-примере. Мы можем не только смотреть, из чего состоит наш виджет, но и воздействовать на его составные части. Для примера, можно менять расположения любых объектов в иерархии (CacheSpace→CacheItem→CacheView) во времени или отрисовывать их с полупрозрачностью. Таким образом, можно собирать целую библиотеку анимаций, которые могут быть применены на любой виджет и на любое пространство ячеек.
В итоге, хочу еще раз перечислить, в каких направлениях можно кастомизировать данную библиотеку:
Space — можно создавать свои типы пространства ячеек CacheSpace — можно создавать новые типы отображения пространств, например, реализовать CacheSpaceCourusel — отображать список ячеек в виде карусельки View — создавать новые виды визуализаций для ячеек Animation — создавать новые анимации Данная заметка является продолжением предыдущих двух: здесь и здесь. Проект qt-items является реализацией идей из этих заметок.
Идей и задач по дальнейшему развитию еще много, так что оставайтесь на связи.