0

The output generated by uic from a .ui files always installs the layouts and child widgets on an existing widget. This works great when you intend to add the layout/children directly on a widget:

#include "ui_form.h"

QWidget widget;
Ui::Form ui;
ui.setupUi(&widget);

Alas, sometimes the intervening widget is unnecessary. First of all, layouts on widgets have non-zero margins, so if you intend to insert the contents of Ui::Form into a layout, you'll have to reset the margins:

QWidget top;
QVBoxLayout layout{&top};
QWidget widget;
Ui::Form ui;
ui.setupUi(&widget);
widget.layout()->setContentsMargins(0,0,0,0);
layout.addWidget(&widget);

Furthermore, in this case the intervening widget is completely unnecessary. Suppose we had setupUi that didn't need a top-level widget (as of Qt 5.7, it crashes when passed a nullptr). We could use it as follows:

QWidget top;
QVBoxLayout layout{&top};
Ui::Form ui;
ui.setupUi(nullptr);
layout.addLayout(ui.layout); // assuming `layout` is the top-level layout there

I've inspected uic and it doesn't support emitting code without a top-level widget. It's true that the needed patch would be minuscule.

Yet, is there any way to implement something to the effect of nullptr-accepting setupUi without patching Qt?

Lest someone think this is a made-up problem: This question was prompted in part by this code from YUView. There, the parentWidget passed to setupUi already has a layout, the purpose being to add the form's layout to another layout after setupUi is done. QWidget::setLayout invoked from inside of setupUi refuses to set the new layout, emits a warning, and things just happen to work in spite of it. The code is brittle since it depends on Qt code breaking just the right way to lead to the desired result.

Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • This is great. Sorry if this sounds stupid, but are there any downsides for using an intervening widget with `setContentsMargins(0,0,0,0);` other than the fact that the widget is not necessary? If this is about the overhead of creating the intervening widget, then the answer essentially creates a temporary widget and takes its layout? So, I guess it is not about performance. So, what are the downsides of setting this in an intervening widget (with margins set to `0`) and adding this widget to the layout instead? – Mike Nov 11 '16 at 10:15
  • I am asking this because I usually use the intervening widget approach when dealing with such problems. – Mike Nov 11 '16 at 10:17
  • I like the hierarchy of the items not to include unnecessary junk. The temporary widget can be kept around forever and reused. About the only extra cost one pays is the temporary wrapper layout that gets constructed and then promptly destructed. It might be much ado about nothing, oh well. – Kuba hasn't forgotten Monica Nov 11 '16 at 14:06
  • I didn't mean to say that there is an extra overhead in your approach to the problem. I was just trying to guess downsides for using the intervening widget approach. It is cleaner when you don't have junk in your hierarchy, but I thought there was another problem that I didn't get. thanks. – Mike Nov 11 '16 at 17:51

1 Answers1

1

Yes. The key observations that enable us to do so are:

  1. A childLayout can be removed from its parent layout by nulling its parent:

    childLayout->setParent(nullptr);
    
  2. A parentless layout will, upon adding it to a widget or a layout with non-null parentWidget(), reparent its children.

Thus, we need to add an additional top-level wrapper layout to the form, then unparent the target top level layout from it, and delete the wrapper. The form's structure as reported by dumpObjectTree is:

QWidget::Form 
    QVBoxLayout::wrapper 
        QVBoxLayout::layout 
            QHBoxLayout::horizontalLayout 
    QLabel::label 
    QLabel::label_2 
    QLabel::label_3

The test case:

// https://github.com/KubaO/stackoverflown/tree/master/questions/layout-take-40497358
#include <QtWidgets>
#include "ui_form.h"

QLayout * takeLayout(QWidget *);

int main(int argc, char ** argv)
{
   QApplication app{argc, argv};

   QWidget parent;
   QHBoxLayout layout{&parent};
   Ui::Form ui;
   {
      QWidget w;
      ui.setupUi(&w);
      w.dumpObjectTree();
      if (true) {
         ui.layout->setParent(nullptr);
         delete ui.wrapper;
         layout.addLayout(ui.layout);
      } else {
         layout.addLayout(takeLayout(&w));
      }
   }
   parent.show();
   return app.exec();
}

