Есть старый добрый CSS: он задаёт размеры шрифта, положение элементов, плавающие блоки и всё такое. Это CSS, который был во времена нашей нежной юности.
С тех пор многое изменилось. CSS стал взрослым, злым и адаптивным, и теперь на нём можно верстать такое, что нам и не снилось. Разберёмся в одном из инструментов современного CSS, который так делает, — Grid.
Для чего нужен Grid
Допустим, у нас стоит задача расположить элементы на странице в определённых местах. Например:
- шапку сайта поставить на самый верх, чтобы она занимала не больше 100 пикселей;
- в этой шапке предусмотреть отдельное место, куда можно поставить информацию о пользователе, когда он введёт свои логин и пароль;
- основное содержимое тоже должно состоять из трёх частей — левого меню, статьи и правого блока с рекламой;
- правый блок с рекламой должен занимать от 200 до 150 пикселей;
- содержимое статьи пусть занимает всё свободное пространство, но с отступом 20 пикселей от каждого края блока;
- ещё блок со статьёй не может быть в ширину меньше чем 500 пикселей, больше — можно;
- внизу должен быть блок на всю ширину с контактной информацией.
Это можно сделать несколькими способами.
CSS Grid Layout. Кроссбраузерность. Grid и Flex. Основные понятия.
Подключить Bootstrap и заверстать всё на нём. Мы сразу получаем адаптивность и разбивку по блокам, но задать нужные размеры будет сложно. Бутстрап хорош, когда нужно быстро сделать что-то адаптивное без сильных требований к пикселям и размерам. Он считает все размеры сам и не особо даёт в это вмешаться.
Использовать таблицы . Проверенный и рабочий способ создать что-то железобетонно надёжное, но не очень гибкое. Минус таблиц — много вложенных тегов и сложная работа с группировкой ячеек. Так делали сайты в девяностых и нулевых.
Применить grid. Наш выбор на сегодня. Grid, как и бутстрап, можно сделать адаптивным, а работа с ячейками и областями в нём намного удобнее, чем в таблицах. Но и синтаксис не такой интуитивный.
Создаем сетку
Когда мы пишем CSS-команду display: grid, у нас на странице появляется сетка из линий, между которыми находятся ячейки. Но в таком виде команда создаст сетку только из одной ячейки. Чтобы ячеек стало больше, можно задать количество строк и столбцов в сетке:
.mygrid < // подключаем сетку display: grid; // делаем 4 колонки шириной по 100 пикселей каждая grid-template-columns: 100px 100px 100px 100px; // и 3 строки высотой по 50 пикселей grid-template-rows: 50px 50px 50px; >…
Обращаемся к частям сетки
Grid — это не просто таблица с ячейками. Сила grid — в способах выделения нужных ячеек, чтобы ими можно было управлять отдельно.
Для того, чтобы определить нужную ячейку, используют линии, дорожки, области и ячейки сетки:
Grid CSS полный курс за 13 минут. Все свойства
Линия сетки — это линии, которые и формируют нашу сетку. В нашем примере у нас 4 горизонтальные линии и 5 вертикальные — первые и последние линии тоже считаются.
Дорожка — область от первой линии сетки до последней. То, на сколько частей мы разделим дорожки, столько у нас и получится строк и столбцов. По умолчанию расстояние между дорожками (строками и столбцами) равно нулю, но мы можем изменить этот параметр и отделить дорожки друг от друга. В этом случае между ячейками появится зазор.
// расстояние между строками
row-gap: 10px;
// расстояние между колонками
column-gap: 5px;
Область сетки — прямоугольник, который получился из нужных нам линий. В нашем примере область задана линиями 2 и 4 по горизонтали и 3 и 5 — по вертикали:
Чтобы управлять поведением и внешним видом области, её нужно выделить в отдельный класс:
.nasha_oblast grid-row-start: 2;
grid-row-end: 4;
grid-column-start: 3;
grid-column-end: 5;
>
Ячейка — самая маленькая область сетки, внутри которой можно что-то разместить. По умолчанию каждая новая единица контента в сетке помещается в отдельную ячейку — слева направо и сверху вниз. Если нужно вынести конкретную ячейку в отдельный класс — делаем это так же, как и для области.
Размеры содержимого сетки
При создании сетки можно использовать разные единицы размерности, смешивая их друг с другом. Например, можно указать, что в сетке будет три столбца — первый шириной 100 пикселей, второй будет размером с половину ширины экрана, а третий пусть занимает всё оставшееся место:
grid-template-columns: 100px 50vw 1fr;
Пиксели: указываем нужный нам размер или пользуемся функцией minmax(), которая задаёт минимальный и максимальный размер дорожки.
Относительные величины: Если ширина или высота у нас зависят от размеров окна браузера, то можно использовать относительную величину vh (высота окна) или vw (ширина окна):
100vh — вся высота окна,
33vh — треть высоты,
50vw — половина ширины окна браузера.
Для простоты можно представить, что числа перед vh и vw — это проценты. 100 — это сто процентов, вся видимая часть, 50 — это половина и так далее. Можно указать больше 100, тогда содержимое уедет за границы окна и нужно будет использовать прокрутку вниз или вбок.
FR работает так:
- создаёт дорожки с нужными пропорциями;
- или разрешает занимать одной дорожке всю свободную область окна браузера.
grid-template-columns: 1fr 1fr 1fr 1fr; — 4 колонки одинаковой ширины;
grid-template-columns: 1fr 1fr 1fr 3fr; — последняя колонка будет шире остальных в три раза.
Если нам нужно что-то растянуть на всё свободное место, мы можем указать размеры остальных элементов, а там, где нужно растянуть, напишем 1fr:
grid-template-columns: 100px 1fr 100px;
Этот код сделает нам три колонки — первая и третья шириной 100 пикселей, а вторая (центральная) займёт всё остальное место. Если размер окна изменится, то боковые колонки останутся по 100 пикселей, а центральная перестроится.
Что дальше
Это первая статья про CSS Grid. В следующей поговорим о том, как быстро создавать нужные шаблоны с сеткой и как работать с областями и ячейками, а потом соберём страницу-портфолио.
Текст, код и иллюстрации
Источник: thecode.media
CSS Grid: что это и как поможет быстрее верстать адаптивные сайты
Верстка сайта, который будет отлично адаптироваться под любые размеры экранов – необходимое требование в любом проекте, хотя это не всегда легкая задача. Облегчает задачу использование специальных технологий, вроде CSS Grid. В таком случае верстка идет по заранее размеченной сетке, которая хорошо адаптируется к изменению размеров экрана. В отличии от использования технологий вроде Bootstrap, к проекту не потребуется подключать дополнительные библиотеки.
Далее рассмотрим, как работать с CSS Grid и как он помогает в процессе верстки.
Что такое CSS Grid
Это новая модель позиционирования элементов двумерных макетов. Она идеально подходит для всего, что требует отзывчивого позиционирования, в том числе и для создания адаптивных сайтов. CSS Grid чем-то похож на табличную верстку, которая активно использовалась в 90-е и начале 00-х годов. За основу страницы бралась таблица, разделенная на несколько колонок, которые наполнялись контентом.
Однако HTML-таблицы создавалась для того, чтобы в них забивали данные, а не верстку, поэтому сайт, сделанный по такой схеме, выглядел не очень хорошо. Плюс, в то время не существовало потребности в адаптивном дизайне, поэтому сайт не мог корректно открываться иногда даже на экранах другого размера, не говоря про смартфоны.
CSS Grid использует похожий принцип работы как у табличной верстки начала эпохи веба, но только сильно усовершенствованный. Он был разработан в начале 2017 года и с этого момента активно внедряется в жизнь веб-разработчиков. Так как Grid создается и поддерживается командой, которая работает над CSS, то он быстро получил поддержку во всех браузерах и уже стал стандартом адаптивной верстки.
Флоат – это основной компонент Grid-верстки. Он является свободно позиционируемым компонентом, за позицию которого отвечают собственные CSS-свойства, а также свойства других компонентов, с которыми он взаимодействует, например, родителя. Изначально планировалось остановиться только на внедрении флоатов, но оказалось, что они не всегда корректно работают во viewport-интерфейсах, то есть на большинстве мобильных устройств. Разработчики пытались исправить это с помощью разных “костылей”, которые со временем стали нормой и были доработаны. Так окончательно появился современный CSS Grid.
Grid Layout позволяет за более короткое время и с меньшим количеством кода решать те задачи, на которые в обычном случае уходило бы больше времени, требовалось бы писать больше кода и иногда пользоваться нестандартными решениями, которые не добавляют стабильности работы. Благодаря поддержки работы в двух измерениях есть возможность создавать более сложные и комплексные шаблоны.
Как работает Grid CSS
В основе лежит система сеток из набора пересекающихся горизонтальных и вертикальных линий. В итоге весь шаблон разделяется на блоки – контейнеры, которые в свою очередь можно поделить на дополнительные контейнеры и спозиционировать их относительно друг друга. Для создания грид-разметки вам требуется прописать в CSS-свойствах к компоненту display: grid. После этого все элементы, находящиеся внутри этого блока станут grid-элементами и будут позионироваться по соответствующим правилам. Для всех новых компонентов, которые будут добавляться в этот блок вы сможете задавать grid-свойствах для их позиционирования относительно друг друга внутри родителя.
Для начала нужно определиться, сколько колонок будет внутри родительского элемента. Их количество можно задать фиксированное, а можно сделать так, чтобы оно менялось в зависимости от размещаемого внутри контента. Размер ячеек обычно не фиксированный и меняется в зависимости от того, что находится внутри них. Ячейки можно объединять между собой, вытеснять другие ячейки на следующие столбцы.
Верстка с использованием CSS Grid Layout
Далее на нескольких примерах рассмотрим подобрано все особенности разработки с использованием Grid-компонентов. Для удобства разделим процесс на несколько этапов.
Создание Grid-элементов
Для начала создадим грид-таблицу внутри одного из элементов. Размеры ячеек оставим по умолчанию, но сделаем между ними отступы. Вот так будет выглядеть CSS-разметка:
В итоге, вы получите сетку 3×3 блока, если смотреть на обычном разрешении. Между ячейками будет небольшой отступ, размер которого будет адаптироваться к изменению размера экрана. Если уменьшить размер экрана, то блоки начнут занимать не ⅓ от размера строки родителя, а 100%.
Пример простой Grid-сетки
В HTML-разметке писать что-то дополнительно не нужно – достаточно задать класс, к которому будут применяться стили, поэтому разберем подробно, какие свойства были написаны в CSS и за что они отвечают:
- display: grid. Отвечает за превращение родительского элемента в грид-контейнер. Теперь к нему и вложенным компонентам можно применять другие grid-стили. По умолчанию у контейнера будет такое же поведение, как и у любого блочного элемента. Вы можете изменить его, приписав соответствующую приставку, например, display: inline-grid. В таком случае будет создан строчный грид-контейнер с соответствующим поведением.
- display: subgrid. Этого свойства не было в примере выше, но его тоже стоит рассмотреть. Оно отвечает за создание подсетки, например, внутри вложенного элемента.
- grid-template-rows: 1fr 1fr 1fr. Задает размер рядов в grid-системе и их позиционирование. Единица измерения fr позволяет подстраивать размер ячеек под контент внутри них и внешние элементы, плюс, учитывать дополнительные условия. В нашем примере пространство распределяется равномерно, но вы можете сделать так, чтобы какой-то конкретной ячейке давалось больше пространства, указав не 1fr, а 2. Кстати, тут можно указывать и нецелые значения – 4.5, 5.2 и подобные. Если по каким-то причинам не подходит единица измерения fr, то можно указать %, em или даже фиксированную величину в пикселях, однако fr дает наилучший результат.
- grid-template-columns: 1fr 1fr 1fr. Аналогично с предыдущим пунктом, но только здесь задается размер колонок.
- grid-gap: 2vw. Задает отступы между колонками сверху, снизу и по бокам. Единицы измерения тоже можно задать любые – хоть %, хоть пиксели. Если нужно задать отступ сбоку, то используйте свойство grid-row-gap, а если сверху и снизу, то grid-column-gap.
Это были основные параметры, отвечающие за позиционирование грид-сетки. Для 80% задач, выполняемых средним верстальщиком, их будет достаточно. Второй блок кода в примере отвечает за стилизацию ячеек. Ничего, связанного с Grid Layout в нем нет.
Пример шаблона сайта на Grid
Разобравшись с основными свойствами и понятиями грид-верстки, попробуем создать шаблон страницы сайта как на картинке ниже. Это примитивный и универсальный шаблон, проблем с которым не должно возникнуть.
У CSS Grid очень простой синтаксис, позволяющий “видеть” выстраивание шаблона в коде, хотя для этого потребуется некоторая практика. Создание шаблонов любой сложности в таком случае станет пустяковым делом. Вот примерно такой CSS-код должен получится:
Здесь грид-контейнером был сделан весь тег body, то есть все содержимое страницы уже приобретает грид-свойства, следовательно, их можно к ним применять. В целом, в этом примере большинство свойств уже рассматривались в первом примере с ячейками. Внимание нужно обратить на grid-template-areas, так как это свойство не рассматривалось ранее и логика его работы отличается от других.
Он определяет наш шаблон. Если же мы посмотрим на код, то увидим, что это 3х3 таблица (три ряда и три колонки). Это означает, что у нас получается пять грид-областей на девяти грид-ячейках. Обратите внимание, что некоторые грид-области занимают несколько ячеек. Например, шапка, занимает весь первый ряд из трех ячеек.
Аналогичное пространство занимает подвал, но только в нижней части. Панель с навигацией и рекламой занимают по одной ячейки, но при этом их размер меньше, чем у центральной ячейки, под которую отведена секция с основным контентом.
Так вот, свойство grid-template-areas позволяет определить для каждого элемента свою область. Она была определена в кавычках: «header header header», “nav article ads” и “footer footer footer”. Затем с помощью свойства grid-area происходит задание элементам области:
Также обратим внимание на задание размеров:
- grid-template-rows: 60px 1fr 60px – первая и третья строки будут обе по 60 пикселей фиксировано. Вторая строка заберет все оставшееся место.
- grid-template-columns: 20% 1fr 15% – отвечает за размер столбцов. Первый 20% от общего размера, третий 15%, а центральный занимает все оставшееся место.
Корректируем шаблон
Готовый шаблон легко меняется посредствам перераспределения грид-областей в свойстве grid-template-areas. Для примера внесем такие изменения:
grid-template-areas:
«nav header header»
«nav article ads»
«nav footer ads»;
Вот пример того, каким получился шаблон. Размеры регулируются в свойствах grid-template-rows и grid-template-columns. В итоге, просто меняя значения в кавычках можно изменить расположение компонентов в шаблоне – все очень просто.
Шаблон после корректировки
Задание адаптивности
Имеющийся шаблон уже умеет подстраиваться под изменения размеров экрана, но полностью адаптивным его назвать трудно, так как на мобильных он все равно отображается не совсем корректно. Исправить это можно с помощью значений auto-fill и auto-fit. С их помощью создаются специальные треки, меняющие параметры элементов в зависимости от размера экрана.
Для начала рассмотрим пример с auto-fill:
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
В нем колонкам присваивается минимальный размер в 150px и максимальный по оставшемуся месту. Они будут повторяться столько раз, сколько необходимо для того чтобы уложиться в заданный контейнер. В случае с параметром auto-fill ячейки подстраиваются таким образом, чтобы полностью заполнить контейнер. В примере минимальный размер ячейки 150 пикселей, а максимальный 1fr.
Auto-fit практически не отличается от auto-fill. Разница между ними только в том, что стягиваются все пустые треки на конце размещения. Fit просто растягивает контейнер таким образом, чтобы не оставалось пустого места, не считая заранее заданных отступов.
Различия между fit и fill
Использование медиа-запросов
Они тоже необходимы для лучшей адаптации интерфейса к разным размерам экрана. Логика работы с медиа-запросами при использовании Grid CSS немного отличается – вместо того, чтобы менять целый перечень свойств как при классической верстки, меняется только пара ключевых свойств.
Вот пример адаптации шаблона под экраны телефонов:
Если разрешение экрана будет меньше 575 пикселей, то все ячейки будут занимать один столбец полностью и выстроятся друг за другом в том порядке, который указан в свойстве grid-template-areas. Еще обратите внимание, что в grid-template-columns теперь только один размер.
Концепция явного и неявного грида
С этой концепцией нужно быть знакомым. В противном случае вы можете столкнуться с несколькими лишними колонками и не понимать, откуда они взялись. Вот определения:
- Явный грид это тот, который вы сами задали с помощью свойств grid-template-rows, grid-template-colums и grid-template-areas.
- Неявные гриды – это те, которые не уместились в вашу заданную сетку. Вы определили грид который может уместить только шесть элементов, но сам контейнер состоит из девяти элементов. В явный грид войдут только шесть элементов, а еще три станут неявными.
Явный и неявный грид
Создание вложенных ячеек
Грид-ячейка может тоже быть родителем для других ячеек. Они будут наследовать ее свойства. Для того, чтобы создать вложенный контейнер просто укажите в его CSS свойствах display: grid или его производные. Распределение пространства внутри будет происходить по тому же принципу, что в обычном грид-контейнере.
Создание вложенных гридов
Наследование
Наследуются только самые основные свойства. Обычно это только размер и количество колонок – оно по умолчанию будет таким же как в контейнере-родителе. Отсутствие расширенного наследования объясняется желанием минимизировать влияние вложенного грида на основной. Это позволяет избежать “поломки” верстки, но не очень удобно тем, что нужно прописывать все свойства повторно.
Заключение
Элемент управления Grid
Табличные элементы управления (обычно в их названии присутствуют слова Table или Grid) широко используются при разработке GUI. Так получилось, что на работе мы используем С++ и MFC для разработки пользовательского интерфейса. В начале мы использовали CGridCtrl — общедоступную и довольно известную реализацию грида.
Но с некоторого времени он перестал нас устраивать и появилась на свет собственная разработка. Идеями, лежащими в основе нашей реализации, я хочу с вами здесь поделиться. Есть задумка сделать open source проект (скорее всего под Qt). Поэтому данную заметку можно рассматривать как «Proof Of Concept». Конструктивная критика и замечания приветствуются.
Причины, по которым меня не устраивают существующие реализации я опущу (это тема для отдельной заметки).
Проекты у нас инженерно-научные, с богатой графикой, и списки и таблицы используются повсеместно. Поэтому новый грид должен был обеспечивать гибкую кастомизацию, хорошее быстродействие и минимальное потребление памяти при показе больших объемов информации. При разработке я старался придерживаться следующим правилом: реализуй функциональность максимально обобщенно и абстрактно, но не во вред удобству использования и оптимальности работы. Конечно, это правило противоречиво, но насколько мне удалось соблюсти баланс — судить вам.
Чтобы с чего-то начать, давайте попробуем дать определение элементу управления grid. Для сохранения общности можно сказать, что grid — это визуальный элемент, который разбивает пространство на строки и столбцы. В результате получается сетка ячеек (место пересечения строк и столбцов), внутри которых отображается некоторая информация. Таким образом у грида можно различить два компонента: структуру и данные. Структура грида определяет как мы будем разбивать пространство на строки и столбцы, а данные описывают, собственно, то, что мы хотим видеть в получившихся ячейках.
- Главное свойство Count — количество линий, из которых состоит Lines
- Каждая линия может менять свой размер (строка высоту, а столбец — ширину)
- Линии можно переупорядочивать (строки сортировать, столбцам менять порядок)
- Линии можно скрывать (делать невидимыми для пользователя)
class Lines < public: Lines(UINT_t count = 0); UINT_t GetCount() const < return m_count; >void SetCount(UINT_t count); UINT_t GetLineSize(UINT_t line) const; void SetLineSize(UINT_t line, UINT_t size); bool IsLineVisible(UINT_t line) const; void SetLineVisible(UINT_t line, bool visible); template void Sort(const Pred const vector void SetPermutation(const vector UINT_t GetAbsoluteLineID(UINT_t visibleLine) const; UINT_t GetVisibleLineID(UINT_t absoluteLine) const; Event_t changed; private: UINT_t m_count; vector m_linesSize; vector m_linesVisible; >;
Комментарии и некоторые служебные функции и поля опущены для наглядности.
Вы можете заметить, что в классе есть функции GetAbsoluteLineID и GetVisibleLineID . Так как мы позволяем перемешивать и скрывать линии, то абсолютный и видимый индекс линии различаются. Надеюсь картинка наглядно показывает эту ситуацию.
Также нужно сделать пояснение по поводу строки
Event_t changed;
Здесь определён сигнал (так он называется в Qt или boost). С появлением С++11 и std::function, можно легко написать простую реализацию signals/slots, чтобы не зависеть от внешних библиотек. В данном случае мы определили эвент в классе Lines, и к нему можно подключать любую функцию или функтор. Например грид подключается к этому эвенту и получает оповещение, когда экземпляр Lines меняется.
Таким образом структура грида у нас представлена двумя экземплярами Lines:
private: Lines m_rows; Lines m_columns;
Переходим к данным. Каким образом давать гриду информацию о том, какие данные он будет отображать и как их отображать? Здесь уже всё изобретено до нас — я воспользовался триадой MVC (Model-View-Controller). Начнем с элемента View.
Так же как класс Lines определяет не одну линию, а целый набор, определим класс View как нечто, что отображает какие-то однородные данные в некотором подмножестве ячеек грида. Например, у нас в первом столбце будет отображаться текст.
Это означает, что мы должны создать объект, который умеет отображать текстовые данные и который умеет говорить, что отображаться эти данные должны в первой колонке. Так как данные у нас могут отображаться разные и в разных местах, то лучше реализовать эти функции в разных классах. Назовем класс, который умеет отображать данные, собственно View, а класс, который умеет говорить где данные отображать Range (набор ячеек). Передавая в грид два экземпляра этих классов, мы как раз указываем что и где отображать.
Давайте подробнее остановимся на классе Range. Это удивительно маленький и мощный класс. Его главная задача — быстро отвечать на вопрос, входит ли определенная ячейка в него или нет. По сути это интерфейс с одной функцией:
class Range < public: virtual bool HasCell(CellID cell) const = 0; >;
Таким образом можно определять любой набор ячеек. Самыми полезными конечно же будут следующие два:
class RangeAll < public: bool HasCell(CellID cell) const override < return true; >>; class RangeColumn < public: RangeColumn(UINT_t column): m_column(column) <>bool HasCell(CellID cell) const override < return cell.column == m_column; >private: UINT_t m_column; >;
Первый класс определяет набор из всех ячеек, а второй — набор из одного конкретного столбца.
- Сколько надо места, что бы отобразить данные (например чтобы колонкам установить ширину, достаточную для отображения текста — режим Fit)
- Дай текстовое представление данных (чтобы скопировать в буфер обмена как текст или отобразить в tooltip)
class View < public: virtual void Draw(DrawContext virtual Size GetSize(DrawContext virtual bool GetText(CellID cell, INTENT intent, String >;
А что, если мы хотим отрисовать разные типы данных в одной и той же ячейке? Например нарисовать иконку и рядом текст или нарисовать чекбокс и рядом текст. Не хотелось бы для этих комбинаций реализовывать отдельный тип View. Давайте разрешим в одной ячейке показывать несколько View, только нужен класс, который говорит как разместить конкретный View в ячейке.
class Layout < public: virtual void LayoutView(DrawContext cellRect, Rect >;
Для наглядности рассмотрим пример в котором в первом столбце отображаются чекбоксы и текст. Во втором столбце представлены радио-кнопки, квадратики с цветом и текстовое представление цвета. И еще в одной ячейке есть звёздочка.
Например для чекбокса мы будем использовать LayoutLeft, который спросит у View его размер и «откусит» прямоугольник нужного размера от прямоугольника ячейки. А для текста мы будем использовать LayoutAll, к которому в параметре cellRect перейдет уже усеченный прямоугольник ячейки. LayoutAll не будет спрашивать размер у своего View, а просто «заберет» все доступное пространство ячейки. Можно напридумывать много разных полезных Layouts, которые будут комбинироваться с любыми View.
Возвратимся к классу Grid, для которого мы хотели задавать данные. Получается, что хранить мы можем тройки , которые определяют в каких ячейках, каким образом отображать данные, плюс как эти данные должны быть расположены внутри ячейки. Итак класс Grid у нас выглядит примерно так:
class Grid < private: Lines m_rows; Lines m_columns; vector> m_data; >;
Вот как выглядит m_data для нашего примера
В сущности, этого достаточно для отрисовки грида. Но информация организована не оптимальным образом — просто список записей, определяющих отображение данных.
Давайте подумаем, как с помощью нашего класса Grid можно отрисовать какую-то ячейку.
-
Нужно отфильтровать m_data и оставить только те тройки, для которых наша ячейка попадает в Range
for (auto d: grid.m_data) if (d.range->HasCell(cell)) cell_data.push_back(d);
Rect cellRect = CalculateCellRect(grid.m_rows, grid.m_columns, cell);
vector view_rects(cell_data.size()); auto view_rect_it = view_rects.begin(); for (auto d: cell_data) d.layout->LayoutView(grid.GetDC(), d.view, cellRect, *view_rect_it++);
auto view_rect_it = view_rects.begin(); for (auto d: cell_data) d.view->Draw(grid.GetDC(), *view_rect_it++, cell);
class CellCache < public: CellCache(Grid grid, CellID cell); void Draw(DrawContext private: CellID m_cell; Rect m_cellRect; vector> m_cache; >;
Этот класс в конструкторе выполняет первые три пункта и сохраняет результат в m_cache. При этом функция Draw получилась достаточно легковесной. За эту легковесность пришлось заплатить в виде m_cache.
Поэтому создавать экземпляры такого класса на каждую ячейку будет накладно (мы ведь договорились не иметь данных, зависящих от общего количества ячеек). Но нам и не надо иметь экземпляры CellCache для всех ячеек, достаточно только для видимых. Как правило в гриде видна небольшая часть всех ячеек и их количество не зависит от общего числа ячеек.
Таким образом у нас появился еще один класс, который управляет видимой областью грида, хранит CellCache для каждой видимой ячейки и умеет быстро рисовать их.
class GridCache < public: GridCache(Grid grid); void SetVisibleRect(Rect visibleRect); void Draw(DrawContext private: Grid m_grid; Rect m_visibleRect; vectorm_cells; >;
Когда пользователь меняет размер грида или скроллирует содержимое, мы просто выставляем новый visibleRect в этом объекте. При этом переформируется m_cells, так чтобы содержать только видимые ячейки. Функциональности GridCache достаточно, что бы реализовать read-only грид.
class GridWindow < public: Grid GetGrid() < return m_gridCache.GetGrid(); >void OnPaint() < m_gridCache.Draw(GetDrawContext()); >void OnScroll() < m_gridCache.SetVisibleRect(GetVisibleRect()); >void OnSize() < m_gridCache.SetVisibleRect(GetVisibleRect()); >private: GridCache m_gridCache; >;
Разделение классов Grid и GridCache очень полезно. Оно позволяет, например, создавать несколько GridCache для одного экземпляра Grid. Это может использоваться для реализации постраничной печати содержимого грида или экспорта грида в файл в виде изображения. При этом объект GridWindow никаким образом не модифицируется — просто в стороне создается GridCache, ссылающийся на тот же экземпляр Grid, в цикле новому GridCache выставляется visibleRect для текущей страницы и распечатывается.
Как же добавить интерактивности? Здесь на первый план выходит Controller. В отличие от остальных классов, этот класс определяет интерфейс со многими функциями. Но лишь потому, что самих мышиных событий достаточно много.
class Controller < public: virtual bool OnLBttnDown(CellID cell, Point p) = 0; virtual bool OnLBttnUp(CellID cell, Point p) = 0; . >;
Так же как и для отрисовки, для работы с мышью нам нужны только видимые ячейки. Добавим в класс GridCache функции обработки мыши. По положению курсора мыши определим какая ячейка (CacheCell) находится под ней.
Далее в ячейке для всех View, в чей прямоугольник попала мышь, забираем Controller и вызываем у него соответствующий метод. Если метод возвратил true — прекращаем обход Views. Данная схема работает достаточно быстро. При этом нам пришлось в класс View добавить ссылку на Controller.
Осталось разобраться с классом Model. Он нужен как шаблон адаптер. Его основная цель — предоставить данные для View в «удобном» виде. Давайте рассмотрим пример. У нас есть ViewText который умеет рисовать текст.
Что бы его нарисовать в конкретной ячейке, этот текст надо для ячейки запросить у объекта ModelText, который, в свою очередь, лишь интерфейс, а его конкретная реализация знает откуда текст взять. Вот примерная реализация класса ViewText:
class ViewText: public View < public: ViewText(ModelText model): m_model(model) <>void Draw(DrawContext const StringGetText(cell); dc.DrawText(text, rect); > private: ModelText m_model; >;
Таким образом несложно угадать какой интерфейс должен быть у ModelText:
class ModelText: public Model < public: virtual const String virtual void SetText(CellID cell, const String >;
Обратите внимание, мы добавили сеттер для того, что бы им мог воспользоваться контроллер. На практике наиболее часто используется реализация ModelTextCallback
class ModelTextCallback: public ModelText < public: functiongetCallback; function setCallback; const String return getCallback(cell); >void SetText(CellID cell, const String if (setCallback) setCallback(cell, text); >>;
Эта модель позволяет при инициализации грида назначить лямбда функции доступа к настоящим данным.
Ну а что же общего у моделей для разных данных: ModelText, ModelInt, ModelBool . В общем-то ничего, единственное, что про них всех можно сказать, что они должны информировать все заинтересованные объекте о том, что данные изменились. Таким образом базовый класс Model у нас примет следующий вид:
class Model < public: virtual ~Model() <>Event_t changed; >;
В итоге наш грид разбился на множество небольших классов, каждый из которых выполняет четко определенную небольшую задачу. С одной стороны может показаться, что для реализации грида представлено слишком много классов.
Но, с другой стороны, классы получились маленькими и простыми, с четкими взаимосвязями, что упрощает понимание кода и уменьшает его сложность. При этом всевозможные комбинации наследников классов Range, Layout, View, Controller и Model дают очень большую вариативность. Использование лямбда функций для ModelCallback позволяют легко и быстро связывать грид с данными.
В следующей заметке я опишу как реализовать стандартную функциональность грида: selection, sorting, column/row resize, printing, как добавить заголовок (фиксированные верхние строки и левые столбцы).
Раскрою небольшой секрет — все что описано в данной статье уже достаточно для реализации вышеперечисленного. Если какую-то функциональность я пропустил, пожалуйста, пишите в комментариях и я опишу их реализацию в следующей статье.
Источник: habr.com