53

I've got a window full of QPushButtons and QLabels and various other fun QWidgets, all layed out dynamically using various QLayout objects... and what I'd like to do is occasionally make some of those widgets become invisible. That is, the invisible widgets would still take up their normal space in the window's layout, but they wouldn't be rendered: instead, the user would just see the window's background color in the widget's rectangle/area.

hide() and/or setVisible(false) won't do the trick because they cause the widget to be removed from the layout entirely, allowing other widgets to expand to take up the "newly available" space; an effect that I want to avoid.

I suppose I could make a subclass of every QWidget type that override paintEvent() (and mousePressEvent() and etc) to be a no-op (when appropriate), but I'd prefer a solution that doesn't require me to create three dozen different QWidget subclasses.

demonplus
  • 5,613
  • 12
  • 49
  • 68
Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234

10 Answers10

93

This problem was solved in Qt 5.2. The cute solution is:

QSizePolicy sp_retain = widget->sizePolicy();
sp_retain.setRetainSizeWhenHidden(true);
widget->setSizePolicy(sp_retain);

http://doc.qt.io/qt-5/qsizepolicy.html#setRetainSizeWhenHidden

Thorbjørn Martsum
  • 1,761
  • 1
  • 12
  • 8
15

The only decent way I know of is to attach an event filter to the widget, and filter out repaint events. It will work no matter how complex the widget is - it can have child widgets.

Below is a complete stand-alone example. It comes with some caveats, though, and would need further development to make it complete. Only the paint event is overridden, thus you can still interact with the widget, you just won't see any effects.

Mouse clicks, mouse enter/leave events, focus events, etc. will still get to the widget. If the widget depends on certain things being done upon an a repaint, perhaps due to an update() triggered upon those events, there may be trouble.

At a minimum you'd need a case statement to block more events -- say mouse move and click events. Handling focus is a concern: you'd need to move focus over to the next widget in the chain should the widget be hidden while it's focused, and whenever it'd reacquire focus.

The mouse tracking poses some concerns too, you'd want to pretend that the widget lost mouse tracking if it was tracking before. Properly emulating this would require some research, I don't know off the top of my head what is the exact mouse tracking event protocol that Qt presents to the widgets.

//main.cpp
#include <QEvent>
#include <QPaintEvent>
#include <QWidget>
#include <QLabel>
#include <QPushButton>
#include <QGridLayout>
#include <QDialogButtonBox>
#include <QApplication>

class Hider : public QObject
{
    Q_OBJECT
public:
    Hider(QObject * parent = 0) : QObject(parent) {}
    bool eventFilter(QObject *, QEvent * ev) {
        return ev->type() == QEvent::Paint;
    }
    void hide(QWidget * w) {
        w->installEventFilter(this);
        w->update();
    }
    void unhide(QWidget * w) {
        w->removeEventFilter(this);
        w->update();
    }
    Q_SLOT void hideWidget()
    {
        QObject * s = sender();
        if (s->isWidgetType()) { hide(qobject_cast<QWidget*>(s)); }
    }
};

