- PVSM.RU - https://www.pvsm.ru -

qt-items — новый фреймворк, или попытка найти Теорию Всего

Как известно, физики давно пытаются найти Теорию Всего, в рамках которой можно было бы объяснять все известные взаимодействия в природе. Склонность к обобщениям присуща не только физикам, но и математикам, и программистам. Способность меньшим количеством сущностей объяснять и предсказывать большой спектр явлений очень ценна. Для программистов в роли теорий выступают различные API и фреймворки. Некоторые из них решают узкоспециализированные проблемы, а какие-то претендуют на роль универсальных теорий. Примером последних может выступать Qt — универсальный фреймворк, предназначенный, в основном, для разработки GUI.

Далее я расскажу, что мне не нравится в Qt и как его можно сделать ещё более универсальным, мощным и удобным для работы.

Демо видео (лучше смотреть в HD).


Qt, как и многие другие GUI фреймворки развивался от простого к сложному. Сначала создавались простые виджеты, потом более сложные и составные. Появился Model/View framework, для отображения данных в табличном или древовидном виде. Появился Graphics Items framework для отображения набора графических элементов. Все эти фреймворки имеют различные API и несовместимы друг с другом. По сути у нас есть три независимых и почти не пересекающихся теории в рамках одной большой. Когда мне нужно разработать какой-либо новый визуальный элемент, то я должен выбрать, в каком из трёх фреймворков я собираюсь его использовать и применять соответствующее API. Таким образом я не могу создать элемент, который можно было бы использовать и в качестве отдельного виджета, и внедрить в ячейки таблицы, и использовать в узлах графической сцены.

Qt развивается под лозунгом — Write once, run anythere. Для написания конечных приложений это может быть и правда, но для расширения и кастомизации самой библиотеки это не так.

Давайте подумаем, как должны быть устроены виджеты, что бы библиотека Qt стала по-настоящему единой и мощной.
Рассмотрим разные виджеты (чекбокс, таблица, дерево и графическая сцена) и постараемся найти в них что-то общее. Информация в них сгруппирована в ячейки (Items). Чекбокс состоит из одной ячейки, таблица — из рядов и столбцов ячеек, в сцене ячейками являются узлы. Таким образом можно сказать, что все виджеты отображают ячейки, только их количество и расположение в пространстве специфичны для разных типов виджетов. Давайте скажем, что виджет отображает некоторое пространство ячеек (Space [1]). Для простых виджетов пространство ячеек тривиально SpaceItem [2], и состоит из единственной ячейки. Для таблицы можно придумать SpaceGrid [3], которое описывает, как ячейки организованы в строки и столбцы. Для графической сцены имеем SpaceScene [4], где ячейки могут располагаться как угодно.

Что есть общего у всех пространств, что можно выделить в базовый класс?
Пока что, можно выделить две вещи:

  1. Возвращать общий размер пространства (обычно это bounding box всех ячеек)
  2. Возвращать расположение ячейки по её координате ItemID
class Space {
    virtual QSize size() const = 0;
    virtual QRect itemRect(ItemID item) const = 0;
};

Давайте теперь внимательно рассмотрим сами ячейки. Для наглядности будем изучать такую таблицу:
qt-items — новый фреймворк, или попытка найти Теорию Всего - 1

Ячейки тоже имеют некоторую структуру. Например, чекбокс состоит из квадратика с галочкой и текста. В таблице ячейки могут быть очень сложными (содержать текст, картинки, ссылки, как в моём видео-примере). Заметим, что для таблицы у нас, как правило, ячейки в одном столбце имеют одинаковую структуру. Поэтому нам легче описывать не каждую ячейку, а целый набор. Наборы ячеек (Range [5]) могут быть разными, например, все ячейки RangeAll, ячейки из колонки RangeColumn, ячейки из строки RangeRow, ячейки из четных строк RangeOddRow и т.п. Какой же интерфейс можно выделить для базового класса Range [5]? Интерфейс простой и лаконичный — отвечать на вопрос, входит какая-то ячейка в Range [5] или нет:

class Range {
    virtual bool hasItem(ItemID item) const = 0;
};

После того, как мы определились с подмножеством ячеек, нам надо указать, какой тип информации в этих ячейках мы хотим отобразить. За отображение самого маленького и неделимого кусочка информации будет отвечать класс View [6]. Например, ViewCheck [7] умеет отображать значок чекбокса, ViewText [8] — отображает строку текста и т.п.
Пока что базовый класс View [6] должен уметь лишь рисовать информацию в ячейке:

class View {
    virtual void draw(QPainter* painter, ItemID item, QRect rect) const = 0;
};

