Пишем свой QTableView с нуля

Итак жил был фреймворк Qt и последние 10 лет ничего почти в нем не менялось. И захотел один чел написать свой QTableView с нужным ему функционалом, а именно захотелось ему выводить ячейки в несколько рядов в одной строке. Ещё ему хотелось растягивать одну из ячеек по ширине двух других и т.д. (ну как в 1С например).

Искал, искал чел готовый пример в интернете и не находил. И вот однажды подумал он посмотреть как сделан внутри сам QTableView и стало плохо ему от количества строк кода, не одна тысяча там.

И взял с собой терпения чел и пошел по коду, долго он шел, и примерно на 5 день понял он, что здесь уже проходил. И обернулся и посмотрел чел на свой путь и открылось ему, что ходит он по кругу. И предстал ему весь его путь и вспомнил он все дороги и открылось ему царство знаний и понял он чего он хочет.

Тепер к делу: надо создать шаблон расположения секций. Это по сути как шахматная доска, только теперь у строки может быть 2,3 и т.д. ряда. И теперь одну ячейку можно располагать на 2,3 и т.д. клетках шахматной доски как по горизонтале так и по вертикале.

Приходится теперь как-то обозвать что есть что. У строки есть теперь горизонтальные и вертикальные ряды. Ряды вертикальные не будем путать с колонками. Понятие колонки оставим со смыслом как в модели данных, то есть колонка это номер поля (в select запросе).

Теперь номер поля может быть в шаблоне указан в нескольких ячейках с одним только ограничением — в строго в прямоугольной области. То есть не где попало, а компактно в пределах визуального поямоугольника и без пропусков.

А далее оказалось для нужной отрисовки мы переопределяем метод painEvent класса QTableView и painEvent класса QHeaderView и получается, что совсем не сложно нарисовать так:

f83f499f1381002c6008a283b4eb1130.png

Итак смысл прост в отрисовке QTableView. А именно: через drawCell рисуем каждую ячейку отдельно, передавая координаты ее прямоугольника, данные и стиль отрисовки. Потом рисуем линии сетки между ячейками.

Чтобы знать координаты, ширину и высоту каждой ячейки используются четыре новых контейнера. Они логично хранятся в горизонтальном хэдере.

QTableView работает вместе с классами заголовками QHeaderView. Хедеров две штуки: горизонтальный и вертикальный.

Хэдеры имеют важное значение, именно по их геометрии (расположение секций) мы определяем расположение (геометнию) ячеек самой таблицы (плюс смещение к конкретной строке по Y). Также в хэдерах мы изменяем ширину колонок или высоту строк. По сути это удобный каркас (шаблон) геометрии ячеек.

Нам придется сделать свои хэдеры. И приходится теперь отдельно делать горизонтальный и вертикальный хэдер, потому-что вся эта универсальность Qt нам не подходит.

Горизонтальный хэдер (QpHorHeaderView) это будет каркас расположения ячеек. Также как и раньше QTableView для получения информации (куда рисовать ячейку) будет обращаться к горизонтальному QHeaderView для получения QRect ячейки плюс будет добавлять смещение по y для отрисовки в конкретной строке.

Для большей совместимости мы сохраним вертикальный хедер как таковой, но по сути он будет брать все данные о геометрии ячеек из горизонтального хэдера.

Надо немного сказать, что создание своего QpTableView идёт по принципу попытки сохранения совместимости с оригинальным функционалом QTableView, то есть методы класса остаются практически те же,.

Но логично удалить часть функционала, связанная с объединением ячеек (span), а также можно удалить функционал реверса секций, то есть отображения в обратном порядке, так как для нас это на самом деле не актуально. Ещё не актуально скрывать секции и этот функционал тоже удалим. Зачем скрывать секцию, если можно просто инициализировать новый шаблон секций.

Надо понимать, что отрисовка хэдеров и самой таблицы это отдельные независимые операции. В каждом классе для этого переопределен виртуальный метод paintEvent.

Примечание: очень своеобразная ширина и высота прямоугольника QRect в Qt. Оказывается если мы видим через qDebug () такое: QRect (0,0 101×101) и вроде бы ширина (и высота) равны 101. Но самом деле это означает реально ширину (или высоту) 100 px.
А 101 это количество пикселей, то есть количество от 0 до 100, и это равно 101 штуке.

