Пишем свой QTableView с нуля
Итак жил был фреймворк Qt и последние 10 лет ничего почти в нем не менялось. И захотел один чел написать свой QTableView с нужным ему функционалом, а именно захотелось ему выводить ячейки в несколько рядов в одной строке. Ещё ему хотелось растягивать одну из ячеек по ширине двух других и т.д. (ну как в 1С например).
Искал, искал чел готовый пример в интернете и не находил. И вот однажды подумал он посмотреть как сделан внутри сам QTableView и стало плохо ему от количества строк кода, не одна тысяча там.
И взял с собой терпения чел и пошел по коду, долго он шел, и примерно на 5 день понял он, что здесь уже проходил. И обернулся и посмотрел чел на свой путь и открылось ему, что ходит он по кругу. И предстал ему весь его путь и вспомнил он все дороги и открылось ему царство знаний и понял он чего он хочет.
Сразу небольшое ознакомительное видео по новым возможностям: QpTableView.
Теперь к делу: надо создать шаблон расположения секций. Это по сути как шахматная доска, только теперь у строки может быть 2,3 и т.д. ряда. И теперь одну ячейку можно располагать на 2,3 и т.д. клетках шахматной доски как по горизонтале так и по вертикале.
Приходится теперь как-то обозвать что есть что. У строки есть теперь горизонтальные и вертикальные ряды. Ряды вертикальные не будем путать с колонками. Понятие колонки оставим со смыслом как в модели данных, то есть колонка это номер поля (в select запросе).
Теперь номер поля может быть в шаблоне указан в нескольких ячейках с одним только ограничением - в строго в прямоугольной области. То есть не где попало, а компактно в пределах визуального поямоугольника и без пропусков.
А далее оказалось для нужной отрисовки мы переопределяем метод paintEvent класса QTableView и paintEvent класса QHeaderView и получается, что совсем не сложно нарисовать так:
Итак смысл прост в отрисовке QTableView. А именно: через drawCell рисуем каждую ячейку отдельно, передавая координаты ее прямоугольника, данные и стиль отрисовки. Потом рисуем линии сетки между ячейками.
Чтобы знать координаты, ширину и высоту каждой ячейки используются четыре новых контейнера. Они логично хранятся в горизонтальном хэдере.
QTableView работает вместе с классами заголовками QHeaderView. Хедеров две штуки: горизонтальный и вертикальный.
Хэдеры имеют важное значение, именно по их геометрии (расположение секций) мы определяем расположение (геометнию) ячеек самой таблицы (плюс смещение к конкретной строке по Y). Также в хэдерах мы изменяем ширину колонок или высоту строк. По сути это удобный каркас (шаблон) геометрии ячеек.
Нам придется сделать свои хэдеры. И приходится теперь отдельно делать горизонтальный и вертикальный хэдер, потому-что вся эта универсальность Qt нам не подходит.
Горизонтальный хэдер (QpHorHeaderView) это будет каркас расположения ячеек. Также как и раньше QTableView для получения информации (куда рисовать ячейку) будет обращаться к горизонтальному QHeaderView для получения QRect ячейки плюс будет добавлять смещение по y для отрисовки в конкретной строке.
Для большей совместимости мы сохраним вертикальный хедер как таковой, но по сути он будет брать все данные о геометрии ячеек из горизонтального хэдера.
Надо немного сказать, что создание своего QpTableView идёт по принципу попытки сохранения совместимости с оригинальным функционалом QTableView, то есть методы класса остаются практически те же,.
Но логично удалить часть функционала, связанная с объединением ячеек (span), а также можно удалить функционал реверса секций, то есть отображения в обратном порядке, так как для нас это на самом деле не актуально. Ещё не актуально скрывать секции и этот функционал тоже удалим. Зачем скрывать секцию, если можно просто инициализировать новый шаблон секций.
Надо понимать, что отрисовка хэдеров и самой таблицы это отдельные независимые операции. В каждом классе для этого переопределен виртуальный метод paintEvent.
Примечание: очень своеобразная ширина и высота прямоугольника QRect в Qt. Оказывается если мы видим через qDebug() такое: QRect(0,0 101x101) и вроде бы ширина (и высота) равны 101. Но самом деле это означает реально ширину (или высоту) 100px.
А 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 - буду безмерно признателен...
С другой стороны один раз сделав сборку из исходников понимаешь, что процесс добавления своего функционала внутрь, отладка, сборка совсем не трудные операции.
Примечание: на самом деле разобрались с этим вопросом позднее. Оказалось что, чтобы наши классы QpTableView и т.д. предоставить открыто для добавления к любому проекту (без пересборки исходников Qt) надо еще свой вариант класса QAbstractItemView/QAbstractItemViewPrivate тащить с собой. А вот QAbstractScrollArea уже не надо, потому что QAbstractItemViewPrivate не экспортируемый, а QAbstractScrollAreaPrivate экспортируемый.
Вот есть видео на эту тему:
После того как мы первый раз отрисовали свою таблицу с новым шаблоном расположения ячеек, начинается самое интересное, а именно:
Создание делегатов
Прокрутка (скроллинг)
Изменение ширины ряда при перетаскивании мышкой границы ряда вправо или влево (вверх, вниз).
Выделение ячейки, выделение колонок, строк, выделение произвольного сектора и т.д.
И все это убивает огромное количество времени, так как заставляет досконально разобраться в работе отрисовки. Но оно того стоит, так как впоследствие лишних вопросов почему что-то не так отрисовываются уже не возникает.
Самые интересные этапы отрисовки таблицы и хэдеров возникают при интерактивном изменении ширины колонки или высоты ряда (в горизонтальном хэдере) при перетаскивании мышкой края колонки (или ряда). При перетаскивании края колонки (ряда) мы видим как изменяется таблица и выбираем приятный для себя визуальный вариант, но это значит, что отрисовка происходит постоянно при движении мышки. Тут используется таймер потому, что в событии moveMouseEvent нельзя сразу отрисовывать таблицу или хэдер. Правильнее взвести таймер и когда moveMouseEvent благополучно завершится (и возможно несколько раз) отрисовать таблицу по событию таймера, то есть спустя некоторое разумное время.
Итак поезд тронулся и вышла первая бета версия нашего набора классов QpTableView/QpHorHeaderView/QpVertHeaderView.
Завтра выложим на гитхабе и китайским товарищам на gitee и oschina.
Если долго, нудно , как шел процесс, то здесь на kkmspb.ru (записки сумасшедшего)