Возникает вопрос, откуда ViewCheck [7] знает, что ему надо рисовать значок слева в ячейке, а ViewText [8] знает, что ему нужно рисовать текст после значка чекбокса? Для этого заведем ещё один «карликовый» класс Layout [9]. Этот класс умеет размещать View [6] внутри ячейки. Например, LayoutLeft разместит View [6] у левого края ячейки, 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 [6] требуется еще одна функция — 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 [10]. Полностью наш класс Space [1] выглядит примерно так:

class Space {
    virtual QSize size() const = 0;
    virtual QRect itemRect(ItemID item) const = 0;

    QVector<ItemSchema> schemas;
};

Вот наглядный пример (подписи немного устарели, но основная идея, думаю, понятна):
qt-items — новый фреймворк, или попытка найти Теорию Всего - 2

Создавая разных наследников классов Range [5], View [6] и Layout [9], и комбинируя их различным образом, мы имеем богатые возможности по кастомизации любого пространства ячеек и, таким образом, любого виджета. Например, создав класс ViewRating [11], который отображает оценку в виде звёздочек, я могу использовать его и как отдельный виджет, и в ячейках таблицы, и в элементах графической сцены.

Данная архитектура располагает к сотрудничеству программистов. Кто-то может написать свой тип пространства ячеек, который укладывает ячейки каким-то специальным образом. Кто-то напишет View, который отображает специфичные данные. И эти программисты могу воспользоваться результатом работы друг друга. Вот не полный список моих реализаций класса View, их легко создавать и использовать (реализация буквально несколько строк кода):

  1. ViewButton [12] — рисует кнопку
  2. ViewCheck [13] — рисует значок чекбокса
  3. ViewColor [14] — заливает область определенным цветом
  4. ViewEnumText [15] — рисует текст из ограниченного списка
  5. ViewImage [16], ViewPixmap [16], ViewStyleStandardPixmap [16] — рисуют изображения
  6. ViewLink [17] — рисует текстовые ссылки
  7. ViewAlternateBackground [18] — рисует через-полосицу
  8. ViewProgressLabel [19], ViewProgressBox [19] — рисуют прогрессбар или проценты
  9. ViewRadio [20] — рисует значок радиобаттона
  10. ViewRating [21] — рисует значки оценки
  11. ViewSelection [22] — рисует выделенные ячейки
  12. ViewText [23] — рисует текст
  13. ViewTextFont [23] — меняет шрифт последующего текста
  14. ViewVisible [24] — показывает или скрывает другой View

Идём дальше. Как правило, виджет отображает не всё пространство ячеек, а только видимую часть. Класс Space [1] удобен для описания пространства ячеек, но плох для отрисовки ячеек в некоторой ограниченной видимой области. Давайте определим специальный класс для отображения под-области пространства CacheSpace [25]:

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;
};

qt-items — новый фреймворк, или попытка найти Теорию Всего - 3
Каждый конкретный наследник от CacheSpace [25] (CacheGrid [26], CacheScene [27] и др.) хранит набор кешированных ячеек CacheItem [28] по-разному (но оптимально для данного типа пространства). Поэтому мы выделим в базовом классе функцию visit, которая посещает все кешированные ячейки. С помощью неё легко реализовать функцию draw — просто нужно посетить все кешированные ячейки и вызвать у них свою функцию draw.

Как понятно из названия, CacheItem [28] хранит всю информацию, нужную для отображения конкретной ячейки:

class CacheItem {
    ItemID item;
    QRect itemRect;
    QVector<CacheView> views;

    void draw(QPainter* painter) const;
};

Здесь функция draw устроена тоже очень просто — в цикле вызвать draw у класса CacheView [29], который отвечает за отрисовку самого маленького и неделимого кусочка информации внутри ячейки.

class CacheView {
    View view;
    QRect viewRect;

    void draw(QPainter* painter, ItemID item) const;
};

Таким образом, виджету необходимо иметь CacheSpace [25] и с помощью него рисовать содержимое своего пространства ячеек:

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 [25] на уровень отдельной ячейки CacheItem [28] и, далее, внутри ячейки перебирая отдельные CacheView [29].

Эта возможность, представить любой виджет, как иерархию CacheSpace->CacheItem->CacheView, даёт нам большие возможности по управлению и интроспекции виджета.

Например, мы можем реализовать единый интерфейс доступа к любому нашему виджету из системы автоматического тестирования. Система автоматического тестирования GUI обычно запрашивает необходимую область в виджете и потом воздействует на эту область мышью, имитируя действия пользователя. Мы можем предоставить такой системе самую подробную «карту» областей, на которые можно воздействовать.

