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


В книжке по QT можно найти вот такой пример управления компоновкой элементов (кнопок например) в окне:

#include <QtWidgets>
int main(int argc, char** argv)
{
    QApplication app(argc, argv);
    app.setApplicationDisplayName("Вложенное размещение");
    QWidget wgt;

    QPushButton* pcmdA = new QPushButton("A");
    QPushButton* pcmdB = new QPushButton("B");
    QPushButton* pcmdC = new QPushButton("C");
    QPushButton* pcmdD = new QPushButton("D");

    QVBoxLayout* pvbxLayout = new QVBoxLayout;
    QHBoxLayout* phbxLayout = new QHBoxLayout;
    phbxLayout->setMargin(5);
    phbxLayout->setSpacing(15);
    //В горизонтальную компоновку
    //добавляем виджеты кнопок pcmdC и pcmdD
    phbxLayout->addWidget(pcmdC);
    phbxLayout->addWidget(pcmdD);

    pvbxLayout->setMargin(5);
    pvbxLayout->setSpacing(15);
    //Виджеты кнопок pcmdA и pcmdB по очереди передаются
    //в метод QLayout::addWidget() вертикальной компоновки pvbxLayout
    pvbxLayout->addWidget(pcmdA);
    pvbxLayout->addWidget(pcmdB);
    //При помощи метода QBoxLayout:: addLayout() 
    //передается объект горизонтальной компоновки phbxLayout
    pvbxLayout->addLayout(phbxLayout);
    //Вызов метода QWidget::setLayout() устанавливает
    //вертикальную компоновку pvbxLayout в виджете wgt
    wgt.setLayout(pvbxLayout);
    wgt.show();
    return app.exec();
}

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

созданное окно
созданное окно

Мы можем видеть что код выглядит очень естественно он фактически отражает процесс рисования объектов на экране.

Если коротко сформулировать что здесь закодировано на человеческом языке

  • Мы создаем окно,

    задаем ему способ вертикальной компоновки,

    добавляем два визуальных объекта (две кнопки),

    и устанавливаем способ горизонтальной компоновки для третьего объекта, который будет составным объектом,

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

Я поясню идею компоновки как я ее понимаю. QT дает возможность задавать размеры и координаты визуальных элементов (виджетов) внутри контейнера в который они помещены, напрямую вызывая соответствующие методы этих виджетов. Но если вы хотите определить некоторый алгоритм вычисления этих координат и размеров в зависимости от количества виджетов (например) помещенных в один контейнер вы должны будете постоянно выполнять вычисления этих координат и размеров при каждом добавлении виджета в контейнер, но эти параметры нужны только во время отрисовки, то есть когда все контейнеры заполнены и все количества известны. Объект компоновки как раз позволяет отложить эти вычисления координат и размеров до момента перед самым рисованием. Перед тем как отрисовать окно и все дочерние виджеты в нем вызываются функции стандартного интерфейса компоновки, который наследуют все объекты компоновки, чтобы посчитать и задать координаты и размеры всех дочерних элементов в окне. Как мы видим из приведенного примера объекты компоновки также допускают вложенность. В нашем примере объект вертикальной компоновки принимает объект горизонтальной компоновки в качестве дочернего.

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

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

Как вы думаете как вместо второй кнопки добавить другую пару визуальных объектов (чек-боксов например) по горизонтали?

мое предположение

надо создать вместо кнопки В еще один объект компоновки и добавить в него чек-боксы и потом добавить этот новый QHBoxLayout как первый объект компоновки перед добавлением того QHBoxLayout который уже есть в коде?

Я пока не проверял, если мнения разойдутся мне придется проверить эту свою версию.

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

1. Главное окно (QWidget wgt)

1.1. -Объект вертикальной компоновки (QVBoxLayout* pvbxLayout)

1.1.1. --Кнопка А

1.1.2. --Кнопка B

1.1.3. --Объект горизонтальной компоновки (QVBoxLayout* pvbxLayout)

1.1.3.1. ---Кнопка С

1.1.3.2. ---Кнопка D