class Window : public QWidget
{
    Q_OBJECT
    Hider m_hider;
    QDialogButtonBox m_buttons;
    QWidget * m_widget;
    Q_SLOT void on_hide_clicked() { m_hider.hide(m_widget); }
    Q_SLOT void on_show_clicked() { m_hider.unhide(m_widget); }
public:
    Window() {
        QGridLayout * lt = new QGridLayout(this);
        lt->addWidget(new QLabel("label1"), 0, 0);
        lt->addWidget(m_widget = new QLabel("hiding label2"), 0, 1);
        lt->addWidget(new QLabel("label3"), 0, 2);
        lt->addWidget(&m_buttons, 1, 0, 1, 3);
        QWidget * b;
        b = m_buttons.addButton("&Hide", QDialogButtonBox::ActionRole);
        b->setObjectName("hide");
        b = m_buttons.addButton("&Show", QDialogButtonBox::ActionRole);
        b->setObjectName("show");
        b = m_buttons.addButton("Hide &Self", QDialogButtonBox::ActionRole);
        connect(b, SIGNAL(clicked()), &m_hider, SLOT(hideWidget()));
        QMetaObject::connectSlotsByName(this);
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Window w;
    w.show();
    return a.exec();
}

#include "main.moc"
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • The Hider should probably be a singleton, like QApplication is. – Kuba hasn't forgotten Monica May 31 '12 at 21:04
  • Does it work for complex widgets? I mean like a QWidget witch contains a QPushButton and a QLabel. – Viktor Benei Jun 02 '12 at 10:17
  • +1 for the complete code - although it's a bit complicated (too much code to solve it + make it error proof) but maybe it's still one of the cleanest/easiest ones. – Viktor Benei Jun 02 '12 at 10:23
  • There is no uncomplicated way. The code is a complete example, the Hider class is what, 15 lines? It'd be equally simple to block other events from reaching the widget. Most widgets won't care. Making it a complete, production-grade solution that would work for any widget -- thus preserving all the protocols that widgets implicitly depend on, that is more complicated. – Kuba hasn't forgotten Monica Jun 02 '12 at 14:52
  • Should the signature of Q_SLOT void Hider::hideWidget() be Q_SLOT void hideWidget(Qbject *sender) ? – glennr Jul 01 '14 at 20:27
  • @glennr No. To start with, this is working code. Compiled, debugged, etc. The code is literally a copy-paste from the IDE. Secondly not, because you can connect arbitrary signals to this slot, and an arbitrary signal by definition won't be signature compatible to the slot. The only signals compatible with the slot you propose would be those that have `QObject *` as the first argument. `QAbstractButton::clicked()` has zero arguments. Thirdly, I'm not using an argument, I'm using the `QObject::sender()` method. – Kuba hasn't forgotten Monica Jul 02 '14 at 04:22
9

You can use a QStackedWidget. Put your button on the first page, a blank QWidget on the second, and change the page index to make your button vanish while retaining its original space.

goertzenator
  • 1,960
  • 18
  • 28
  • This looks like the simplest solution to implement. The overhead (both runtime of having a lot of extra QStackedWidgets and in code to construct and layout a QStackedWidget everywhere one is needed) could make this less than desirable. But this is still probably the solution that will have the least surprises later on. – Tom Panning Jan 09 '13 at 13:02
  • A `QStackedWidget` could be inserted dynamically :) – Kuba hasn't forgotten Monica Sep 23 '13 at 18:31
4

I've 3 solutions in my mind:

1) Subclass your QWidget and use a special/own setVisible() replacement method witch turns on/off the painting of the widget (if the widget should be invisible simply ignore the painting with an overridden paintEvent() method). This is a dirty solution, don't use it if you can do it other ways.

2) Use a QSpacerItem as a placeholder and set it's visibility to the opposite of the QWidget you want to hide but preserve it's position+size in the layout.

3) You can use a special container widget (inherit from QWidget) which gets/synchronizes it's size based on it's child/children widgets' size.

Viktor Benei
  • 3,447
  • 2
  • 28
  • 37
3

I had a similar problem and I ended up putting a spacer next to my control with a size of 0 in the dimension I cared about and an Expanding sizeType. Then I marked the control itself with an Expanding sizeType and set its stretch to 1. That way, when it's visible it takes priority over the spacer, but when it's invisible the spacer expands to fill the space normally occupied by the control.

yagni
  • 1,160
  • 13
  • 15
2

May be QWidget::setWindowOpacity(0.0) is what you want? But this method doesn't work everywhere.

dmr
  • 322
  • 1
  • 11
2

One option is to implement a new subclass of QWidgetItem that always returns false for QLayoutItem::isEmpty. I suspect that will work due to Qt's QLayout example subclass documentation:

We ignore QLayoutItem::isEmpty(); this means that the layout will treat hidden widgets as visible.

However, you may find that adding items to your layout is a little annoying that way. In particular, I'm not sure you can easily specify layouts in UI files if you were to do it that way.

cgmb
  • 4,284
  • 3
  • 33
  • 60
2

Here's a PyQt version of the C++ Hider class from Kuba Ober's answer.

class Hider(QObject):
    """
    Hides a widget by blocking its paint event. This is useful if a
    widget is in a layout that you do not want to change when the
    widget is hidden.
    """
    def __init__(self, parent=None):
        super(Hider, self).__init__(parent)

    def eventFilter(self, obj, ev):
        return ev.type() == QEvent.Paint

    def hide(self, widget):
        widget.installEventFilter(self)
        widget.update()

    def unhide(self, widget):
        widget.removeEventFilter(self)
        widget.update()

    def hideWidget(self, sender):
        if sender.isWidgetType():
            self.hide(sender)
Community
  • 1
  • 1
glennr
  • 2,069
  • 2
  • 26
  • 37
1

I believe you could use a QFrame as a wrapper. Although there might be a better idea.

Yassir Ennazk
  • 6,970
  • 4
  • 31
  • 37
0

Try void QWidget::erase (). It works on Qt 3.

shan
  • 1,164
  • 4
  • 14
  • 30