If you wished to factor this functionality out, you'd have a template setupLayout function that you'd use in place of setupUi. The testcase above would become:

   QWidget parent;
   QHBoxLayout layout{&parent};
   Ui::Form ui;
   layout.addLayout(setupLayout(&ui));
   parent.show();

The setupLayout is:

//*** Interface

QLayout * setupLayout_impl(void * ui, void(*setupUi)(void * ui, QWidget * widget));

// Extracts the top layout from a Ui class generated by uic. The top layout must
// be enclosed in a wrapper layout.
template <typename Ui> QLayout * setupLayout(Ui * ui) {
  struct Helper {
    static void setupUi(void * ui, QWidget * widget) {
      reinterpret_cast<Ui*>(ui)->setupUi(widget);
    }
  };
  return setupLayout_impl(static_cast<void*>(ui), &Helper::setupUi);
}

//*** Implementation

static void unparentWidgets(QLayout * layout) {
  const int n = layout->count();
  for (int i = 0; i < n; ++i) {
    QLayoutItem * item = layout->itemAt(i);
    if (item->widget()) item->widget()->setParent(0);
    else if (item->layout()) unparentWidgets(item->layout());
  }
}

QLayout * setupLayout_impl(void * ui, void(*setupUi)(void * ui, QWidget * widget))
{
  QWidget widget;
  setupUi(ui, &widget);
  QLayout * wrapperLayout = widget.layout();
  Q_ASSERT(wrapperLayout);
  QObjectList const wrapperChildren = wrapperLayout->children();
  Q_ASSERT(wrapperChildren.size() == 1);
  QLayout * topLayout = qobject_cast<QLayout*>(wrapperChildren.first());
  Q_ASSERT(topLayout);
  topLayout->setParent(0);
  delete wrapperLayout;
  unparentWidgets(topLayout);
  Q_ASSERT(widget.findChildren<QObject*>().isEmpty());
  return topLayout;
}

What if you can't or are unwilling to alter the form's structure? You can use Qt's internals to implement a takeLayout function that rips out a widget's layout and returns it, unattached to the widget. Note that the private QWidget::takeLayout is insufficient, as it returns a layout with its topLevel flag set: such layouts can only be installed directly on widgets, and will fail an assertion when one attempts to add them to a layout (as a sub-layout). Here's how:

#include <private/qwidget_p.h>
#include <private/qlayout_p.h>

class WidgetHelper : private QWidget {
   struct LayoutHelper : private QLayout {
      static void resetTopLevel(QLayout * l) {
         auto d = static_cast<QLayoutPrivate*>(static_cast<LayoutHelper*>(l)->d_ptr.data());
         d->topLevel = false;
      }
   };
public:
   static QLayout * takeLayout(QWidget * w) {
      auto d = static_cast<QWidgetPrivate*>(static_cast<WidgetHelper*>(w)->d_ptr.data());
      auto l = w->layout();
      if (!l) return nullptr;
      d->layout = 0;
      l->setParent(nullptr);
      LayoutHelper::resetTopLevel(l);
      return l;
   }
};
QLayout * takeLayout(QWidget * w) { return WidgetHelper::takeLayout(w); }

The form.ui looks as follows:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Form</class>
 <widget class="QWidget" name="Form">
  <layout class="QVBoxLayout" name="wrapper">
   <item>
    <layout class="QVBoxLayout" name="layout">
     <item>
      <widget class="QLabel" name="label">
       <property name="text">
        <string>Top</string>
       </property>
      </widget>
     </item>
     <item>
      <layout class="QHBoxLayout" name="horizontalLayout">
       <item>
        <widget class="QLabel" name="label_2">
         <property name="text">
          <string>Left</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QLabel" name="label_3">
         <property name="text">
          <string>Right</string>
         </property>
        </widget>
       </item>
      </layout>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
</ui>
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313