Search code examples
reactjsqtdesign-patternspyside2

Qt "passthrough" or "container" widget


In Qt / PySide2, is there such a thing as a Qt widget that simply passes through to a wrapped widget, without adding any extra layers of layout etc.

I'm coming from a web frontend background, so my mental model is of a React container component that adds some behavior but then simply renders a wrapped presentational component.

However, there doesn't seem to be a way of doing this sort of thing in Qt without at least creating a layout in the wrapping widget, even if that layout only contains one widget. I could see that this could lead to multiple layers of redundant layout, which could be inefficient.

I acknowledge that it may be better to not try to replicate React patterns in Qt, so any suggestions of equivalent but more idiomatic patterns would also be welcome.


Solution

  • First I have to ask, what is the point of creating a container widget to just hold one widget, with no extra padding, layouts, or other "overhead?" Why not just show the widget which would be contained?

    Second, nothing says you must have a QLayout inside a QWidget. The layout simply moves any contained widgets around using QWidget::setGeometry() (or similar) on the child widget(s). It's trivial to implement a QWidget which sizes a child widget to match its own size, though it's fairly pointless because that's what QLayout is for. But I have included such an example below (C++, sorry)

    A top-level QLayout set on a QWidget has default content margins (padding around the contained widget(s)). This can easily be removed with QLayout::setContentMargins(0, 0, 0, 0) (as mentioned in a previous comment).

    "No-layout" "passthrough" QWidget:

    #include <QWidget>
    class PassthroughWidget : public QWidget
    {
      Q_OBJECT
      public:
        PassthroughWidget(QWidget *child, QWidget *parent = nullptr) :
          QWidget(parent),
          m_child(child)
        {
          if (m_child)
            m_child->setParent(this);  // assume ownership
        }
    
      protected:
        void resizeEvent(QResizeEvent *e) override
        {
          QWidget::resizeEvent(e);
          if (m_child)
            m_child->setGeometry(contentsRect());  // match child widget to content area
        }
    
        QWidget *m_child;  // Actually I'd make it a QPointer<QWidget> but that's another matter.
    }
    

    ADDED: To expand on my comments regarding being a widget vs. having (or managing) widget(s).

    I just happen to be working on a utility app which makes use of both paradigms for a couple of parts. I'm not going to include all the code, but hopefully enough to get the point across. See screenshot below for how they're used. (The app is for testing some painting and transform code I'm doing, quite similar to (and started life as) the Transformations Example in Qt docs.)

    What the code parts below actually do isn't important, the point is how they're implemented, again specifically meant to illustrate the different approaches to a "controller" for visual elements.

    First example is of something being a widget, that is, inheriting from QWidget (or QFrame in this case) and using other widgets to present a "unified" UI and API. This is an editor for two double values, like for a size width/height or coordinate x/y value. The two values can be linked so changing one will also change the other to match.

    class ValuePairEditor : public QFrame
    {
        Q_OBJECT
      public:
        typedef QPair<qreal, qreal> ValuePair;
    
        explicit ValuePairEditor(QWidget *p = nullptr) :
          QFrame(p)
        {
          setFrameStyle(QFrame::NoFrame | QFrame::Plain);
          QHBoxLayout *lo = new QHBoxLayout(this);
          lo->setContentsMargins(0,0,0,0);
          lo->setSpacing(2);
    
          valueSb[0] = new QDoubleSpinBox(this);
          ...
          connect(valueSb[0], QOverload<double>::of(&QDoubleSpinBox::valueChanged), 
            this, &ValuePairEditor::onValueChanged);
          // ... also set up the 2nd spin box for valueSb[1]
    
          linkBtn = new QToolButton(this);
          linkBtn->setCheckable(true);
          ....
          lo->addWidget(valueSb[0], 1);
          lo->addWidget(linkBtn);
          lo->addWidget(valueSb[1], 1);
        }
    
        inline ValuePair value() const 
          { return { valueSb[0]->value(), valueSb[1]->value() }; }
    
      public slots:
        inline void setValue(qreal value1, qreal value2) const
        {
          for (int i=0; i < 2; ++i) {
            QSignalBlocker blocker(valueSb[i]);
            valueSb[i]->setValue(!i ? value1 : value2);
          }
          emit valueChanged(valueSb[0]->value(), valueSb[1]->value());
        }
    
        inline void setValue(const ValuePair &value) const 
          { setValue(value.first, value.second); }
    
      signals:
        void valueChanged(qreal value1, qreal value2) const;
    
      private slots:
        void onValueChanged(double val) const {
          ...
          emit valueChanged(valueSb[0]->value(), valueSb[1]->value());
        }
    
      private:
        QDoubleSpinBox *valueSb[2];
        QToolButton *linkBtn;
    };
    

    Now for the other example, using a "controller" QObject which manages a set of widgets, but doesn't itself display anything. The widgets are available to the managing application to place as needed, while the controller provides a unified API for interacting with the widgets & data. Controllers can be created or destroyed as needed.

    This example manages a QWidget which is a "render area" for doing some custom painting, and a "settings" QWidget which changes properties in the render area. The settings widget has further sub-widgets, but these are not directly exposed to the controlling application. In fact it also makes use of ValuePairEditor from above.

    class RenderSet : public QObject
    {
      Q_OBJECT
      public:
        RenderSet(QObject *p = nullptr) : 
          QObject(p),
          area(new RenderArea()),
          options(new QWidget())
        {
          // "private" widgets
          typeCb = new QComboBox(options);
          txParamEdit = new ValuePairEditor(options);
          ...
          QHBoxLayout *ctrLo = new QHBoxLayout(options);
          ctrLo->setContentsMargins(0,0,0,0);
          ctrLo->addWidget(typeCb, 2);
          ctrLo->addWidget(txParamEdit, 1);
          ctrLo->addLayout(btnLo);
    
          connect(txParamEdit, SIGNAL(valueChanged(qreal,qreal)), this, SIGNAL(txChanged()));
        }
    
        ~RenderSet() override
        {
          if (options)
            options->deleteLater();
          if (area)
            area->deleteLater();
        }
    
        inline RenderArea *renderArea() const { return area.data(); }
        inline QWidget *optionsWidget() const { return options.data(); }
    
        inline Operation txOperation() const 
          { return Operation({txType(), txParams()}); }
        inline TxType txType() const 
          { return (typeCb ? TxType(typeCb->currentData().toInt()) : NoTransform); }
        inline QPointF txParams() const 
          { return txParamEdit ? txParamEdit->valueAsPoint() : QPointF(); }
    
      public slots:
        void updateRender(const QSize &bounds, const QPainterPath &path) const {
          if (area)
            ...
        }
    
        void updateOperations(QList<Operation> &operations) const {
          operations.append(txOperation());
          if (area)
            ...
        }
    
      signals:
        void txChanged() const;
    
      private:
        QPointer<RenderArea> area;
        QPointer<QWidget> options;
        QPointer<QComboBox> typeCb;
        QPointer<ValuePairEditor> txParamEdit;
    };
    

    enter image description here