QTableView новые возможности

О чём тут речь? Как располагать ячейки одной строки компактно в 1,2,3 ряда. При этом понятие конкретной строки должно оставаться, просто одно поле может быть под другим полем чисто для удобства отображения.

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

фотка 1

А дальше будет долгий, но интересный путь - как самим сделать свой вариант QTableView.

Е3сть кстати небольшое видео об этом QpTableView.

Пишем в виде шпаргалки по горячим следам как шел процесс длиною в пару месяцев.

Мы узнали, что в wxWidgets разраблтчики такое уже реализовали, так же пример 1С 7.7 тоже такое реализовано.

А что Qt5,6? А воз и ныне там. Стыдно товарищи.

А давайте отважимся изучить исходники старого доброго Qt 4.8.1,  и можно не смотреть в сторону Qt5,6  ибо там ничего существенно не изменили впоследствие.

На странице QTableView paint() мы уже попробовали посмотреть функцию отрисовки QTableView paintEvent.

Но теперь мы идём дальше и пытаемся управлять процессом отрисовки по-своему. Для этого мы сделали дубликат  QTableView и QHeaderView в исходниках Qt, класс назвали не долго думая QpTableView и QpHorHeaderView , к нему не забыли создать их приватные классы  QpTableViewPrivate и QpHorHeaderViewPrivate, и  понеслось дальше, пересобрали исходники, и  на дупликатах экспериментируем. Вертикальный хэдер тоже будет отдельный QpVertHeaderView(Private), это неизбежно.

Qp это наш так сказать префикс, по аналогии с Qx, довольно удобно различать свои классы с классами самого Qt.

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

Например как вычисляется позиция по вертикали для начала отрисовки строки: просто у verticalHeader берем sectionSize() и умножаем на номер строки - все банально. 

d->verticalHeader->sectionSize(row)

И далее по коду вызывается метод headerSectionSize класса QHeaderViewPrivate. 

То есть получается, что QpHorHeaderView является шаблоном (каркасом) для отрисовки ячеек строки. И это логично.

А почему бы не попробовать изменить здесь высоту вертикального хедера в два раза например.

Итак изначально есть некая QTableView, которая выглядит примерно так (span встречается 1 раз, но дело не в этом):

фотка 2

Попробуем что-нибудь изменить, например,  переопределим sectionViewportPosition и sectionSize  горизонтального хэдера (класс QHeaderView), чтобы увеличить значение в 2 раза.

QpVertHeaderView::QpVertHeaderView( Qt::Orientation orientation,
                          QWidget *parent )
    :
      QHeaderView(orientation,
                  parent)
{
}


int QpVertHeaderView::sectionViewportPosition(int logicalIndex) const
{

     int pos = QpVertHeaderView::sectionViewportPosition( logicalIndex ) * 2;

     qDebug()<<" QpVertHeaderView::sectionViewportPosition logicalIndex " << logicalIndex << "  pos " << pos;

     return pos;
}