На самом деле такая иерархия не совсем корректна с точки зрения QT так как объекты компоновки не являются наследниками класса QWidget они являются наследниками класса QLayout и поэтому вместо одной иерархии при программировании мы будем иметь дело с двумя горизонтально связанными иерархиями: иерархия виджетов и иерархия QLayout -ов.

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

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


  1. RomeoGolf
    28.01.2025 10:56

    Простите, а что в вашем понимании "шпаргалка"?

    при программировании мы будем иметь дело с двумя горизонтально связанными иерархиями: иерархия виджетов и иерархия QLayout -ов.

    И те, и другие - наследники QObject, стало быть, у них все идентично с точки зрения взаимоотношений "parent-child" и все идентично в плане кто на ком лежит и кто автоуничтожается если уничтожается родитель. Просто в layout`ы добавлены удобные автопересчеты координат и размеров. Так что, в каком смысле мы имеем дело с двумя иерархиями - тоже не понял. Так можно дойти и до горизонтальной иерархии кнопок QPushButton "A", QPushButton "B".

    С толку сбивает множественное наследование?

    - QWidget Class Inherits:QObject and QPaintDevice

    - QLayout Class Inherits:QObject and QLayoutItem

    ну, в QML в этом смысле чуть меньше "параллельности иерархий", там раскладки и кнопки прямо восходят к Item. И все же почему "такая иерархия не совсем корректна"?


    1. rukhi7 Автор
      28.01.2025 10:56

      И все же почему "такая иерархия не совсем корректна"?

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


      1. RomeoGolf
        28.01.2025 10:56

        Обджекту нужен парент обджект, чтобы было понятно, кто на ком лежит и кто за чье существование отвечает. А визуально ориентированные штуки уже опираются на эту информацию, чтобы знать, относительно кого использовать координаты и что с ними делать дальше. Лэйаутам не очень нужны виджетовские фишки, потому что они реально не рисуются. Виджетам не нужны лэйаутовские штуки, потому что они легче относятся к пересчетам координат на лету. Так что работа с координатами у них есть у обоих, но реализована несколько по-разному. Поэтому у них разные вторые корневые предки. Хотя координаты используются и у тех, и у других. Но главное здесь - родитель/дочерний, для этого у них есть общий КуОбджект. Так что реальная иерархия тут только одна, восходящая к обджекту, остальное лишь детали реализации некоторых штук, которые не влияют на взаимоотношения между элементами. Основное взаимоотношение здесь - parent/child, остальное от лукавого.


        1. rukhi7 Автор
          28.01.2025 10:56

          Лэйаутам не очень нужны виджетовские фишки, потому что они реально не рисуются.

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


          1. RomeoGolf
            28.01.2025 10:56

            Вернусь сюда еще. Лэйаут себя не рисует, и никого на себе лежащего не рисует. В коде объявляются разного рода визуальные элементы. В коде этих элементов нет команд для рисования, для вывода в видеопамять через прастиосспади прерывание INT 10h. На основании иерархии, выстроенной на отношениях parent/child, рисуется граф сцены, который будет отрисовываться рантайм-частью библиотеки Qt, без которой ваше творение не запустится

            Попробую обрисовать очень грубо, общий принцип.

            Объявили мы в коде две кнопки. Через что-то типа setWidget() добавили на форму. Тем самым (внутри этой setWidget) сделали форму их родителем, при этом кнопки добавились в список childs этой формы. (ну, допустим, не непосредственно на форму, а через посредничество лэйаута, но это неважно). Добавили еще один лэйаут, он стал чайлд для формы. Добавили на новый лэйаут две кнопки, они стали чайлд для него. Такая иерархия, основанная только на свойствах parent/child из QObject

            Запустили код. Сцена будет отрисовывать этот граф: берем форму, рисуем заданным размером в заданном месте экрана, помним координаты. Берем список чайлдс этой формы (хорошо, корневого лэйаута в нашем случае), там три элемента. Поехали по порядку. Сперва кнопка, ищем правила ее отрисовки. В зависимости от лэйаута это могут быть координаты, берем их из свойств кнопки и рисуем относительно запомненных координат текущего родителя. Либо раскладкой управляет лэйаут, тогда свойства-координаты дочернего элемента игнорируем, а используем правила вычисления координат для дочерних элементов, взятые от текущего родителя (типа прижать влево, поэтому для первого дочернего х=0). Нарисовали саму кнопку - смотрим ее список дочерних элементов. Пусто - хорошо. Не пусто - смотрим, есть ли визуальные. Нарисовали одну кнопку, берем следующую. Координаты перекрываются почему-то? Если z-ордер одинаковый, тупо рисуем поверх. Берем следующий элемент из списка - это еще один лэйаут. Для начала расчитываем его координаты исходя из его родителя (лэйаута и его правил) и заданных уточнений в самом лэйауте, как для предыдущих кнопок. По типу элемента понимаем, что рисовать его мы не будем, но имеем новый комплект координат для привязки и новый список дочерних элементов. Рекурсивно проходим по нему, рисуя все, что по типу должно быть отрисовано и имеет признак visible==true.

            Ну и примерно таким образом обрабатываются движения мыши, фокус элементов и все такое. Обход дерева родителей/детей и выяснение, что делать с текущим элементом в зависимости от его типа и установленных свойств. То, что какой-то элемент не QWidget, а QLayout, не добавляет параллельных иерархий, а уточняет в конкретный момент обхода дерева, что делать с элементом.


            1. rukhi7 Автор
              28.01.2025 10:56

              Поехали по порядку. Сперва кнопка, ищем правила ее отрисовки.

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

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

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

              я вот такие действия имел ввиду:

              pvbxLayout->addLayout(phbxLayout);

              и

              pvbxLayout->addWidget(pcmdA);

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

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


              1. RomeoGolf
                28.01.2025 10:56

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

                Насколько я знаю, нет фиксированного общепринятого или определенного в документации понятия "правило", где это функция, поэтому я не считаю, что я неправильно выразился. Я имею в виду совокупность условий, определяющих то, как параметр будет отрисовываться. Это может быть параметр, например, цвет. Это может быть функция, например, вычисление координаты правого угла из координаты левого угла и ширины. Это может быть набор функций, определяющих, где брать параметры и функции, например, сперва выяснить, кто у нас родитель и надо ли использовать координаты, прописанные в дочернем элементе или взять координаты родителя целиком, если в дочернем сказано "заполнить родителя". Поэтому я использовал расплывчатое слово "правила", если бы можно было сказать "параметр" или "функция", я бы так и сказал. Но я хотел обрисовать ситуацию очень грубо, поверхностно, не вдаваясь в детали. Логика компоновки зависит от класса отображаемых объектов, от свойств, заданных в них, от некоторых функций (методов), объявленных в них, от позиции их в иерархии родительствования.

                Это же разные функции, можно предположить что они формируют разные независимые связи между объектами

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

                И кстати, а что, это -

                 pvbxLayout->addLayout(phbxLayout);

                компилируется? Когда я давненько уже работал с виджетами, так было нельзя...

                Обход дерева кстати очень прост, <...> То есть просто идем по дереву сверху вниз.

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


                1. rukhi7 Автор
                  28.01.2025 10:56

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

                  я просто искал аналогии с WPF. Там определены дерево визуальных обектов и дерево логических объектов:

                  wpf logical vs visual tree

                  Есть даже гибридное дерево:

                  Наследование значений свойств (Property Value Inheritance): Наследование значений свойств осуществляется с помощью гибридного дерева (a hybrid tree). Фактические метаданные, содержащие свойство Inherits, которое позволяет наследовать свойства, - это класс FrameworkPropertyMetadata на уровне платформы WPF FrameworkPropertyMetadata ...

                  для использования значений свойств родительских объектов дочерними объектами в дереве (наследование как использование дочерними).


        1. rukhi7 Автор
          28.01.2025 10:56

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

          кроме работы с координатами и в общем кроме рисования еще надо знать кому события от мыши/стилуса/клавиатуры диспатчить.


          1. RomeoGolf
            28.01.2025 10:56

            Это все идет в рамках отношений родитель/дочерний. Остальное - детали реализации, важные лишь если есть желание сделать это своими руками, обработать каждое событие, каждую координату. Неохота? Так уже все сделали...


      1. RomeoGolf
        28.01.2025 10:56

        И все же, а где обещанная шпаргалка?

        ладно, понял. Вместо хэштега, видимо, для простоты поиска


    1. rukhi7 Автор
      28.01.2025 10:56

      "шпаргалка" это напоминание-подсказка, даже если что-то здесь сформулировано неправильно, одажды решив проблему мы о ней не забудем.


  1. ForestDront
    28.01.2025 10:56

    Лучше QML изучай. Виджеты используют всё меньше.


    1. rukhi7 Автор
      28.01.2025 10:56

      а QML виджетов нет? После того как QML загрузится он на объектах какого типа работает?


      1. RomeoGolf
        28.01.2025 10:56

        С точки зрения CPP - QQuickItem/QObject, с точки зрения QML - Item/QtObject. Но основная суть в том, что надо нарисовать QML-описание интерфейса в декларативном стиле (где допускается императивщина, но стоит постараться ее избегать где только можно), а логика вынесена в плюсы. Если сделать грамотно, то может получиться красивый вариант, где фронтенд так хорошо отделен от логики, что его можно почти безболезненно заменить на веб-морду или переписать на другой GUI-движок с минимальными проблемами.


        1. rukhi7 Автор
          28.01.2025 10:56

          я просто на WPF до этого рисовал вот например

          тут вроде тоже самое, но маленько по другому.


          1. RomeoGolf
            28.01.2025 10:56

            Это действительно очень похоже на wpf, и здесь тоже есть соблазн напихать логики в описание gui, тем более что это очень просто, используя js


            1. rukhi7 Автор
              28.01.2025 10:56

              тем более что это очень просто, используя js

              если код на С++ прикрутить к нему интерпретатор js для меня не реальная задача (или даже просто разбираться как с этим правильно работать), хотя у меня есть проект (из предыдущей статьи) в котором прикручены и питон и java, я не вижу смысла с этой связью разбираться, она енвайронмент-библиотеко зависимая очень, ее перенести куда то не реально, мне кажется.


              1. RomeoGolf
                28.01.2025 10:56

                Не надо ничего прикручивать, все уже украдено до нас. Пишем, например,

                Button { }

                И внутри скобок описываем, что хотим от кнопки - где, какая, признаки. Например, color: "red". Чтобы добавить императивщины, пишем onClicked: { } а в скобках js-код, который выполнится без лишних усилий с вашей стороны, причем, там можно использовать упомянутый color в качестве переменной, а также разные самостоятельно объявленные property.

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

                Кстати, в qml можно объявлять сигналы и привязывать их к слотам в плюсах, а можно qml-функции привязать к плюсовым сигналам. В общем, есть где разгуляться.

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


                1. rukhi7 Автор
                  28.01.2025 10:56

                  такого я не знал:

                  пишем onClicked: { } а в скобках js-код, который выполнится

                  то есть в QML встроен js-интерпретатор, правильно понимаю?

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


                  1. ForestDront
                    28.01.2025 10:56

                    Во времена старинные в таких случаях говорили RTFM


                  1. RomeoGolf
                    28.01.2025 10:56

                    встроен интерпретатор

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

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


                    1. rukhi7 Автор
                      28.01.2025 10:56

                      А что касается документации, пардон, лучшей документации, чем встроенная в QtCreator, я еще не встречал.

                      блин, не знал - не ожидал, я пока только со стороны смотрю, так сказать. Спасибо за информацию, значит надо будет QtCreator.все таки установить, если конечно проект не поменяется, что-то какая-то неопределенность в последнее время наблюдается.


    1. lrdprdx
      28.01.2025 10:56

      Offtopic немного. Хотел в декларативном стиле писать интерфейсы на Lua под LOVE2D, вдохновился qml (ну и существующими библиотечками типа yui). Накатал вот это: . https://github.com/LRDPRDX/LikeliHUD . Сыроварня пока что, но работает.


  1. LaRN
    28.01.2025 10:56

    Весь это код для gui... , по идее интерфейс нужно рисовать в редакторе, а не кодить, так сразу видно результат. Вот на визуале выстрелил delphi, там один из самых удобных редакторов интерыейса. Почему сейчас так не делают.


    1. RomeoGolf
      28.01.2025 10:56

      Во-первых, делают. В Qt тоже есть визуальный редактор интерфейса.

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

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

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

      А для монстров, в которых интерфейс собирается на разного рода view (ListView, GridView и иже с ними) на основе настраиваемых моделей из БД, где может быть меню, определяющее состояние текущей страницы, на которой может быть десяток вкладок, на каждой из которых полсотни контролов либо динамически подгружаемые подстраницы, такие редакторы бесполезны абсолютно. А это, например, целая куча приборов, вроде современных цифровых осциллографов или мед. оборудования.

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


      1. rukhi7 Автор
        28.01.2025 10:56

        Если тем более в проекте работает дизайнер, который продумал и нарисовал хороший интерфейс в какой-нибудь фигме

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


      1. LaRN
        28.01.2025 10:56

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


        1. RomeoGolf
          28.01.2025 10:56

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