Как известно, физики давно пытаются найти Теорию Всего, в рамках которой можно было бы объяснять все известные взаимодействия в природе. Склонность к обобщениям присуща не только физикам, но и математикам, и программистам. Способность меньшим количеством сущностей объяснять и предсказывать большой спектр явлений очень ценна. Для программистов в роли теорий выступают различные API и фреймворки. Некоторые из них решают узкоспециализированные проблемы, а какие-то претендуют на роль универсальных теорий. Примером последних может выступать Qt — универсальный фреймворк, предназначенный, в основном, для разработки GUI.
Далее я расскажу, что мне не нравится в Qt и как его можно сделать ещё более универсальным, мощным и удобным для работы.
Демо видео (лучше смотреть в HD).
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<Range, View, Layout>. Такую тройку я назвал ItemSchema. Полностью наш класс Space выглядит примерно так:
class Space {
virtual QSize size() const = 0;
virtual QRect itemRect(ItemID item) const = 0;
QVector<ItemSchema> schemas;
};
Вот наглядный пример (подписи немного устарели, но основная идея, думаю, понятна):
Создавая разных наследников классов Range, View и Layout, и комбинируя их различным образом, мы имеем богатые возможности по кастомизации любого пространства ячеек и, таким образом, любого виджета. Например, создав класс ViewRating, который отображает оценку в виде звёздочек, я могу использовать его и как отдельный виджет, и в ячейках таблицы, и в элементах графической сцены.
Данная архитектура располагает к сотрудничеству программистов. Кто-то может написать свой тип пространства ячеек, который укладывает ячейки каким-то специальным образом. Кто-то напишет 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<CacheView> views;
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 является реализацией идей из этих заметок.
Идей и задач по дальнейшему развитию еще много, так что оставайтесь на связи.
Автор: lexxmark