int QpVertHeaderView::sectionSize(int logicalIndex) const
{
    int sz = QpVertHeaderView::sectionSize( logicalIndex ) *2 ;

    qDebug()<<" QpVertHeaderView::sectionSize logicalIndex " << logicalIndex << " sz " <
фотка 3

И видим, что увеличение размера строк и ячеек произошло в 2 раза. Причем в sectionViewportPosition увеличиваем именно смещение по вертикали от начала вьюпорта, а в sectionSize отдаем  увеличенную высоту строк вертикального хедера.

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

При этом надо отметить, что выделения , прокрутка работает не нормально, но это пока не важно.

Переходим к главной идеи

А хочется нам, как-то теперь попробовать расположить колонку 2 под колонкой 1, то есть в два ряда.

Отрисовать таким образом не проблема, как выясняется. Делается это там же в paintEvent наследника класса QTableView:

фотка 4

Что работает неправильно и бросается в глаза (на данном этапе).

1. Остаётся убрать как-то пустую колонку 3, содержание ее переместилось вниз колонки 2. На самом деле тут надо переместить содержание и заголовок колонки 4 влево на одну позицию.

2. Горизонтальный хэдер (сверху) неправильно колонки отрисовывает, точнее  отрисовывает как раньше (по старому). Для этого смотрим paintEvent хедера. Дело в том, что у хедера свой paintEvent, в отличии от QTableView ("как ни странно"). И тут в выясняется, что в коде по факту вызов paintEvent будет переадресовываться на виртуальный метод paintSection класса QHeaderView, который можно переопределить.

В paintSection  передается QRect &rect и int logicalIndex. rect это куда будем рисовать, а logicalIndex это номер поля (колонки) в модели данных для получения данных содержания ячейки, что будем рисовать в прямоугольник.

Учитываем выше сказанное, получаем результат как на картинке ниже. Это фактически прям то, что надо:

фотка 5

Что дальше работает еще не правильно.

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

Проблема в том, что тут не правильно вычисляется QModelIndex ячейки по QPointer, который передается на вход обработчика события mousePressEvent.

QModelIndex QTableView::indexAt(const QPoint &pos) const - определение индекса для модели данных по геометрическому расположению ячейки.

Более пристальное изучение выполнения кода приводит к некоторым полезным выводам:

Вся визуализация ячеек таблицы происходит на основании данных, хранящихся в  объекте класса QHeaderViewPrivate. То есть QTableView, также пользуется данными о расположении ячеек из объекта класса QHeaderViewPrivate. Это значит, что надо переделывать именно эту часть кода. Кстати именно поэтому придется собирать свое развитие QpHorHeaderView/QpTableView/QpVertHeaderView именно в составе исходников

Вот основные методы, которые предстоит переделать:

int QHeaderViewPrivate::headerVisualIndexAt(int position) const - по значению координаты x в области ячейки (позиция) однозначно определяем визуальный индекс.

int QHeaderViewPrivate::headerSectionPosition(int visual) const - теперь наоборот по визуальному номеру колонки (индексу) определяем левую крайнюю координату ячейки (для горизонтального хэдера это по x).

Это две важные функции для начала. Итак мы поняли одну из главных концепций парадигмы модель-представление: по визуальным координатам получаем индекс для модели данных и по индексу получаем расположение ячейки, в которую надо отрисовать данные.

Далее сохраняем промежуточный вариант, то есть  делаем резервную копию QpTableView и QpHorHeaderView.  

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

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

Для отрисовки переделаем функцию headerSectionPosition. Но теперь она у нас будет возвращать сразу прямоугольник (QRect) с координатами по x и y секции заголовка. Данные координат мы теперь тоже будем хранить по новому, в новых контейнерах. И вот чего мы добились уже:

фотка 6

Секцией мы теперь будем называть область, где расположено значение из модели данных. В общем со смыслом как ранее в Qt, но теперь только секция может располагаться на 1,2,3 и т.д ячейках по горизонтали (и по вертикали).


И вот теперь приходит понимание, что ничто не остановит нас от реализации собственной QpTableView, которую можно будет использовать как и раньше с любой моделью данных (из работающих с QTableView).

Важный момент - придется разделить класс QHeaderView отдельно на горизонтальный и вертикальный. В оригинальном QHeaderView реализации была одинаковая для горизонтального и вертикального хэдера, теперь будут разные хэдеры (QpHorHeaderView и QpVertHeaderView).

Убираем не нужный функционал

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

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

Небольшое лирическое отступление: выкидывая из QHeaderView span функционал, мы поразились как это усложняет понимание и использование кода. Даже на мгновение показалось, что span составляет 80% кода.

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

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

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

фотка 7

Отрисовываются ячейки и рамка вокруг. 

На данном этапе ещё не работает правильно прокрутка и выделение ячеек, отрисовка пустых областей вьюпорта таблицы, и ещё много наверное чего. Но на самом деле больше половины дела сделано.

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

Несколько слов о прокрутке (вертикальный скроллинг к примеру). При щелчке мышкой внизу справа на кнопке прокрутки вниз срабатывает событие QMousePressEvent, далее срабатывает виртуальный метод scrollContentsBy класса QTableView, который переопределяет scrollContentsBy  класса QAbstractScrollArea.

Дело в том,что QTableView наследуется от QAbstractItemView и далее от QAbstractScrollArea.

Восстанавливаем сломанную функциональность скроллинга. 

За 5 дней уже не успеваем допилить свой класс. Об'ем кода для изменения примерно  10000 строк как оказывается.

Заметим, по поводу применения css стилей такой важный момент: теперь мы понимаем, что ячейки отрисовываются каждая отдельно в индивидуальную прямоугольную область в буквально смысле. Теперь вопрос а когда применяются стили для ячеек. По факту при drawCell мы передает и options, и по догике только внутри drawCell может происходить применение стилей.

Разбираемся как работает скроллинг, его выбросить нельзя. Скроллинг работает по двум направлениям, соответственно по координатам x и y. Для визуализации отображаемой области таблицы введено понятие вьюпорт (viewport). Вьюпорт это отображаемая часть таблицы. Соответственно у вьюпорта есть начальные смещения по x и y, они именуются offset.

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

Управление вьюпортом идёт через события от мышки, клавиши,..

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

Далее наследование QHeaderView  и QTableView от одних и тех же классов не означает, что они используют весь функционал QAbstractItemView:

QHeaderView2 / 	QAbstractItemView / QAbstractScrollArea
		
QTableView   /  QAbstractItemView / QAbstractScrollArea

Например QHeaderView не обращает внимание на scrollDelayOffset класса QAbstractItemView, а QTableView использует его активно. Все это немного вводит в заблуждение. 

Шаг скролл-бара  (например горизонтальный) измеряется в колонках. Настраивается минимальное и максимальное значение в методе updateGeometries.

Надо сказать, что отрисовка при скроллинге наверное самая трудная часть реализации функционала.

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

length приватного класса QHeaderView используется для отслеживания размера хэдера в писелях. Так же это будет и размер   самой таблицы QTableView по соответствующей оси.

length используется для отслеживания выхода за границы таблицы.

Изменение length вертикального хэдера происходит при вызове метода insertRows класса QTableView. У горизонтального хэдера соответственно insertColumns.

logicalIndexAt для горизонтального хэдера пришлось переделать. Теперь для однозначного определения логического индекса недостаточно передавать координату по x , надо еще передавать координату по y. Мы оставили вариант logicalIndexAt с передачей одного параметра QPoint (так проще).
int logicalIndexAt(int position) - удалили .

Итак день 8-ый завершается. Скроллинг начал работать нормально.

Переходим к делегатам. Поверяем создание делегатов (например QComboBox),  проверяем корректность работы делегата. Все это уже об интерактивном взаимодействии пользователя с интерфейсом таблицы. Это уже проще.

Метод visualRect надо подправить.visualRect по индексу модели данных выдает прямоугольник ячейки.

Когда у нас происходит двойной клик по ячейке, у которой назначен делегат, например QComboBox, тогда будет вызван QRect QTableView::visualRect(const QModelIndex &index) const.

Думали мы, что с прокруткой мы разобрались, но оказалось есть еще ньюанс. Дело в том, что при прокрутке до конца строк (или колонок) возникает обычно ситуация, что первая видимая строка( или колонка) отображается уже не полностью, а частично. При этом последняя строка ( или колонка соответственно отображается полностью. И теперь в этой ситуации нам надо учитывать, что offset хэдера (допустим вертикального) не кратен высоте строки. Учитывать это надо в  методе QTableView::columnAt.

int QTableView::columnAt(int x) const // не используем больше
int QTableView::columnAt(int x, int y) const // используем этот

Теперь в columnAt надо еще передавать вертикальную координату (по вьюпорту), чтобы понимать какая линия (колонок) в горизонтальном хэдере. 

Чтобы понимать, где хранится вертикальный offset вьюпорта надо обратиться к классу QAbstractScrollAreaPrivate, который содержит смещение вьюпорта:
xoffset
yoffset

Получить их можно только через ловлю QTableView::scrollContentsBy :
q->scrollContentsBy(dx, 0);
q->scrollContentsBy(0, dy);

И потом для QTableView это учитывается в соответствующем хэдере : 
например d->verticalHeader->offset(), значением которого мы и будем пользоваться в QTableView::columnAt.

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

Делегат по умолчанию вызывается через метод QDefaultItemEditorFactory::createEditor. 

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

QRect QStyleSheetStyle::subElementRect(SubElement se, const QStyleOption *opt, const QWidget *w) const

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

Теперь попробуем восстановить resize размеров секций, то есть попробуем восстановить изменение размеров секций интерактивно (действиями пользователя).

Вобщем-то тут проблем не возникло. Все секции нормально меняют ширину, если потащить мышкой за край границы. При подводе курсора мышкой краю границы разделения секций курсор менят вид и в этот момент можно кликнуть и потащить границу в сторону.

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

День десятый. Ну вроде бы все работает нормально. Пора делать видео для демонстрации новых возможностей. Далее обкатаем новый функционал на паре  коммерческих проектах и наверняка будут ещё исправления.

Итак убито 2 рабочие недели на реализацию своей QTableView. Какие результаты и ощущения появились по итогам разборки кода исходников.

Во-первых это то, что в Qt хороший рабочий проверенный многократно код, которому можно доверять и на котором можно учиться. Все по делу, логично и правильно. Респект тролям.

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

Ну и  в третьих это то, что в общем-то можно создать и свой фреймворк, если очень захочется. Не даром все утверждают, что главное достоинство Qt, это нативная отрисовка Гуя. И возникает логичный вопрос, а почему до сих пор так мало попыток сделать свой полностью gui. У двух студентов из норвежского университета давно за лет 5 примерно это получилось, они открыли исходный код, чтобы все могли изучить его, но "ни у кого" не получилось повторить это по своему.

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

Началась третья неделя изучения QTableView и имеем уже прилично работающий вариант со своими делегатами (QComboBox), рамка между ячейками горизонтального хедера и рамка между ячейками.таблицы:

фотка 8

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

Осталось еще разобраться с выделением ячеек как оказывается. Работает выделение не до конца как-то правильно. При клике на другой ячейке выделение на предыдущей иногда не снимается (остаётся фон). К тому же рамка горизонтальная так же иногда почему-то исчезает. 

Тут надо сказать, что paintEvent класса QTableView вызывается с указанием какую область таблицы отрисовать и часто не всю таблицу, а какую-то конкретную ячейку или несколько ячеек. Вся эта информация заложена в передаваемом параметре QPaintEvent *event. А именно в event есть параметр QRegion region, который в свою очередь имеет контейнер QVector rects (он и содержит набор прямоугольников, которые надо отрисовать).  В коде QTableView::paintEvent(..) эти прямоугольники называются dirtyArea. 

Клик по ячейке вызывает ее визуальное выделение и отрисовку как оказывается, ибо все что визуально меняется это отрисовка. Итак как понять кто передает координаты отрисовки в paintEvent.

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

Поскольку именно операционная система получает изначально информацию об аппаратном событии, то должен быть стандартный механизм передачи этого события в соответствующее приложение. И этот механизм конечно же существует и реализован (у нас Windows) через передачу указателей на соответствующие обработчики событий - смотрим файл qapplication_win.cpp.  

У событий в Windows есть номера типа :

#define WM_MOUSELEAVE                   0x02A3

Для обработки событий от операционной системы приложением (при старте) передается указатель на функцию обработчик , у нас :

extern "C" LRESULT QT_WIN_CALLBACK QtWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

В QtWndProc мы получаем все события от ОС и обрабатываем их как нам надо. Например клик мышкой по ячейке вызовет:

result = widget->translateMouseEvent(msg);

В параметре msg мы имеем хэндл главного окна, геометрию главного окно и координаты куда кликнули на экране. Причем координаты от начала экрана,а не окна приложения.

В translateMouseEvent много чего происходит, но нам надо выделить, что определяются координаты клика уже для внутреннего виджета в координатах внутреннего виджета и событие передается дальше через QApplicationPrivate::sendMouseEvent этому виджету:

res = QApplicationPrivate::sendMouseEvent(widget, &e, alienWidget, this, &qt_button_down, qt_last_mouse_receiver);

параметр widget - это указатель виджет приложения, либо главный либо может быть другой вложенный, например qt_scrollarea_viewport;
параметр e - это событие где есть координаты позиции клика внутри вложенного виджета

Примечание: для определения виджета внутри приложения, на котором сработал клик используется метод QApplicationPrivate::pickMouseReceiver:

QWidget *QApplicationPrivate::pickMouseReceiver(QWidget *candidate, const QPoint &globalPos,
                                                QPoint &pos, QEvent::Type type,
                                                Qt::MouseButtons buttons, QWidget *buttonDown,
                                                QWidget *alienWidget)

На выходе получим указатель на этот виджет (QWidget *alienWidget). 

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

Например у нас далее сработает case QEvent::MouseButtonPress: виртуального метода bool QWidget::event(QEvent *event) внутреннего виджета, который вызывает виртуальный метод mousePressEvent((QMouseEvent*)event);

Виртуальные методы можно переопределять в классах потомках и у нас будет вызван для QTableView вариант  void QAbstractItemView::mousePressEvent(QMouseEvent *event). Внутри уже будет определен индекс ячейки.

И вот в QAbstractItemView::mousePressEvent мы задаем прямоугольник QRect в качестве точки ( размер 0х0) и координаты клика внутри виджета и передаем этот прямоугольник в виртуальный метод setSelection:

virtual void setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command)

Таким образом еще прямоугольник для отрисовки не определен. Идем дальше setSelection переводит координаты клика в индекс(ы) модели данных и передает в метод QItemSelectionModel::select модели выделения: 

void QItemSelectionModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command)

