如何在不改变其他Qt小部件位置的情况下使一个Qt小部件不可见?

54

我的窗口里有很多QPushButtons、QLabels和其他有趣的QWidgets,它们都是使用各种QLayout对象动态布局的...我想做的是偶尔让一些小部件变得不可见。也就是说,这些不可见的小部件仍将占据其在窗口布局中的正常空间,但它们不会被渲染:相反,用户只会看到窗口背景色在小部件的矩形/区域内。

hide()和/或setVisible(false)无法解决问题,因为它们会使小部件完全从布局中移除,从而允许其他小部件扩展以占用“新可用”的空间; 这是我想避免的效果。

我想我可以创建每个QWidget类型的子类,覆盖paintEvent() (和mousePressEvent()等)以成为no-op(在适当时),但我更希望有一个不需要我创建三十多个不同的QWidget子类的解决方案。


什么是Qt版本? - shan
1
你可以使用QGraphicsOpacityEffect。 - stdout
很遗憾,小部件上的图形效果不是跨平台的。 :( - Kuba hasn't forgotten Monica
也许你并不想隐藏小部件,而是想将它们停用? - Troyseph
禁用小部件是最初的功能,但负责人希望它们变为不可见。 - Jeremy Friesner
可能是 Hiding Qt widget and keeping widget space 的重复问题。 - George Hilliard
10个回答

95

15
顺便说一句,感谢您的反馈。我为Qt编写/贡献了代码,所以知道它很有用真是太好了:) - Thorbjørn Martsum
10
确实非常有用!如果它能以Qt Designer选项的形式呈现,那么它将变得更加有用。 - Neurotransmitter

15
我所知道的唯一可行的方法是将事件过滤器附加到小部件上,并过滤掉重绘事件。它将适用于任何复杂的小部件 - 它可以具有子小部件。
下面是一个完整的独立示例。虽然它带有一些警告,并且需要进一步开发才能使其完整。只重写了绘画事件,因此您仍然可以与小部件交互,只是看不到任何效果。
鼠标单击、鼠标进入/离开事件、焦点事件等仍将到达小部件。如果小部件依赖于某些在重绘时执行的特定操作,例如由于在这些事件上触发的update(),可能会出现问题。
至少需要一个case语句来阻止更多的事件 - 例如鼠标移动和点击事件。处理焦点是一个问题:如果小部件在被隐藏时具有焦点,则需要将焦点移到链中的下一个小部件,并在每次重新获取焦点时进行处理。
鼠标跟踪也存在一些问题,如果之前正在跟踪鼠标,则需要假装小部件失去了鼠标跟踪。正确模拟这需要一些研究,我不知道Qt向小部件呈现的确切鼠标跟踪事件协议是什么。
//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"

隐藏器可能应该像 QApplication 一样是单例模式。 - Kuba hasn't forgotten Monica
它适用于复杂的小部件吗?我的意思是像包含QPushButton和QLabel的QWidget这样的小部件。 - Viktor Benei
虽然代码有点复杂(需要解决很多代码问题和错误),但是这可能仍然是最干净/最简单的代码之一。如果能提供完整的代码,那就更好了。 - Viktor Benei
没有简单的方法。这段代码是一个完整的示例,Hider类只有15行?同样简单的是阻止其他事件到达小部件。大多数小部件都不会在意。将其制作成一个完整的、生产级别的解决方案,适用于任何小部件——从而保留小部件隐含依赖的所有协议,这更加复杂。 - Kuba hasn't forgotten Monica
Q_SLOT void Hider::hideWidget() 的签名应该是 Q_SLOT void hideWidget(Qbject *sender) 吗? - glennr
@glennr 不是的。首先,这是可工作的代码。已编译、调试等。该代码文字上是从IDE中复制粘贴的。其次,您不能这样做,因为您可以将任意信号连接到此插槽,并且根据定义,任意信号都不会与插槽兼容。唯一与您提出的插槽兼容的信号将是具有 QObject * 作为第一个参数的信号。QAbstractButton::clicked() 没有参数。第三,我没有使用参数,而是使用 QObject::sender() 方法。 - Kuba hasn't forgotten Monica

9
您可以使用 QStackedWidget。将您的按钮放在第一页上,一个空白 QWidget 放在第二页,通过改变页面索引来使您的按钮消失,同时保留其原始空间。

这看起来是最简单的实现方案。然而,额外的 QStackedWidgets 的运行时开销以及在需要的地方构建和布局 QStackedWidget 的代码可能会使其不太理想。但这仍然可能是后期最少出现意外的解决方案。 - Tom Panning
一个 QStackedWidget 可以动态插入 :) - Kuba hasn't forgotten Monica

4
我有三种解决方案:
1)子类化您的QWidget并使用一个特殊/自己的setVisible()替换方法来开启/关闭小部件的绘制(如果该小部件应该是不可见的,只需通过重写paintEvent()方法忽略绘制即可)。这是一种肮脏的解决方案,如果您可以用其他方法做到,请不要使用它。
2)使用QSpacerItem作为占位符,并将其可见性设置为要隐藏但保留在布局中的QWidget相反的状态。
3)您可以使用特殊容器小部件(从QWidget继承),该容器小部件基于其子小部件的大小获取/同步其大小。

这可以在不使用子类化、交换小部件等操作的情况下完成。事件过滤器来拯救。 - Kuba hasn't forgotten Monica
1
你的第二个解决方案并不是一个坏主意;它甚至可以作为一个包装器QWidget来实现。 - Tom Panning

3

我曾经遇到过类似的问题,最后我在我的控件旁边放了一个大小为0的间隔,并使用了Expanding的sizeType。然后我把控件本身标记为Expanding的sizeType,并将其stretch设置为1。这样,当它可见时,它优先于间隔占据空间,但当它不可见时,间隔会扩展填充控件通常占用的空间。


2
也许你需要使用 QWidget::setWindowOpacity(0.0) 方法?但是这个方法并不适用于所有情况。

1
据我所知,setWindowOpacity在嵌入式QWidgets上不起作用,只能在顶层窗口上使用。 - Viktor Benei

2
一种选择是实现一个新的QWidgetItem子类,对于QLayoutItem::isEmpty总是返回false。我怀疑这会起作用,因为Qt的QLayout例子子类忽略了QLayoutItem::isEmpty()documentation

我们忽略QLayoutItem::isEmpty(); 这意味着布局将把隐藏的小部件视为可见。

但是,如果您以这种方式添加项目到布局中,您可能会发现有点麻烦。特别是,如果您要以这种方式完成布局,则不确定是否可以轻松地在UI文件中指定布局。

2

这是一个PyQt版本的C++ Hider类,源自Kuba Ober的回答。

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)

1

我认为您可以使用QFrame作为包装器。虽然可能有更好的想法。


0
尝试使用void QWidget::erase ()。它适用于Qt 3。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接