c++qtqwidgetqsplitter

Custom QSplitter handle button not disappearing when toggling off righthand widget


I'm experiencing an issue with a custom QWidget/QSplitter implementation where the handle button for the righthand widget does not disappear when the widget's visibility is toggled off (using QWidget::hide() or show()).

The issue only occurs with the righthand widget and not with the lefthand widget.

Repo: https://github.com/fairybow/CollapsibleSplitter

enter image description here

The splitter widget here is a container meant to hold both a real QSplitter subclass (TrueSplitter) as well as 1 QPushButton per visibile QSplitterHandle and coordinate their movements).

When I run my code and toggle off the righthand widget using its toggle button, the righthand widget disappears but its corresponding handle button remains visible. The issue does not occur when toggling off the lefthand widget.

I've tried replacing the righthand widget (Preview) with other widgets (e.g., a second QTreeView), but the issue still persists. I'm not sure what in my code is causing this issue or how to fix it.

Working backward:

  1. The Splitter class toggles the buttons' visibilities by finding the stored pointer from a list of structs containing meta-info for each widget:
struct Meta {
        int widgetIndex;
        std::optional<QPushButton*> handleButton;
        State state = State::Expanded;
        int expandedWidth = -1;
        QPushButton* button() { return handleButton.has_value() ? handleButton.value() : nullptr; }
    };
Splitter(Qt::Orientation orientation, QVector<QWidget*> widgets, QWidget* parent)
        : QWidget(parent)
    {
        /* etc. */
        connect(m_trueSplitter, &TrueSplitter::widgetVisibilityChanged, this, &Splitter::showOrHideButtons);
    }
void showOrHideButtons(int widgetIndex, TrueSplitter::WidgetWas visibility)
    {
        for (auto& m_meta : m_metas) {
            if (m_meta.widgetIndex != widgetIndex) continue;
            auto button = m_meta.button();
            if (button == nullptr) continue;
            (visibility == TrueSplitter::WidgetWas::Hidden) ? button->hide() : button->show();
        }
    }
  1. TrueSplitter will call for the above when one of its non-central widgets is toggled off (detected by installing an event filter on each child widget):
class TrueSplitter : public QSplitter
{
    Q_OBJECT

public:
    enum class WidgetWas {
        Hidden,
        Shown
    };

    TrueSplitter(QWidget* parent) : QSplitter(parent) {}

signals:
    void resized();
    void widgetVisibilityChanged(int widgetIndex, WidgetWas visibility);

protected:
    virtual void childEvent(QChildEvent* event) override
    {
        if (event->type() == QEvent::ChildAdded && event->child()->isWidgetType())
            installFilters();
        QSplitter::childEvent(event);
    }

    virtual bool eventFilter(QObject* object, QEvent* event) override
    {
        if (event->type() == QEvent::Show || event->type() == QEvent::Hide) {
            for (auto i = 0; i < count(); ++i) {
                if (!widget(i)->isVisible()) {
                    emit widgetVisibilityChanged(i, WidgetWas::Hidden);
                    qDebug() << "!!!" << widget(i) << ": eventFilter widgetVisibilityChanged signal (Hidden)";
                }
                else {
                    emit widgetVisibilityChanged(i, WidgetWas::Shown);
                    qDebug() << "!!!" << widget(i) << ": eventFilter widgetVisibilityChanged signal (Shown)";
                }
            }
            
            for (int i = 0; i < count(); ++i) {
                auto widget = this->widget(i);
                (widget == nullptr)
                    ? qDebug() << "Widget at index" << i << "has been removed from the QSplitter"
                    : qDebug() << "Widget at index" << i << "is still a child of the QSplitter";
            }
        }
        return QSplitter::eventFilter(object, event);
    }

    virtual void resizeEvent(QResizeEvent* event) override
    {
        QSplitter::resizeEvent(event);
        emit resized();
    }

    virtual QSplitterHandle* createHandle() override { return new SplitterHandle(orientation(), this); }

private:
    void installFilters()
    {
        for (auto i = 0; i < count(); ++i)
            widget(i)->installEventFilter(this);
    }
};

Does anyone have any ideas or suggestions for how to troubleshoot this issue?


Solution

  • The problem seems to be the order in which things are happening.

    Apparently childEvent is called when a widget is added, but before that process is finished. Therefore, when you call installFilters, the iteration only sees all the widgets which were added previously and (re-)installs the filter in those. The new widget is ignored.

    Luckily, QChildEvent provides the newly added child via the child member function, so you do have to loop at all:

    virtual void childEvent(QChildEvent* event) override
    {
        if (event->type() == QEvent::ChildAdded && event->child()->isWidgetType())
            event->child()->installEventFilter(this);
        QSplitter::childEvent(event);
    }