QItemSelectionModel - это отдельная модель данных для учета выделения ячеек.

параметр QItemSelection это в индексах модели данных, а не координат x,y.

Интересный момент связан с вызовом сигнала emit selectionChanged(selected, deselected);. Дело в том,ч то сигнал не связан ни с одним слотом нигде. Но есть тем не менее вызывается такой метод:

void QItemSelectionModel::selectionChanged(const QItemSelection & _t1, const QItemSelection & _t2)
{
    void *_a[] = { 0, const_cast(reinterpret_cast(&_t1)), const_cast(reinterpret_cast(&_t2)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

То есть как получается, что в результате вызывается QMetaObject::activate, который в свою очередь (там не совсем понятно, много кода) вызывает:

void QTableView2::selectionChanged(const QItemSelection &selected,
                                   const QItemSelection &deselected)

Далее QTableView2::visualRegionForSelection

QRegion QTableView2::visualRegionForSelection(const QItemSelection &selection) const

В общем прошли мы долгий путь отрисовки выделения ячейки. Но неожиданно наткнулись на странное поведение QRect::width()

inline int QRect::width() const
{ return  x2 - x1   1; }

inline void QRect::setWidth(int w)
{ x2 = (x1   w - 1); }

x1=0; x2=100; width= 101;? почему ширина 101. Значит имеется ввиду, что ширина это количество пикселей.


Вот примерный путь по методам, начиная setSelection:

void QTableView2::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command)
	....
	void QItemSelectionModel::select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command)
	....
	void QItemSelectionModel::emitSelectionChanged(const QItemSelection &newSelection, const QItemSelection &oldSelection)
		emit selectionChanged(selected, deselected);
		....

void QTableView2::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)

