Qt "passthrough" 或 "container" 小部件

Qt "passthrough" or "container" widget

在 Qt/PySide2 中,是否有 Qt 小部件这样的东西,它只是简单地传递给包装的小部件,而不添加任何额外的布局层等

我来自 Web 前端背景,所以我的心智模型是 React container component,它添加了一些行为,但随后只渲染了一个包装的表示组件。

但是,在 Qt 中似乎没有一种方法可以在不至少在包装小部件中创建布局的情况下执行此类操作,即使该布局仅包含一个小部件。我可以看到这可能会导致多层冗余布局,这可能是低效的。

我承认最好不要尝试在 Qt 中复制 React 模式,因此也欢迎任何等效但更惯用模式的建议。

在 Qt 中有两种管理小部件的方法:通过布局或通过父级。您是否尝试过使用 'parent' 方法?

Docs says:

...The base class of everything that appears on the screen, extends the parent-child relationship. A child normally also becomes a child widget, i.e. it is displayed in its parent's coordinate system and is graphically clipped by its parent's boundaries.

因此,基本上,如果您使用 setParent 来包含小部件,则无需创建布局。

首先我要问,创建一个只容纳一个小部件的容器小部件有什么意义,没有额外的填充、布局或其他 "overhead?" 为什么不只显示将包含的小部件?

其次,没有人说你必须在 QWidget 中有一个 QLayout。布局只是简单地在子部件上使用 QWidget::setGeometry()(或类似的)移动任何包含的部件。实现一个 QWidget 来调整子窗口小部件的大小以匹配其自身的大小是微不足道的,尽管它相当没有意义,因为这就是 QLayout 的目的。但是我在下面包含了这样一个例子(C++,抱歉)

QWidget 上设置的顶级 QLayout 具有默认内容边距(在包含的小部件周围填充)。这可以很容易地用 QLayout::setContentMargins(0, 0, 0, 0) 删除(如前一条评论中所述)。

"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.
}

添加:扩展我关于成为小部件与拥有的评论(或管理)小部件。

我刚好正在开发一个实用程序,它在几个部分使用了这两种范例。我不打算包括所有代码,但希望足以让大家明白这一点。请参阅下面的屏幕截图了解它们的使用方式。 (该应用程序用于测试我正在做的一些绘画和转换代码,与 Qt 文档中的 Transformations Example 非常相似(并且开始使用)。)

下面的代码部分实际上做了什么并不重要,重点是如何它们是如何实现的,再次明确表示说明视觉元素 "controller" 的不同方法。

第一个例子是一个小部件,也就是说,继承自QWidget(在本例中为QFrame)并使用其他小部件来呈现 "unified" UI 和 API。这是一个用于两个 double 值的编辑器,例如大小 width/height 或坐标 x/y 值。这两个值可以关联,因此更改一个值也会更改另一个值以匹配。

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;
};

现在看另一个例子,使用 "controller" QObject 来管理一组小部件,但它本身不显示任何东西。小部件可供管理应用程序根据需要放置,而控制器提供统一的 API 用于与小部件和数据交互。可以根据需要创建或销毁控制器。

此示例管理一个 QWidget,它是一个 "render area" 用于进行一些自定义绘画,以及一个 "settings" QWidget 用于更改渲染区域中的属性。设置小部件有更多的子小部件,但这些不直接暴露给控制应用程序。事实上,它还利用了上面的 ValuePairEditor

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;
};

我以类似的方式使用了 QFrame,所有属性都设置为 0,Shape 设置为 QFrame::NoFrame。它作为最终完成繁重工作的实际小部件的哑容器非常有用,QStackedWidget 是其中的一个用户。引自文档:

The QFrame class can also be used directly for creating simple placeholder frames without any contents.

但是,我不确定它是否 100% 符合您的要求,因为我不熟悉您概述的 React 方法。也不确定在不使用布局的情况下可以合理地走多远。