По времени создание своей QTableView и двух QHeaderView заняло примерно 4 рабочих недели. Поскольку мы удалили span функционал, а он был сильно интегрирован, нам пришлось восстановить работу практически всего сломанного функционала, в частности интерактивного изменения ширины колонок и высоты строк (рядов) мышкой, также поломалось выделение (ячеек, колонок, строк).

Пришлось разобраться довольно подробно в коде Qt. Например рамка ячейки в таблице рисуется явно через drawLine, а вот рамка в хэдере не рисуется явно, это просветы фона между секциями.

Отрисовка при выделении ячейки (ячеек) не приводит к отрисовки всей таблицы, а отрисовываются только те ячейки, изображение которых хоть немного визуально изменилось (изменение фона — это тоже визуальное изменение).

По поводу делегатов, то тут все просто: делегату передается прямоугольник ячейки и далее делегат сам все отрисовывает в ячейке как ему надо. Это про комбобоксы, всякие чекбоксы и т.д. Интересно где срабатывает отрисовка делегата — это («как ни странно») событие выделения ячейки и метод setSelection.

Еще наверное надо отметить, что классы QTableView и QHeaderView оба наследуются от QAbstractItemView (каждый естественно самостоятельно). Класс QAbstractItemView наследуется от QAbstractScrollArea.

Тут надо отметить, что выше указанные классы не полностью абстрактные, в них реализована и часть функционала. И что ещё важнее часть функционала реализована в их приватных спутниках типа QAbstractItemViewPrivate. А это значит, что нам придется собирать свои классы в составе исходников Qt (ветка gui), ибо методы приватных классов наружу в библиотеки не торчат, в чем и смысл заложенный Qt-никами.

По факту мы переписываем полностью классы QTableView и QHeaderView полностью заново.

Поэтому мы решили обозвать наши классы с префиксом Qp, чтобы было понятно и наглядно. То есть у нас будут классы типа QpTableView, QpHorHeaderView, QpVertHeaderView. Сами файлы будут называться qp_tableview.h/.cpp, то есть сеть ещё добавим знак подчеркивания. Знак подчеркивания хорошо выделяет наши файлы в куче исходников Qt.

Да теперь о сборке нашего функционала в составе исходников Qt. А по другому не получается, то есть сделать чисто открытое наследование от QAbstractItemView можно, оно скомпилируется, но при сборке линковщик не найдет методы QAbstratItemViewPrivate, потому-что они не помечены как экспортируемые в библиотеках Qt. В результате надо как минимум править заголовок Qt файла qabstractitemview_p.h и как следствие придется пересобрать опять же ветку исходников gui. То есть пересобрать исходники придется по любому как минимум один раз.

Это очень неприятная проблема для начинающих Qt-ников. Если кто знает как можно сделать свой класс , уналедованный от QAbstractItemView, чтобы можно было свободно его распространять без необходимости лезть в исходники Qt — буду безмерно признателен…

С другой стороны один раз сделав сборку из исходников понимаешь, что процесс добавления своего функционала внутрь, отладка, сборка совсем не трудные операции.

После того как мы первый раз отрисовали свою таблицу с новым шаблоном расположения ячеек, начинается самое интересное, а именно:

  1. Создание делегатов

  2. Прокрутка (скроллинг)

  3. Изменение ширины ряда при перетаскивании мышкой границы ряда вправо или влево (вверх, вниз).

  4. Выделение ячейки, выделение колонок, строк, выделение произвольного сектора и т.д.

И все это убивает огромное количество времени, так как заставляет досконально разобраться в работе отрисовки. Но оно того стоит, так как впоследствие лишних вопросов почему что-то не так отрисовываются уже не возникает.

Самые интересные этапы отрисовки таблицы и хэдеров возникают при интерактивном изменении ширины колонки или высоты ряда (в горизонтальном хэдере) при перетаскивании мышкой края колонки (или ряда). При перетаскивании края колонки (ряда) мы видим как изменяется таблица и выбираем приятный для себя визуальный вариант, но это значит, что отрисовка происходит постоянно при движении мышки. Тут используется таймер потому, что в событии moveMouseEvent нельзя сразу отрисовывать таблицу или хэдер. Правильнее взвести таймер и когда moveMouseEvent благополучно завершится (и возможно несколько раз) отрисовать таблицу по событию таймера, то есть спустя некоторое разумное время.

Итак поезд тронулся и вышла первая бета версия нашего набора классов QpTableView/QpHorHeaderView/QpVertHeaderView.

Небольшое ознакомительное видео по новым возможностям: QpTableView.

Завтра выложим на гитхабе и китайским товарищам на gitee.

© Habrahabr.ru