В QTableView есть метод startTimer и killTimer. Они используются только для отрисовки, когда мышкой тащим за край секции для изменения размера. Это делается, чтобы видеть сразу в динамике новую геометрию колонок и строк, то есть в момент перемещения края секции делать repaint таблицы (и хэдера тоже). Дело в том, что в событии (к примеру в mouseMoveEvent) нельзя почему-то напрямую вызывать repaint таблицы. Для этого взводят таймер, потом событие mouseMoveEvent благополучно завершается, далее в очереди событий отрабатываются как правило еще события, потом приходит в свою очередь событие от таймера и происходит repaint таблицы. 

Надо отметить, что функционал таймера реализован в QAbstractEventDispatcherPrivate, указатель на который хранится в классе QThreadData, указатель на который есть в классе QObjectPrivate. То есть функционал таймера есть по умолчанию у всех классов наследников QObject.

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

В нашем варианте это становится не важно. То есть мы не скрываем колонки, мы просто заново инициализируем каркас (шаблон) расположения колонок.

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

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

0 1 2 2 5
3 4 2 2 6
7 4 8 8 8

Секция соответствует номеру колонки (модели данных), это у нас одно и то же. 

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

Заметили и пытаемся побороть странный дефект выделения, который проявляется иногда при последовательном выделении ячеек. Вдруг не снимается выделение с предыдущей ячейки и получается 2 ячейки выделены:

