Search code examples
c++qtinstanceqtabwidget

Is there a way to place a widget into more than one of the tabs of a QTabWidget?


The motivating scenario: I'm working on a Qt/QWidgets C++ app whose GUI is largely arranged into tabs, via a hierarchy of QTabWidgets. Many of these tabs are various agglomerations of the same content, (e.g. there's a tab with widget A and B, a tab with widget B and C, and so on).

I have one particular widget class W which is a fairly heavyweight GUI object and it appears in many (but not all) of the tabs. Currently I handle that by simply creating a separate object of the W class for each tab I want it to appear in, and that basically works, but it's not 100% satisfactory for a couple of reasons:

  1. Since the widget is heavy, creating a number of instances of it slows down the GUI's creation on startup, and uses more system resources than I would like.
  2. Every time the user changes the layout/state of the widget in one tab, I have to manually echo that change to all of its "clones" in the other tabs; otherwise the user will notice the different states of the different W widgets as he moves back and forth from one tab to another. This is doable, but it's a maintenance and testing headache.

So what I'd like to do is create just a single instance of W and have it magically appear in its expected location within whichever tab is currently visible. Since only one tab with W should ever be visible at one time, it seems like a single W instance ought to be enough to accomplish that.

I thought about making a lightweight proxy/container-widget of some sort, and overriding its showEvent() method to setParent() the real W object to be its child as necessary; I think that might work, or it might turn out to be full of gotchas, so I thought I'd ask first if anyone else knows of a more elegant or better-supported way to accomplish the same result.


