Qt“直通”或“容器”小部件

Posted

技术标签:

【中文标题】Qt“直通”或“容器”小部件【英文标题】:Qt "passthrough" or "container" widget 【发布时间】:2019-10-23 10:18:50 【问题描述】:

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

我来自网络前端背景,所以我的心智模型是一个 React container component,它添加了一些行为,然后只是呈现一个包装好的演示组件。

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

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

【问题讨论】:

听起来像是过早优化的明显案例。您有什么实际证据表明添加布局效率低下?如果您没有编写任何代码,那么担心这样的实现细节是没有意义的。 这不仅仅是优化。我发现添加额外的布局层似乎也会增加小部件内的填充量(尽管可能对此进行了样式修复)。 这根本不是优化。 contents margins 可以很容易地调整。不管你喜不喜欢,布局是 Qt 的基本组成部分,所以我建议你学习如何正确使用它们。如果您有不知道如何解决的真正问题,请提供minimal reproducible example。 为我的答案添加了 2 个示例方法,以扩展之前的 cmets。 【参考方案1】:

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

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

QWidget 上设置的***QLayout 具有默认内容边距(包含的小部件周围的填充)。这可以通过QLayout::setContentMargins(0, 0, 0, 0) 轻松删除(如之前的评论中所述)。

“无布局”“直通”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.


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

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

什么下面的代码部分实际上做了什么并不重要,重点是如何它们是如何实现的,再次专门用于说明“控制器”的不同方法" 用于视觉元素。

第一个示例是成为小部件,即从QWidget(或本例中的QFrame)继承并使用其他小部件来呈现“统一”的 UI 和 API。这是两个double 值的编辑器,例如大小宽度/高度或坐标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;
;

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

本示例管理一个QWidget,它是一个“渲染区域”,用于进行一些自定义绘画,以及一个“设置”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;
;

【讨论】:

“创建一个容器小部件来只容纳一个小部件有什么意义”:所以,我正在尝试在反应中重新创建一种模式,您可以在其中包装“组件”(即小部件)向包装的组件添加行为,即使包含的组件也是唯一实际呈现的东西。在 React 中,这种区别在于“容器”和“展示”组件之间。我想,演示组件类似于“被动视图”。显然,React 模式在这种情况下可能不合适,但在那种情况下,我希望得到一些关于适当的等效 Qt 习惯用法的提示。 @samfrances 是的,抱歉,我没有读过那篇作者不再推荐的 React 文章。 :) 我仍然不确定我是否明白这一点。如果“容器”实际上没有显示任何内容,那么它可能不应该 QWidget。它可能拥有一个小部件,或者它可以被分配一个小部件来管理,但它没有意义成为一个小部件,除非它要添加设计元素。如果它确实需要绘制/绘制有助于“演示”部分的东西(比如 QFrame 能够添加视觉边框),那么容器成为一个小部件是有意义的。 Qt 中最接近的等效概念是模型(数据容器)和视图(展示小部件)之间的区别 - 请参阅 Qt 文档文章 Model/View Programming。小部件和布局之间的关系是完全不同的。布局是纯粹的表示组件,它管理小部件的相对几何形状。它们不包含数据或提供任何附加功能 - 请参阅 Qt 文档文章 Layout Management。 Qt 没有可用于所有小部件的通用容器组件。相反,它更喜欢使用单继承向基类添加额外的功能。例如,QPushButton、QCheckBox、QRadioButton 等都继承自 QAbstractButton。它还大量使用虚拟和受保护方法,这些方法可以在内置 Qt 类的用户定义子类中被覆盖或重新实现 - 请参阅 Qt 文档文章 Designing the User Interface。 QWidget Qt C++ 中用于视觉呈现的“通用容器”(图形框架除外,但即便如此也需要QWidget“容器”来实际显示某些东西)。正如所有其他小部件都继承自它的事实所证明的那样。如果这还不够通用,我不知道是什么。【参考方案2】:

在 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 来包含小部件,则无需创建任何布局。

【讨论】:

这似乎不起作用。包装的小部件根本不显示。【参考方案3】:

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

QFrame 类也可以直接用于创建没有任何内容的简单占位符框架。

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

【讨论】:

不是没有实际框架的QFrame,然后只是具有无用属性的普通QWidget?它仍然需要一个布局或某种方式来管理任何孩子。 没错,你不能回避使用任何可用/可扩展的布局。虽然如果你真的想在一个框架(OP 术语中的容器)和实际的小部件(Presentational)之间进行某种逻辑分离,这就是我会使用的。但是,可以说,通过更适合框架和 C++ 世界的不同类型的设计范式来更好地实现这一点。 QFrame类继承了QWidget,所以它们之间没有逻辑上的分离。它们都旨在以纯粹的呈现方式“包含”其他小部件。它们根本不是容器组件(在 React 意义上),也不是任何布局类(它们也纯粹是展示性的)。在 React 中,容器组件仅包含数据、状态和其他抽象功能——在 Qt 术语中,它们有点像模型(而不是视图)。

以上是关于Qt“直通”或“容器”小部件的主要内容,如果未能解决你的问题,请参考以下文章

可枚举的 Qt 小部件

Qt 在另一个小部件中调整小部件的大小

Qt半透明背景导致子小部件在父小部件中“印记”

GTK 或 Qt 的图表小部件 [关闭]

具有重叠子小部件的 Qt 自定义小部件

如何检测 qt 标签(或任何小部件)是不是具有所需的所有空间?