фотка 9

Оказывается последнее выделение хранится в переменной hover класса QAbstractItemViewPrivate. То есть не в QTableView, а в абстрактном классе QAbstractItemViewPrivate. Меняется индекс hover в методе QAbstractItemViewPrivate::setHoverIndex. Но вот вызывается метод setHoverIndex как-то непонятно странно не при каждом очередном выделении, а иногда через 2,3,4 раза, причем происходит это в обработчике события:
void QAbstractItemView::mouseMoveEvent(QMouseEvent *event)  и далее через метод QAbstractItemViewPrivate::checkMouseMove. И вот пока не понятно зачем это делается...

Но можно решить это проблему так: в 

void QTableView2::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command)

делать очищение hover через d->setHoverIndex( QModelIndex());

Заканчивается четвертая неделя, все работает вроде бы нормально, но встаёт новая проблема - надо обеспечить, чтобы в матрице (шаблоне колонок) можно было указывать колонки из модели данных в произвольном порядке и с пропусками в нумерации. То есть мы уходим от визуальных виртуальных номеров колонок и используем только логические номера из модели данных, что значительно упрощает жизнь.

Ещё небольшое дополнение о paintEvent. Там внутри используется некий QBitArray, так вот используется QBitArray только для блокировки повторной отрисовки одноц и той же ячейки в пределах одного paintEvent.

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


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

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

Теперь если мы захотим сделать открытое наследование классов снаружи, мы сталкиваемся с тем, что нам надо унаследоваться от QAbstractItemViewPrivate

А это возможно только в составе исходников Qt. Либо отказаться от идеи приватных классов и все наследование сделать открытым. Вообще закрытые приватные классы типа QAbstractItemViewPrivate ппри сборке qt логично не экспортируются в QtGuid4.dll. И соответственно мы не сможем собрать свой проект с открытым наследованием, при линковке получим примерно такое сообщение об ошибке:

error: LNK2019: unresolved external symbol "public: virtual __thiscall QAbstractItemViewPrivate::~QAbstractItemViewPrivate(void)" (??1QAbstractItemViewPrivate@@UAE@XZ) referenced in function __unwindfunclet$??0QzHeaderView2Private@@QAE@XZ$0

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

Но мы попытаемся. Первое, что надо сделать: весь функционал QHeaderViewPrivate перенести в QHeaderView. QHeaderViewPrivate  как таковой не используем более вообще.  Но функционал QAbstractItemView продолжаем использовать. То есть:

В коде QHeaderView все макросы типа 
Q_D( QHeaderView );
меняем на :
Q_D( QAbstractItemView );

В объявлении класса QHeaderView в private разделе добавляем:
Q_DECLARE_PRIVATE(QAbstractItemView)
и таким образом станут работать макросы Q_D( QAbstractItemView );

Аналогично поступаем с приватный классом QTableViewPrivate.

Итак компилируется наш проект нормально. Но в линковке в остаются ошибки типа unresolved external symbol "public: void __thiscall QAbstractItemViewPrivate::doDelayedItemsLayout(int):

..\QAbstractImtemView_0\qzabstractitemview.cpp(163) : warning C4100: 'parent' qzabstractitemview.obj:-1: error: LNK2019: unresolved external symbol "public: void __thiscall QAbstractItemViewPrivate::doDelayedItemsLayout(int)" (?doDelayedItemsLayout@QAbstractItemViewPrivate@@QAEXH@Z) referenced in function "public: __thiscall QzAbstractItemView::QzAbstractItemView(class QWidget *)" (??0QzAbstractItemView@@QAE@PAVQWidget@@@Z)

если мы пытаемся в коде вызвать d->doDelayedItemsLayout(); 

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

В коде у класса  QHeaderViewPrivate есть префикс Q_AUTOTEST_EXPORT, который объявляется в global.h файле и он пустой (Q_AUTOTEST_EXPORT).

Если установить для Q_AUTOTEST_EXPORT значение Q_DECL_EXPORT , то проблема линковки разрешится! 

По факту скомпилированный классов QHeaderViewPrivate и QHeaderView код помещается  в один файл qabstractitemview.obj. Отличие только в доступе к методам классов (Q_DECL_EXPORT).