Solution

  • With a little help I was able to make this technique work, as shown in the example code below. Note that the green label "Shared Widget!" is only created once, but it appears in all 5 tabs (along with various normal QLabels):

    demo screenshot

    #include <QApplication>
    #include <QLabel>
    #include <QMap>
    #include <QSet>
    #include <QStackedLayout>
    #include <QTabWidget>
    #include <QWidget>
    
    /** This is a special container-class that holds a single target widget so that the target widget can be placed
      * into more than one QTabWidget at a time.  This widget will handle moving the target widget around from proxy
      * to proxy as tabs are shown, so that instead of having to create N identical widgets, we can just create one
      * target-widget and have it jump from tab to tab as necessary.
      */
    class TabProxyWidget : public QWidget
    {
    public:
       /** Constructor
         * @param optTargetWidget if non-NULL, this will be passed to SetTargetWidget().  Defaults to NULL.
         */
       TabProxyWidget(QWidget * optTargetWidget = NULL)
          : _layout(new QStackedLayout(this))
          , _targetWidget(NULL)
       {
          SetTargetWidget(optTargetWidget);
       }
    
       virtual ~TabProxyWidget() {SetTargetWidget(NULL);}
    
       /** Set the widget that we want to be a proxy for
         * @param optTargetWidget the widget we will proxy for, or NULL to disassociate us from any target widget
         * @note the same pointer for (optTargetWidget) can (and should!) be passed to multiple TabProxyWidget objects
         */
       void SetTargetWidget(QWidget * optTargetWidget);
    
       virtual void showEvent(QShowEvent *);
       virtual bool eventFilter(QObject * o, QEvent * e);
    
    private:
       void AdoptTargetWidget();
       void UpdateSizeConstraints();
    
       QStackedLayout * _layout;
       QWidget * _targetWidget;
    };
    
    static QMap<QWidget *, QSet<TabProxyWidget *> > _targetWidgetToProxies;
    
    void TabProxyWidget :: SetTargetWidget(QWidget * targetWidget)
    {
       if (targetWidget != _targetWidget)
       {
          if (_targetWidget)
          {
             _targetWidget->removeEventFilter(this);
    
             QSet<TabProxyWidget *> * proxiesForTargetWidget = _targetWidgetToProxies.contains(_targetWidget) ? &_targetWidgetToProxies[_targetWidget] : NULL;
             if ((proxiesForTargetWidget == NULL)||(proxiesForTargetWidget->isEmpty()))
             {
                printf("TabProxyWidget::SetTargetWidget(NULL):  can't proxies-table for target widget %p is %s!\n", targetWidget, proxiesForTargetWidget?"empty":"missing");
                exit(10);
             }
    
             (void) proxiesForTargetWidget->remove(this);
             if (proxiesForTargetWidget->isEmpty())
             {
                (void) _targetWidgetToProxies.remove(_targetWidget);
                delete _targetWidget;
             }
             else if (dynamic_cast<TabProxyWidget *>(_targetWidget->parentWidget()) == this)
             {
                proxiesForTargetWidget->values()[0]->AdoptTargetWidget();  // hand him off to another proxy to for safekeeping
             }
          }
    
          _targetWidget = targetWidget;
    
          if (_targetWidget)
          {
             if (_targetWidgetToProxies.contains(_targetWidget) == false) _targetWidgetToProxies[_targetWidget] = QSet<TabProxyWidget *>();
             _targetWidgetToProxies[_targetWidget].insert(this);
    
             if ((isHidden() == false)||(_targetWidget->parentWidget() == NULL)||(dynamic_cast<TabProxyWidget *>(_targetWidget->parentWidget()) == NULL)) AdoptTargetWidget();
    
             UpdateSizeConstraints();
             _targetWidget->installEventFilter(this);
          }
       }
    }
    
    bool TabProxyWidget :: eventFilter(QObject * o, QEvent * e)
    {
       if ((o == _targetWidget)&&(e->type() == QEvent::Resize)) UpdateSizeConstraints();
       return QWidget::eventFilter(o, e);
    }
    
    void TabProxyWidget :: UpdateSizeConstraints()
    {
       if (_targetWidget)
       {
          setMinimumSize(_targetWidget->minimumSize());
          setMaximumSize(_targetWidget->maximumSize());
          setSizePolicy (_primaryWidget->sizePolicy());
       }
       else
       {
          setMinimumSize(QSize(0,0));
          setMaximumSize(QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX));
          setSizePolicy (QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored));
       }
    }
    
    void TabProxyWidget :: showEvent(QShowEvent * e)
    {
       AdoptTargetWidget();
       QWidget::showEvent(e);
       if (_targetWidget) _targetWidget->show();
    }
    
    void TabProxyWidget :: AdoptTargetWidget()
    {
       if ((_targetWidget)&&(_targetWidget->parentWidget() != this))
       {
          QLayout * layout = _targetWidget->layout();
          if (layout) layout->removeWidget(_targetWidget);
    
          _targetWidget->setParent(this);
          _layout->addWidget(_targetWidget);
       }
    }
    
    static void SetWidgetBackgroundColor(QWidget * w, const QColor bc)
    {
       QPalette p = w->palette();
       p.setColor(QPalette::Window, bc);
       w->setAutoFillBackground(true);
       w->setPalette(p);
    }
    
    int main(int argc, char ** argv)
    {
       QApplication app(argc, argv);
    
       QTabWidget * tabWidget = new QTabWidget;
       tabWidget->setWindowTitle("Proxy Widget test");
    
       QWidget * proxyMe = new QLabel("Shared Widget!");
       SetWidgetBackgroundColor(proxyMe, Qt::green);
    
       int counter = 0;
       for (int i=0; i<5; i++)
       {
          QWidget * nextTab = new QWidget;
          QBoxLayout * tabLayout = new QBoxLayout(QBoxLayout::TopToBottom, nextTab);
    
          const int numAbove = rand()%3;
          for (int i=0; i<numAbove; i++) tabLayout->addWidget(new QLabel(QString("Unshared label #%1 above").arg(++counter)));
    
          tabLayout->addWidget(new TabProxyWidget(proxyMe));
    
          const int numBelow = rand()%3;
          for (int i=0; i<numBelow; i++) tabLayout->addWidget(new QLabel(QString("Unshared label #%1 below").arg(++counter)));
    
          tabWidget->addTab(nextTab, QString("Tab %1").arg(i+1));
       }
    
       tabWidget->show();
       return app.exec();
    }