Другой пример — анимации, которые представлены в видео-примере. Мы можем не только смотреть, из чего состоит наш виджет, но и воздействовать на его составные части. Для примера, можно менять расположения любых объектов в иерархии (CacheSpace->CacheItem->CacheView) во времени или отрисовывать их с полупрозрачностью. Таким образом, можно собирать целую библиотеку анимаций, которые могут быть применены на любой виджет и на любое пространство ячеек.

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

  1. Space [1] — можно создавать свои типы пространства ячеек
  2. CacheSpace [25] — можно создавать новые типы отображения пространств, например, реализовать CacheSpaceCourusel — отображать список ячеек в виде карусельки
  3. View [6] — создавать новые виды визуализаций для ячеек
  4. Animation [30] — создавать новые анимации

Данная заметка является продолжением предыдущих двух: здесь [31] и здесь [32]. Проект qt-items [33] является реализацией идей из этих заметок.

Идей и задач по дальнейшему развитию еще много, так что оставайтесь на связи.

Автор: lexxmark

Источник [34]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/yuzabiliti/88755

Ссылки в тексте:

[1] Space: https://github.com/lexxmark/qt-items/blob/master/src/space/Space.h

[2] SpaceItem: https://github.com/lexxmark/qt-items/blob/master/src/space/SpaceItem.h

[3] SpaceGrid: https://github.com/lexxmark/qt-items/blob/master/src/space/SpaceGrid.h

[4] SpaceScene: https://github.com/lexxmark/qt-items/blob/master/src/space/SpaceScene.h

[5] Range: https://github.com/lexxmark/qt-items/blob/master/src/core/Range.h

[6] View: https://github.com/lexxmark/qt-items/blob/master/src/core/View.h

[7] ViewCheck: https://github.com/lexxmark/qt-items/blob/master/src/items/checkbox/Check.h

[8] ViewText: https://github.com/lexxmark/qt-items/blob/master/src/items/text/Text.h

[9] Layout: https://github.com/lexxmark/qt-items/blob/master/src/core/Layout.h

[10] ItemSchema: https://github.com/lexxmark/qt-items/blob/master/src/core/ItemSchema.h

[11] ViewRating: https://github.com/lexxmark/qt-items/blob/master/src/items/rating/Rating.h

[12] ViewButton: https://github.com/lexxmark/qt-items/tree/master/src/items/button

[13] ViewCheck: https://github.com/lexxmark/qt-items/tree/master/src/items/checkbox

[14] ViewColor: https://github.com/lexxmark/qt-items/tree/master/src/items/color

[15] ViewEnumText: https://github.com/lexxmark/qt-items/tree/master/src/items/enum

[16] ViewImage: https://github.com/lexxmark/qt-items/tree/master/src/items/image

[17] ViewLink: https://github.com/lexxmark/qt-items/tree/master/src/items/link

[18] ViewAlternateBackground: https://github.com/lexxmark/qt-items/tree/master/src/items/misc

[19] ViewProgressLabel: https://github.com/lexxmark/qt-items/tree/master/src/items/progressbar

[20] ViewRadio: https://github.com/lexxmark/qt-items/tree/master/src/items/radiobutton

[21] ViewRating: https://github.com/lexxmark/qt-items/tree/master/src/items/rating

[22] ViewSelection: https://github.com/lexxmark/qt-items/tree/master/src/items/selection

[23] ViewText: https://github.com/lexxmark/qt-items/tree/master/src/items/text

[24] ViewVisible: https://github.com/lexxmark/qt-items/tree/master/src/items/visible

[25] CacheSpace: https://github.com/lexxmark/qt-items/blob/master/src/cache/space/CacheSpace.h

[26] CacheGrid: https://github.com/lexxmark/qt-items/blob/master/src/cache/space/CacheSpaceGrid.h

[27] CacheScene: https://github.com/lexxmark/qt-items/blob/master/src/cache/space/CacheSpaceScene.h

[28] CacheItem: https://github.com/lexxmark/qt-items/blob/master/src/cache/CacheItem.h

[29] CacheView: https://github.com/lexxmark/qt-items/blob/master/src/cache/CacheView.h

[30] Animation: https://github.com/lexxmark/qt-items/blob/master/src/misc/CacheSpaceAnimation.h

[31] здесь: http://habrahabr.ru/post/203968/

[32] здесь: http://habrahabr.ru/post/204374/

[33] qt-items: https://github.com/lexxmark/qt-items

[34] Источник: http://habrahabr.ru/post/255573/