То есть получается, что в библиотеке QtGuid4.dll реализация класса QHeaderViewPrivate скрыта от прямого использования извне (а QHeaderView пожалуйста используйте).

При статической сборке таких проблем скорее всего не будет изначально.

Таким образом получается, что исходники Qt, а именно заголовочный файл qabstractitemview_p.h подправить придется и потом придется пересобрать ветку исходников gui заново.

Либо как мы делали ранее собирать наши классы в составе исходником Qt, наследую их приватные классы.

Лично для нас собирать и отлаживать удобнее в составе исходников Qt, а для мирового сообщества конечно было бы проще распространять просто пару файлов, которые можно было бы подкинуть в свой конечный проект и пользоваться (но просто так не получится).

Можно попробовать вариант добавить в проект obj файл явно примерно таким образом и посмотреть,ч то будет:

LIBS  = D:\\QtSDK1.2.1\\QtSources\\4.8.1\\COMMON_DEBUG_SHARED_MDd\\gui\\tmp\\obj\\debug_shared\\qabstractitemview.obj

Но вообще-то теперь мы логично получаем already defined, что говорит о том, что есть 2 экземпляра каждого метода, один в QtGuid4.dll/QtGuid4.lib другой в qabstractitemview.obj. 

Кстати попутно выясняется, что в принципе lib файл это коллекция obj файлов, то по сути это одно и то же.

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

5 1 1 2 3
0 4 4 4 9

И это уже работает нормально. Остаётся только подправить выделение строк.

Оказывается чтобы выделение корректно работало надо подправить метод visualViewportForSelection. Этот метод по индексам выделенного прямоугольника (в таблице), создаёт регион прямоугольников с координатами отрисовки.

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

Ещё остаётся разобрать побочный эффект, а именно выделение строки кликом на секции вертикального хэдера.

Для шаблона, в котором нет поля с номером 0, ну например есть только номера 1,3,4,7 выделения секции вертикального хэдера не происходит. Это небольшая визуальная проблема, заложена она в методе isRowSelected в модели выделения selectedModel класса QSelectedItemModel. Дело в том, что в исходниках Qt поле с номером 0 (модели данных) всегда присутствует.

И тут приходит очередное прояснение мозга: в модели данных не должно быть пропусков в номерах полей. И у нас их тоже нет в модели данных, но в модели представления (QTableView) есть. И это надо исправить. То есть номера полей будут присутствовать все, но ненужные будут скрываться, что вполне себе у русле логики Qt.

Ещё раз неважно - какая модель данных QStandardItemModel или QSqlTableModel - номера полей начинаются с нуля и идут без пропусков (всегда).  Даже если вы в QStandardItemModel  создаете 5-ую колонку через 
model->setHorizontalHeaderItem(5, new QStandardItem(QString("555555")));
данные в ячейке col=0 row=... присутствует, точнее без явной установки значение есть и равно по дефолту QVariant(). Не говоря уже о QSqlTableModel , где по другому и не бывает.

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

фотка 10

Оказывается данный эффект устанавливается объектом QPainter (а не QStyleOptionHeader как могло показаться). А именно именно здесь painter.font().weight().

На самом деле тут не было проблемы. Просто фишка в том, что отрисовывая каждую секцию надо предварительно сохранять и потом восстанавливать объект painter, потому что в внутри кода метода paintSection объект painter возможно будет изменен, а передается painter в метод paintSection как указатель.

По итогам вышла первая бета версия 1.0.0, выложена на гитхабе, ютьюбе, хабре:

Итак начали мы на практике использовать свой же класс QpTableView в одном своем коммерческом проекте и обнаружили, что хочется ещё как-то иметь возможность выводить некие заголовки внутри уже самих ячеек, например как на картинке ниже:

фотка 11

И так для каждой строки соответственно. То есть слова типа НАЛ:, БН: должны повторяться стандартно на каждой строке. Конечно же эти слова не должны быть редактируемые в таблице и задаются один как-то в шаблоне.

Можно конечно придумать дополнительные колонки в модели данных, но это чревато путаницей при дальнейшем наследовании от модели данных.

Можно конечно расширить роли модели данных и там отдавать эти константные слова. 

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

Буквально за пару тройку часов мы реализовали эту отрисовку. Осталось подправить выделение, фон и все вроде бы все.

фотка 12

Выделение строки заработало почти гут. Нюанс остался такой, что первый клик по секции (сразу после инициализации шаблона) вертикального заголовка, то есть по строке, не приводит к выделению лейблов, а следующие клики происходят нормально.

При чем если переключится на другое приложение и обратно отрисовка фона происходит нормально 

Причина в том, что к option state не добавляется атрибут Selected. Остальные секции берут этот атрибут из модели выделения, но наши лейблы к модели выделения никак штатно не относятся.

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

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

К счастью проблема решается без изменения модели выделения или данных. 

Для нас вообще говоря оказалось достаточно выделять лейблы только при поведении onRowsSelected. Для выделения только колонок или произвольного выделения прямоугольного кадра - нас выделение лейблов не требуется.

