- PVSM.RU - https://www.pvsm.ru -
Как известно, физики давно пытаются найти Теорию Всего, в рамках которой можно было бы объяснять все известные взаимодействия в природе. Склонность к обобщениям присуща не только физикам, но и математикам, и программистам. Способность меньшим количеством сущностей объяснять и предсказывать большой спектр явлений очень ценна. Для программистов в роли теорий выступают различные 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], где ячейки могут располагаться как угодно.
Что есть общего у всех пространств, что можно выделить в базовый класс?
Пока что, можно выделить две вещи:
class Space {
virtual QSize size() const = 0;
virtual QRect itemRect(ItemID item) const = 0;
};
Давайте теперь внимательно рассмотрим сами ячейки. Для наглядности будем изучать такую таблицу:
Ячейки тоже имеют некоторую структуру. Например, чекбокс состоит из квадратика с галочкой и текста. В таблице ячейки могут быть очень сложными (содержать текст, картинки, ссылки, как в моём видео-примере). Заметим, что для таблицы у нас, как правило, ячейки в одном столбце имеют одинаковую структуру. Поэтому нам легче описывать не каждую ячейку, а целый набор. Наборы ячеек (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;
};
Вот наглядный пример (подписи немного устарели, но основная идея, думаю, понятна):
Создавая разных наследников классов Range [5], View [6] и Layout [9], и комбинируя их различным образом, мы имеем богатые возможности по кастомизации любого пространства ячеек и, таким образом, любого виджета. Например, создав класс ViewRating [11], который отображает оценку в виде звёздочек, я могу использовать его и как отдельный виджет, и в ячейках таблицы, и в элементах графической сцены.
Данная архитектура располагает к сотрудничеству программистов. Кто-то может написать свой тип пространства ячеек, который укладывает ячейки каким-то специальным образом. Кто-то напишет View, который отображает специфичные данные. И эти программисты могу воспользоваться результатом работы друг друга. Вот не полный список моих реализаций класса 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;
};
Каждый конкретный наследник от 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) во времени или отрисовывать их с полупрозрачностью. Таким образом, можно собирать целую библиотеку анимаций, которые могут быть применены на любой виджет и на любое пространство ячеек.
В итоге, хочу еще раз перечислить, в каких направлениях можно кастомизировать данную библиотеку:
Данная заметка является продолжением предыдущих двух: здесь [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/
Нажмите здесь для печати.