- PVSM.RU - https://www.pvsm.ru -
В первом посте [1] мы разбили функциональность грида на несколько классов. Давайте еще раз быстренько их опишем:
Так же мы описали модели и вью для текстовых данных (ModelText, ModelTextCallback. ViewText). Давайте попробуем создать грид и привязать к нему текстовые данные. Новую функциональность, которая нужна для работы стандартного грида будем добавлять в виде специальных Model/View/Controller.
Для краткости буду опускать в коде указатели, shared_ptr и т.п. Итак, поехали…
int main()
{
Application app;
// создаем окно
GridWindow grid_window("Demo");
// создаем модель текста
ModelTextCallback m;
m.getCallback = [](CellID cell)->String {
return String.Format("Item[%d, %d]", cell.row, cell.column);
};
// указываем гриду где, что и как рисовать
grid_window.AddData(RangeAll(), ViewText(m), LayoutAll());
// выставляем количество строк и столбцов
grid_window.SetRows(10);
grid_window.SetColumns(10);
// показываем окно
grid_window.Show(100, 100, 300, 300);
app.run();
}
Неплохо, только хочется как-то обособлять ячейки: рисовать либо через-полосицу, либо сетку. Давайте реализуем один, а потом другой вариант. Надеюсь код говорит сам за себя.
class ViewOddRowBackground: public View
{
public:
void Draw(DrawContext& dc, Rect rect, CellID cell) const override
{
// закрасить ячейку, если строка чётная
if (cell.row%2 == 0)
dc.FillRect(rect, ColorRGB(200, 200, 255));
}
};
void plugOddRowBackgroud(Grid& grid)
{
grid.AddData(RangeAll(), ViewOddRowBackground(), LayoutAll(Transparent));
}
Заметьте, мы в LayoutAll передали параметр Transparent, он говорит о том, что этот layout не будет модифицировать прямоугольник ячейки. Помните, что по умолчанию LayoutAll «забирает» всю свободную область ячейки и зануляет её. В режиме Transparent, он этого делать не будет и следующий за ним ViewText получит тот же оригинальный прямоугольник.
Осталось добавить вызов новой функции в main
GridWindow grid_window("Demo");
plugOddRowBackgroud(grid_window.GetGrid());
Теперь реализуем сетку.
class ViewCellBounds: public View
{
public:
void Draw(DrawContext& dc, Rect rect, CellID cell) const override
{
// нарисовать линию внизу ячейки
dc.Line(rect.BottomLeft(), rect.BottomRight());
// нарисовать линию у правого края ячейки
dc.Line(rect.TopRight(), rect.BottomRight());
}
};
void plugCellBounds(Grid& grid)
{
grid.AddData(RangeAll(), ViewCellBounds(), LayoutAll(Transparent));
}
...
GridWindow grid_window("Demo");
plugCellBounds(grid_window.GetGrid());
Следующим общим местом для всех гридов является понятие выделенных ячеек — selection. Предыдущие представления (views) не хранили никаких данных, поэтому модели для них мы не создавали. Здесь ситуация немного сложнее. Что же должно входить в ModelSelection? Во-первых, набор выделенных ячеек, а, во-вторых, координаты активной ячейки (это та ячейка, которая обычно выделена рамочкой, и с помощью клавиатуры мы работаем именно с этой ячейкой). Пишем код:
class ModelSelection: public Model
{
public:
Range GetSelectedRange() const { return m_selected_range; }
void SetSelectedRange(Range new_selected_range)
{
m_selected_range = new_selected_range;
changed.invoke(*this);
}
CellID GetActiveCell() const { return m_active_cell; }
void SetActiveCell(CellID new_active_cell)
{
m_active_cell = new_active_cell;
changed.invoke(*this);
}
private:
Range m_selected_range;
CellID m_active_cell;
};
class ViewSelection: public View
{
public:
ViewSelection(ModelSelection selection)
: m_selection(selection)
{}
void Draw(DrawContext& dc, Rect rect, CellID cell) const override
{
// если ячейка активная -> рисуем рамку
if (m_selection.GetActiveCell() == cell)
dc.DrawFrame(rect);
// если ячейка выделена -> закрашиваем область
// и меняем текущий цвет для текста
if (m_selection.GetSelectedRange().HasCell(cell))
{
dc.FillRect(rect, ColorSelectedBackground);
dc.SetTextColor(ColorSelectedText);
}
}
private:
ModelSelection m_selection;
};
ModelSelection plugSelection(Grid& grid)
{
ModelSelection selection;
grid.AddData(RangeAll(), ViewSelection(selection), LayoutAll(Transparent));
return selection;
}
...
GridWindow grid_window("Demo");
plugCellBounds(grid_window.GetGrid());
plugSelection(grid_window.GetGrid());
Реализацию контроллера я опускаю ради экономии места. Поверьте, там тоже всё просто: по нажатию левой клавиши мыши меняем активную ячейку на ту, что под мышкой. При перемещении мыши и отпускании левой клавиши — создаем специальный Range, который описывает прямоугольный блок ячеек от активной (где мы зажали кнопку), до текущей. Задаем этот Range в selection. Еще надо учитывать состояние клавиш Shift и Ctrl, но это детали. В итоге получаем следующую картинку.
Что бы дать возможность пользователю менять размер строк и столбцов, нам нужно реализовать специальный контроллер, который, по нажатию кнопки мышки около края ячейки, запомнит положение мыши, а при отжатии мыши изменит ширину колонки на разницу между начальной точной и текущей. Надеюсь идея понятна. Стоит отметить, что контроллеры у нас «живут» в представлениях (views), поэтому нам надо создать фиктивный View, который ничего не рисует, а лишь определяем прямоугольник, в котором будет активизироваться контроллер.
Довольно часто гриды имеют заголовок — часть грида, которая всегда остается в верхней части окна. Иногда есть похожая конструкция слева (обычно там пишут номер строки). При скроллинге эти области остаются на своих местах. Давайте посмотрим, как заголовок может выглядеть у нас:
Очень похоже на грид, состоящий из одной строки и такого же количества столбцов, что и оригинальный грид. Тогда можно сказать, что заголовок — это грид, колонки которого синхронизированы с колонками оригинального грида (по сути в двух гридах используется один и тот же экземпляр Lines для колонок). Этот грид расположен в верхней части окна и скроллируется только по горизонтали. Подобное утверждение можно сказать про левую фиксированную часть, только там синхронизируются строки. Таким образом у нас в классе GridWindow живут четыре CacheGrid вместо одного.
Пойдем еще дальше и скажем, что фиксированные области могут быть с любых сторон.
Итак, класс GridWindow у нас усложнился — вместо одного CacheGrid, у нас появилось их девять штук. Функции отрисовки, скроллирования, обработки мышиных событий должны тоже усложниться. Я предлагаю здесь остановиться и внимательно посмотреть на последний рисунок. Издалека он похож на область, разбитую на девять под-областей в три строки и три колонки. Похоже на грид с тремя строками и колонками, в ячейках которого отображаются другие гриды. По определению у нас View настолько универсальный, что может отображать любую сущность. Так давайте создадим View, который в ячейке отображает некоторый грид. Для нашего случая с девятью гридами мы получим примерно следующие классы:
class ModelGrid: public Model
{
public:
ModelGrid();
CacheGrid GetGrid(CellID cell) const { return m_grids[cell.row][cell.column]; }
private:
CacheGrid m_grids[3][3];
};
class ViewGrid: public View
{
public:
ViewGrid(ModelGrid model)
:m_model(model)
{}
void Draw(DrawContext& dc, Rect rect, CellID cell) const override
{
CacheGrid cacheGrid = m_model.GetGrid(cell);
cacheGrid.Draw(dc);
}
};
В конструкторе ModelGrid создаются девять объектов CacheGrid и синхронизируются строки и столбцы. Так же не трудно реализуется контроллер. Если мы добавим ViewGrid к нашему старому классу GridWindow, который имел только один объект CacheGrid, то нам нет нужды создавать новый тип GridWindow. Единственное отличие — позицию скроллбаров нам нужно передавать в подгриды: игнорировать позицию скроллов будут угловые гриды (Top/Left, Top/Right, Botton/Right, Bottom/Left), по горизонтали будут скроллироваться Top и Bottom гриды, по вертикали — Left и Right. Ну а центральный Client грид будет скроллироваться по всем сторонам. При этом код отрисовки и взаимодействия с мышью у нас останется прежним.
Здесь опять остановимся и посмотрим, какой манёвр мы только что совершили? В начале у нас было утверждение — грид это набор ячеек, а ячейка — это место, где отображается всё что угодно. Теперь получается, что ячейка может отображать всё что угодно, включая грид. Цепочка замкнулась — грид рисует ячейки, ячейки рисуют грид. Тут возникает философский вопрос о курице и яйце. Что первично? Грид, который состоит из ячеек, или ячейка, которая может представлять всё что угодно, включая грид. Похоже, что понятие ячейки более универсально. Помните класс CellCache из первой статьи — в нем первым делом тройки <Range, View, Layout> фильтровались по Range, то есть по сути ячейка должна потом хранить не тройки, а двойки <View, Layout> — которые описывают, что и где в ячейке рисовать. CellCache умеет себя рисовать и обрабатывать события мышки (передавая их в нужный контроллер), при этом нигде не упоминается Grid.
А что, если мы создадим класс окна, который будет рисовать не CacheGrid, а CacheCell? Тогда мы получим не класс окна грида, а более универсальный элемент управления, в который мы просто, в виде двоек <View, Layout>, задаём что и где рисовать. Например, комбинация {<ViewCheck, LayoutLeft>, <ViewText, LayoutAll>} нам даёт стандартный контрол управления чекбокс. Другие комбинации дадут другие типы контролов, причем разные Layouts дают нам возможность располагать Views в любой конфигурации. Например чекбокс справа текста, снизу, сверху — как угодно.
Давайте назовем такое окно ItemWindow (ячейка — это понятие тесно связанное с гридом, item — более нейтрален). У меня получилась иерархия из трех классов:
Как правило, когда разрабатывают библиотеку визуальных компонентов, то определяют сначала простые типы окон, потом переходят к более сложным (item based). В этот момент базовые понятия окна уже зафиксированны и не учитывают специфику item-based окон. Поэтому для них придумывают какие-то другие абстракции, никак не связанные с базовыми понятиями окна. В результате не получается органично использовать простые и сложные контролы вместе. Мы же пошли от сложного контрола и пришли в итоге к простым окнам. Но их структура немного сложнее, чем в обычных оконных фреймворках. Зато мы получаем универсальность — Views и Layouts у нас используются и в простых контролах (ItemWindow) и в более сложных. Получается один раз реализовав ViewCheck, мы можем использовать его как простой контрол и как подэлемент грида.
Статья получилась довольно длинной и сложной, поэтому пока закончу. Если что-то вам покажется непонятным или неправильным, прошу писать комментарии, я подредактирую статью. Таким образом статья для всех станет проще и понятнее.
В следующей статье я опишу, как можно сделать introspection для нашего окна. Любой инструмент, способный посылать сообщения окну и интерпретировать последовательность байт будет способен «общаться» с гридом. Это позволит нам сделать байндинг к гриду на питоне и делать автоматическое тестирование нашего контрола. Так же расскажу как в моей схеме реализуются всякие «вкусняшки» из devexpress. Как я уже говорил, работающий прототип данной архитектуры уже существует, так что реализовать её в open source будет не сложно.
Автор: lexxmark
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/49752
Ссылки в тексте:
[1] посте: http://habrahabr.ru/post/203968/
[2] Источник: http://habrahabr.ru/post/204374/
Нажмите здесь для печати.