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

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

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

Тепер к делу: надо создать шаблон расположения секций. Это по сути как шахматная доска, только теперь у строки может быть 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 - буду безмерно признателен...

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

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

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

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

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

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

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

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

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

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

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

Комментарии (37)


  1. unC0Rr
    25.10.2023 20:11
    +2

    Не понял этот момент:

    QRect(0,0 101x101) и вроде бы ширина (и высота) равны 101. Но самом деле это означает реально ширину (или высоту) 100px.
    А 101 это количество пикселей, то есть количество от 0 до 100 , и это равно 101 штуке.

    Для простоты рассмотрим QRect(0,0 1x1). Согласно тексту, 1 это количество пикселей от 0 до 0, равно 1. А реальная ширина 0px. Что понимается под шириной, если количество пикселей 1 соответствует ширине 0?


    1. kkmspb Автор
      25.10.2023 20:11

      QRect(0,0 1x1)

      Ширина 0, можно конечно проверить через лупу, но мне лень


    1. frontedward
      25.10.2023 20:11

      Не знаю как вам, но мне ещё ни разу не удалось разглядеть ячейку шириной и высотой в 1рх)


    1. puzon4eg
      25.10.2023 20:11

      Согласно документации, ширина вычисляется как (х2-х1)-1


  1. Sazonov
    25.10.2023 20:11
    +1

    Да уж, Qt4… Попробуйте qml table view из набора Qt6 - удивитесь, насколько прощё всё это можно сделать :)


    1. kkmspb Автор
      25.10.2023 20:11

      Попробуйте qml table view из набора Qt6

      Если не трудно можете продемонстрировать? только без span (это другое)


      1. Sazonov
        25.10.2023 20:11

        Не совсем понял, что именно вы хотите увидеть. Посмотрите примеры интерфейсов на qml и почитайте документацию по qml table view + в целом про qml.


        1. kkmspb Автор
          25.10.2023 20:11

          Не совсем понял, что именно вы хотите увидеть.

          Я про вывод колонок (модели данных) в строке таблицы по шаблону (многорядно).

          Насколько я знаю qml это скриптовая оболочка, которая использует Q_PROPERTY классов С++. И если в классах не реализован какой-то функционал, то откуда в Qml он появится.


          1. Sazonov
            25.10.2023 20:11

            Qml это не скриптовая оболочка, это полноценный язык разметки, совместимый с js и инфраструктурой qt (проперти, сигналы/слоты, биндинги). Это уже больше чем виджеты. Плюс аппаратный рендеринг.

            Если не реализован какой-то функционал, то он просто пишется - в этом и заключается работа программиста. Просто всё что связано с отображением данных на порядок проще делать средствами qml, чем упарываться с переопределением paintEvent и ручным рисованием. А для хранения/модификации данных - старые добрые QAbstractIemModel и их наследники.

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


            1. kkmspb Автор
              25.10.2023 20:11

              Просто всё что связано с отображением данных на порядок проще делать средствами qml

              Это кому как интереснее. Когда вы не сможете понять почему на Qml что-то не так как вам надо отрисовывается вы полезете отладчиком в исходники или смиритесь и оставите все как есть.


              1. Sazonov
                25.10.2023 20:11

                Точно так же, как и с виджетами


  1. Xambey97
    25.10.2023 20:11
    +1

    Спасибо за статью. Ностальжи. Помню начинал с ним работать, когда релизной версией был 4.6 вроде, работало оно более менее, но как вышел QT 5... Матерь божья, они абсолютно забили на поддержку документации (особенно по части обратно совместимости) и половина их примеров или не работали вовсе (из-за кривой работы компонентов или неправильно их использования), или уже давно имели множество альтернативных решений через другие компоненты. Как там с этим дела сейчас? Если я скажем захотел бы мобильное приложение написать на нем? И особенно интересно, как там сейчас дела со встроенным браузером? Помнится он мега криво работал, в плоть до 5.3 где я прекратил работу на с++ вовсе. Были ж времена бгг. Удивительно, как оно продолжает жить, когда на рынке есть много кроссплатформенных решений на разных яп'ах. Людей, что умеют 'приготовить' QT как надо, чтобы он был высокопроизводительным приложением без крит багов, единицы, как по мне (телеграм тому пример), надо им памятник ставить. Почему-то я не удивлен, что таблицы не изменились :)


    1. kkmspb Автор
      25.10.2023 20:11

      Для меня история Qt представляется на сегодня таким образом: два норвежских студента за лет 5-7 написали 500-700 классов, а потом команда из нескольких тысяч сотрудников современного Qt не смогла даже понять, что там написано и занимается только продажей старого функционала, обзывая его Qt5,6,7,8.. Вопрос почему?


      1. lexa
        25.10.2023 20:11
        +3

        Там есть огромный хвост совместимости.
        То есть код для Qt4 почти без правок соберется для Qt5 (с 6 не пробовал пока, хотя уже пора) и внезапно получит Opengl accelerated отрисовку.

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


        1. kkmspb Автор
          25.10.2023 20:11

          Они огромную работу сделали и с Qt6 (QRhi)

          Можете немного про огромную работу поведать? Я не тролю, я действительно практически хотел бы понять чего я потерял не используя Qt5,6 ,например в десктопном приложении. У меня используется SQlite база, интерфейс для пользователя, соединение с ЛК в интернете.


          1. lexa
            25.10.2023 20:11

            Если вас 4-ка устраивает, то не вижу особых проблем, работает и ладно.
            По идее, Qt4 приложение не будет работать с Drag-n-Drop в новых macOS (старые API формально остались, не уверен про macOS 14, но они поломаные), но опять же не всем надо.

            Qt5 мы используем, потому что они дали на Windows абстрацию OpenGL поверх OpenGL или DirectX (через ANGLE), что позволяет иметь один шейдерный код для трех вариантов HW Acceleration (OpenGL, DirectX11, DirectX9).

            В Qt5 работают пальцевые жесты (на трекпаде или на тач-мониторе), ну не так чтобы вполне полностью, но можно использовать уже.

            Qt6 должен дать одну шейдерную базу поверх DX11/12, OpenGL, Metal, Vulkan. Ждем Qt 6.7 с обещаной QRhiWidget. Наверное к 6.9 можно будет в продакшен потихоньку выпускать.

            Это я все про Widgets-based приложения. QML нам не зашло - слишком много надо переписывать (кодовой базе 10+ лет)


            1. kkmspb Автор
              25.10.2023 20:11
              -1

              Если вас 4-ка устраивает, то не вижу особых проблем, работает и ладно

              Спасибо за ликбез. Действительно OpenGL, DirectX11, DirectX9 ничего такого пока не надо было. Графику svg Qt 4 умеет. HiDPI даже пока не понимаю зачем.


              1. lexa
                25.10.2023 20:11

                HiDPI-aware приложения на соответствующих мониторах просто хорошо выглядят.
                Более низачем.


          1. lexa
            25.10.2023 20:11

            В Qt4 нет поддержки HiDPI (кажется даже поддержки маковской ретины нет, но тут могу ошибиться).

            Что, конечно, не очень важно для приложений без графики, но даже иконки на тулбарах будут выгядеть so 199x


          1. Sazonov
            25.10.2023 20:11

            Вы лучше скажите, чем по вашему Qt4 лучше чем Qt6? А то какие-то странные рассуждения получаются.


            1. kkmspb Автор
              25.10.2023 20:11

              Вы лучше скажите, чем по вашему Qt4 лучше чем Qt6?

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

              И уже подозреваю на 99%, что в Qt6 ничего относительно QTableView не изменилось


              1. Sazonov
                25.10.2023 20:11

                Не изменилось потому что виджеты - зрелая технология для своего времени: отрисовка только на CPU (привет тормоза на 4к мониторах), отсутствие анимаций, отсутствие HiDPI.

                Тут нужен был другой подход, и qml - как раз он. Я еще в 2014 общался с ребятами, которые делают qml. Там тоже был тернистый путь и менялись цели. Но про виджеты уже тогда сказали, что там ничего менять не нужно.

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


                1. kkmspb Автор
                  25.10.2023 20:11

                  Я много-го не прошу сделайте, если знаете, демку с таким функционалом. Если не знаете не продолжайте.


        1. kkmspb Автор
          25.10.2023 20:11

          проблема в том что старый код (пользователей библиотеки) нельзя прям уж сильно ломать

          Если они в приватных классах что-то меняют, то пользователю библиотеки это фиолетово.


    1. kkmspb Автор
      25.10.2023 20:11

      Если я скажем захотел бы мобильное приложение написать на нем?

      К сожалению не готов сейчас портировать код под андроид или iOs (пока только под Виндой работаю).

      Тему правда вентилировал для андроид. Но там нет возможности собирать проект прямо на андроид (как к примеру под Windows или Linux ), а так бы проблем бы вообще никаких не было.


    1. kkmspb Автор
      25.10.2023 20:11

      И особенно интересно, как там сейчас дела со встроенным браузером?

      Тут к сожалению не знаю от слова совсем. Смотря какой функционал браузера вам нужен. Современные браузеры сами знаете что за монстры.


    1. Vlafy2
      25.10.2023 20:11
      +1

      Интересно, что же это за "много кроссплатформенных решений на разных яп'ах"? Кроме богомерзкой явы ничего и не вспоминается.


      1. kkmspb Автор
        25.10.2023 20:11

        Присоединяюсь: я на С++ знаю только фреймворк Qt, ещё вроде wxWidgets есть. На С GTK и все...

        Между чем выбирать то? Чем занимаются люди в институтах?


        1. ss-nopol
          25.10.2023 20:11

          Есть ещё gtkmm - C++ обёртка для GTK


      1. osmanpasha
        25.10.2023 20:11
        -1

        Есть ещё AvaloniaUI на C#, есть куча билбиотек на питоне, начиная со встроенного tk. Но нативно ничего не выглядит, везде какие-то косяки вылезают во внешнем виде или поведении. Собственно, по-моему кроме Qt ничего не выглядит достаточно нативно на десктопных платформах. С мобильными платформами ситуация по-лучше конечно.

        (Есть ещё Delphi/Lazarus, которые выглядят хорошо, но в наши дни это уже эзотерика какая-то)


        1. kkmspb Автор
          25.10.2023 20:11
          -1

          AvaloniaUI на C#, есть куча библиотек на питоне

          Давайте ограничим список языком С++, открытыми исходниками и инструментом компилятор/линковщик. То что вы приводите уже интерпретаторы и вы становитесь заложником какой-то фирмы - будет она делать свою библиотеку кроссплатформенно? Ну например Microsoft? Уже звучит глупо.
          Питон конечно кроссплатформенность будет поддерживать, но как и у любого интерпретатора если что-то заглючило, то ты заложник их ошибок. То есть тебя лишают свободы (образно говоря).
          Кстати Delphi/Lazarus наверное хороший пример, там отрисовка нативная (насколько я помню), но begin/end просто невыносимо.


          1. osmanpasha
            25.10.2023 20:11

            Ну например Microsoft? Уже звучит глупо.

            Я не очень в теме, но по-моему Microsoft как раз заопенсорсила .NET настолько, что это сделало возможным кроссплатформенный (и опенсорсный) AvaloniaUI (который работает как на Mono, так и на .Net Core).

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

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

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


            1. kkmspb Автор
              25.10.2023 20:11

              С той же вероятностью вы заложник багов компилятора или реализации стандартной бибилотеки в компилируемых языках

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

              Подозреваю, что сам питон пишется на С++ , компилируется и линкуется под разные платформы.


  1. asaks
    25.10.2023 20:11

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


    1. kkmspb Автор
      25.10.2023 20:11
      -1

      asaks54 минуты назад

      "А почему при изменении высоты одной строки, меняются высоты всех остальных строк в таблице?"

      На мой взгляд так "правильнее" и главное для реализации проще. К тому же человеческий глаз очень положительно реагирует на симметрию.

      Мы сразу чувствуем единообразие и у нас возникает доверие, что тут все упорядочено и проработано.

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

      Хотя реализовать дополнительно такой вариант можно, но придется где-то хранить высоты строк, а если строк тысяча или более?...


  1. SerJook
    25.10.2023 20:11
    +2

    переопределяем метод painEvent класса QTableView

    Забавная опечатка, наверное, вы этим хотели выразить ту боль, которую вы испытали, пока получили рабочее решение :)


    1. kkmspb Автор
      25.10.2023 20:11

      нет просто забыл как буква пишется