Попутно мы сделали отрисовку рамки между секциями внутри строки и между самими строками отдельными. То есть теперь можно рисовать рамку отдельно внутри строки и между строками. 

Добавлена ещё одна фича для отрисовки лейблов, а именно - теперь указываем прямо в шаблоне как выравнивать лейблы, а именно символы > и < говорят сами за себя.

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

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

В процессе обкатки исходного кода на коммерческом проекте появились проблемы при сборке следующего плана:

QAbstractItemView это не чисто абстрактный класс, в нем реализована часть функционала. Более того он наследуется от QAbstractScrollArea, в котором тоже спрятана часть функционала, и т.д. до QObject.

Но самое важное, что еще часть функционала реализована в их спутниках, в приватных классах, функционал которых в библиотеках не торчит наружу.

Это приводит к необходимости собирать свое наследие в составе исходников Qt. И поэтому далее делаем свой QAbstractItemView, чтобы вытащить наружу наше развитие.

Хорошая новость:

class Q_GUI_EXPORT QAbstractScrollAreaPrivate: public QFramePrivate

Q_GUI_EXPORT QAbstractScrollAreaPrivate это означает, что можно слепить свой QAbstractItemView, а вот QAbstractScrollArea уже свой делать НЕ придется(методы его приватного класса как и его самого конечно-же ЭКСПОРТИРУЕМЫЕ).

Кстати далее по цепочке классов родителей так:

class QFramePrivate : public QWidgetPrivate

class Q_GUI_EXPORT QWidgetPrivate : public QObjectPrivate

class Q_CORE_EXPORT QObjectPrivate : public QObjectData

Интересно становится как в Qt5,Qt6 с этим обстоит ситуация...

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

В процессе работы над QpTableView как-то с опозданием открылась старая тема pointSize - pixelSize. Как отрисовывать наш QpTableView в пойнтерах или пикселях? Дело в том, что похоже в paintEvent все расчитывается в пикселях. С другой стороны при открытии приложения фонт по умолчанию имеет значение pointSize 7.5, а pixelSize -1. В общем это отдельная тема для изучения.

Пока нас устраивает вариант, когда мы везде используем метод setFont для управления размерами виджетов и никогда не используем font-size в стилях. Такое у нас правило. Это позволяет динамически менять размеры всем виджетам пропорционально, подстраиваясь под разрешение дисплея.

sectionCount

Исправляем ошибку в отображении количества строк в вертикальном хэдере QVertHeaderView.

А именно не правильное поведение переменной sectionCount после повторного select запроса. Заодно подробнее изучим механизм отрисовки. Текущая версия QpTableView 2.2.0.

Постскриптум: не корректность работы была банально связана с тем, что не надо тупо без знания что-зачем добавлять куда-попало что-попало типа updateGeometry и т.д.).

sectionCount это закрытая приватная переменная для хранения текущего количества строк. По значению sectionCount происходит отрисовка. Но sectionCount не имеет отношения к количеству строк в модели данных (rowCount) и поэтому их надо как-то связать.

А именно синхронизировать sectionCount по значению rowCount, что логично не правда ли?

Смотрим теперь какие сигналы модели, соединяются со слотами модели представления вертикального хэдера:

Q_ASSERT (QObject::connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)),
                                   this, SLOT(sectionsInserted(QModelIndex,int,int))) == true);

Q_ASSERT (QObject::connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
                                   this, SLOT(sectionsAboutToBeRemoved(QModelIndex,int,int))) == true);

Q_ASSERT (QObject::connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
                                   this, SLOT(_q_sectionsRemoved(QModelIndex,int,int)))== true);

Q_ASSERT (QObject::connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)),
                                   this, SLOT(headerDataChanged(Qt::Orientation,int,int)))== true);

Q_ASSERT (QObject::connect(model, SIGNAL(layoutAboutToBeChanged()),
                                   this, SLOT(_q_layoutAboutToBeChanged())) == true);

И именно через эти сигналы и идет управление из модели данных.

Теперь смотрим куда идет управление из модели данных: например при удалении строк. Или как еще похожий вариант может быть: перед select в модели данных надо в модели представления удалить строки (из представления).

QpVertHeaderView::sectionsAboutToBeRemoved( logicalFirst 0  logicalLast 2 ) nothing d->sectionCount 3 
QpTableViewPrivate::_q_updateSpanRemovedRows( first  0 , last  2 ) nothing sectionCount  3 
QpVertHeaderViewPrivate::_q_sectionsRemoved() sectionCount 3 
CHANGED  sectionCount 0 
QpVertHeaderViewPrivate::_q_sectionsRemoved() emit q->sectionCountChanged( 3 , 0 )

Тут примечательно, что сначала всегда обновляется вертикальный хэдер, а потом из него вызывается сигнал emit q->sectionCountChanged для обновления количества строк в QpTableView.

Пора поговорить про таймеры или вызовы  отложенных методов. 

