21

What I am trying to do is render a qwidget onto a different window (manually using a QPainter)

I have a QWidget (w) with a layout and a bunch of child controls. w is hidden. Until w is shown, there is no layout calculations happening, which is expected.

When I call w->render(painter, w->mapToGlobal(QPoint(0,0)), I get a bunch of controls all overlapping each other.
w->layout()->activate();w->layout()->update() doesn't seem to do anything.

Is there a way to force the layout to happen without showing w?

Chris
  • 1,522
  • 1
  • 12
  • 19

6 Answers6

21

Forcing a layout calculation on a widget without showing it on the screen:

widget->setAttribute(Qt::WA_DontShowOnScreen);
widget->show();

The show() call will force the layout calculation, and Qt::WA_DontShowOnScreen ensures that the widget is not explicitly shown.

Ton van den Heuvel
  • 10,157
  • 6
  • 43
  • 82
  • Note that a widget will only become visible if all of its ancestors are also visible. If this is not the case, calling `show()` might not solve the problem here. See my answer below for a different approach which should always work. – emkey08 Oct 07 '13 at 21:36
  • 1
    @MathiasKunter Still, should be noted, that paint events are perfectly flowing to the "shown" (but not visible on screen) widget, if you, say, feed some mouse clicks to it (unlike with plain parentless widget)! – mlvljr Sep 25 '14 at 18:52
  • @mlvljr You're right, this solution may cause incorrect event dispatching. So, don't use this, as it might cause subtle bugs, especially when event handlers are reimplemented within the concerned widget. – emkey08 Sep 26 '14 at 14:03
  • @MathiasKunter Well, my point actually was that if one indeed wants an invisible widget, this is the way to go (probably). But a tricky scenarios may indeed happen, too, due to that very reason (worked well for me in a simple demo though) :) – mlvljr Sep 26 '14 at 18:47
14

The layout calculation of a widget can be forced by calling invalidate() followed by activate() on its layout, even if the widget is hidden. This also causes the widget's size() and sizeHint() functions to return correct and updated values, even if show() has not yet been called on that widget.

It is however necessary to care about all child widgets and layouts recursively, as a layout recalculation request doesn't automatically propagate to the childs.

The following code shows how to do this.

/**
 * Forces the given widget to update, even if it's hidden.
 */
void forceUpdate(QWidget *widget) {
    // Update all child widgets.
    for (int i = 0; i < widget->children().size(); i++) {
        QObject *child = widget->children()[i];
        if (child->isWidgetType()) {
            forceUpdate((QWidget *)child);
        }
    }

    // Invalidate the layout of the widget.
    if (widget->layout()) {
        invalidateLayout(widget->layout());
    }
}

/**
 * Helper function for forceUpdate(). Not self-sufficient!
 */
void invalidateLayout(QLayout *layout) {
    // Recompute the given layout and all its child layouts.
    for (int i = 0; i < layout->count(); i++) {
        QLayoutItem *item = layout->itemAt(i);
        if (item->layout()) {
            invalidateLayout(item->layout());
        } else {
            item->invalidate();
        }
    }
    layout->invalidate();
    layout->activate();
}
emkey08
  • 5,059
  • 3
  • 33
  • 34
  • 1
    The `layout->invalidate(); layout->activate();` worked for me for a slightly different application: forcing the resize of a already visible layout after adding/removing a widget. – Onlyjus Sep 11 '15 at 13:34
  • If `layout->invalidate(); layout->activate();` doesn't refresh the geometry, try adding `QApplication::processEvents();` This is probably not a proper way, but it solved my problem – benk May 07 '20 at 18:47
1

I had some succes in a similar problem by first calling w->layout()->update() before w->layout()->activate(). That seems to force the activate() to actually do something rather than think it is fine because the window isn't being shown anyway.

JanKanis
  • 6,346
  • 5
  • 38
  • 42
1

Try with the QWidget::sizeHint() method, which is supposed to return the size of the widget once laid out.

gregseth
  • 12,952
  • 15
  • 63
  • 96
  • I tried that, and it did give me back reasonable sizes.. but it didn't update the positions! So the sizes look right, but they are still on top of each other. – Chris Mar 16 '10 at 14:05
  • I have found this to intermittently return an incorrect (usually much smaller size) result until after the widget is shown or painted once. – Rotsiser Mho Nov 29 '22 at 14:51
1

When going through QWidget::grab() I noticed this part:

if (r.width() < 0 || r.height() < 0) {
    // For grabbing widgets that haven't been shown yet,
    // we trigger the layouting mechanism to determine the widget's size.
    r = d->prepareToRender(QRegion(), renderFlags).boundingRect();
    r.setTopLeft(rectangle.topLeft());
}

QWidget::grab() has been introduced in Qt 5.0, and this test function with a QDialog containing a layout seems to work on Qt 5.5.1

int layoutTest_2(QApplication& a)
{
    CLayoutTestDlg dlg; // initial dlg size can also be set in the constructor
        // https://stackoverflow.com/questions/21635427

    QPixmap pixmap = dlg.grab(); // must be called with default/negative-size QRect
    bool savedOK = pixmap.save("E:/temp/dlg_img.png");
        // saving is not necessary, but by now the layout should be done

    dlg.show();
    return a.exec();
}
mh2
  • 11
  • 2
0

This worked for me when using the sizeHint() plus translating the painter, however I do this inside the paint() method.

void ParentWidget::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
  painter->save();
  painter->translate(option.rect.topLeft());
  w->render(painter);
  painter->restore();
}

In this case, option.rect.topLeft() gives me the correct placement. You should try a more sensible coordinate instead of w->mapToGlobal(QPoint(0,0).

milton
  • 988
  • 6
  • 19