В отрисовке Qt виджетов это просто везде и повсюду. В чем причина? Оказывается например  нельзя отрисовывать виджет из события типа клик по кнопке и т.д. То есть событие отрисовки (paint например) надо помещать в очередь событий или вызывать по другому событию например по событию таймера. Так вот Троли предпочитают  вызывать отрисовку по событию таймера. Таймер срабатывает допустим раз 0.001с.

Далее по коду Троли иногда по ситуации отключают конкретный таймер с целью не допущения отработки конкретного события. Это тоже сплошь и рядом.

В общем у нас в версии 2.3.0 тема таймеров ещё не изучена досконально, мы просто их не трогаем и они работают как в штатном коде.

Риск не знания продолжается, но не смотря на это наш тестовый проект уже прекрасно функционирует.

Так случилось, что на данном этапе мы попробовали собрать наш тестовый проект на Qt6 и Qt5. В первом результате - полное фиаско, так как заголовочные файлы теперь именуются по другому в Qt5. Также теперь qmake не в моде, а используется сторонний cmake. Кстати сам qmake вы могли скомпилировать и собрать сами в Qt, а теперь приходится переучиваться.

Таймеры,таймеры, таймеры

В штатном QAbstractItemView используются несколько таймеров и мы их тоже используем, ничего не ломаем.

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

Взводится таймер update методом setDirtyRegion, в который ещё передается сам регион, который надо отрисовать.

И мы можем использовать setDirtyRegion для управлення отрисовкой.

Нюанс; если вы изменили размеры виджета QpHorHeaderView, QpVertHeaderView, QpTableView (а это самостоятельные независимые виджеты), то надо бы сделать updateGeometry перед update 

Как работает resizeSections

Метод resizeSections  (класса QHeaderView) формирует сигнал sectionResized, который связан со слотом columnResized класса QTableView (для отрисовки таблицы):

connect(d->horizontalHeader,SIGNAL(sectionResized(int,int,int)),
		this, SLOT(columnResized(int,int,int)));

В конце метода resizeSections  есть viewPort->update(), который отрисовывает заново только QHeaderView.

resizeSections   вызывается иногда напрямую в классе QHeaderView, например из методов setSortIndicatorShown,setSortIndicator,viewportEvent,
а иногда по таймеру  через метод doDelayedResizeSections (методы setResizeMode,updateGeometries,dataChanged). 

Далее существует несколько вариантов режимов ресайза (для каждой секции отдельно могут быть):

enum ResizeMode
{
	Interactive,
	Stretch,
	Fixed,
	ResizeToContents,
	Custom = Fixed
};

Interactive - это когда можно изменять мышкой размер секции заголовка.

ResizeToContents - это когда, ширина секции будет равна ширине текста заголовка хэдера.

С отрисовкой QHeaderView почти все понятно. Теперь возвращаемся к слоту columnResized класса QTableView. Он поможет отрисовать табличную часть теперь.

Вс лот columnResized  передается номер секции, которая изменила свой размер. Мы накапливаем такие секции в контейнере QList<int> columnsToUpdate; (класса QTableView).

Далее применяется таймер (columnResizeTimerID) для отложенной отрисовки таблицы. Этот таймер отслеживается в методе QTableView::timerEvent(..). При срабатывании таймера происходит сначала updateGeometries() (таблицы),
потом происходит отрисовка вьюпорта таблицы: d->viewport->update(rect.normalized());, но только для части экрана, который начинается с области первой по порядку (слева направо) от измененной секции.

Пора сказать очевидную вещь - отрисовка таблицы (и хэдеров) всегда происходит по направлению слева направо. То есть достаточно знать первую слева измененную секцию и достаточно отрисовать только ее и то, что справа от нее до конца вьюпорта.

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

Авторесайзинг

Надо сказать, что есть метод void QpTableView::resizeColumnsByContents(). Он устанавливает режим ResizeToContents. То есть имеется ввиду пересчет размера секций по размеру заголовков хэдера, а не колонок таблицы.

Мы реализовали так сказать свой вариант resizeColumnsByContents, который устанавливает ширину каждой секции в соответствии с содержанием заголовка горизонтального хедера.

Также у нас получился свой упрощенный вариант resizeRowsByContents.

moveSection

Решили мы тоже реализовать метод moveSection. Этот метод перемещает секцию в новое место (в нашем шаблоне секций). Делается это интерактивно стандартно: щелкнули мышкой на секции горизонтального хэдера и потащили на другое место.

Неожиданные проблемы в статике

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

Более точно говоря не срабатывает связка сигнал модели данных QAbstractItemModel::insertRow и слот модели представления QAbstractItemView::insertedRow.

Хотя сам инициализирующий connect завершается true (в методе QpAbstractItemView::setModel).

В динамике (shared) все работает нормально. Как же такое может быть?

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

Помним, что у нас QpAbstractItemView это наш самопальный класс.

Итак проблема оказалась как всегда банальной. Мы почему-то решили обертывать некоторые функции возвращающие bool в макрос Q_ASSERT, а он в релизной версии все , что внутри превращает в пустышку, смотрите Q_ASSERT и статика release. Чуда опять не случилось. Надо просто быть внимательнее при использовании макросов.