dublin19
dublin19

Reputation: 23

Display QWidgets behind a QToolBar

I try to create an application with a transparent QToolBar in the titlebar. This works with some modifications to the window itself by using some Objective-C. Also with setUnifiedTitleAndToolBarOnMac() it looks just like what I wanted. Now there is a problem. I want to add a QGridLayout later on. And just like in the new Photos app on iPadOS I want that the widgets go behind the toolbar. The transparent style would be probably achievable by styling the QToolBar (but this is a problem I can work on). My question is now, is there any possible way to overlap two widgets or send widgets behind any other widget? I could also work with a QVBoxLayout, but I don't know how to set some widgets behind any other widget (or layout).

What I try to achieve is the following:

enter image description here

My current approach is this:

enter image description here

I heard about stackUnder() but this does not work.

I hope I got my question clear, its my first time posting here.

Thanks!

EDIT:

QToolBar *tabBar = new QToolBar(this);
tabBar->setMovable(false);
tabBar->setFloatable(false);
addToolBar(tabBar);
this->setUnifiedTitleAndToolBarOnMac(true);
QPushButton *tabBtn = new QPushButton("Test", this); // simulates our iPadOS tab control
QWidget *spaceLeft = new QWidget(this);
QWidget *spaceRight = new QWidget(this);
spaceLeft->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
spaceRight->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
tabBar->addWidget(spaceLeft);
tabBar->addWidget(tabBtn);
tabBar->addWidget(spaceRight);

ui->toggleMin->stackUnder(tabBar);

The three buttons are done using QtDesigner / .ui!

Upvotes: 2

Views: 1498

Answers (1)

Maxim Paperno
Maxim Paperno

Reputation: 4869

Here is an example of a somewhat generic container QWidget that manages an overlay QWidget, which in this case is a toolbar-style widget (but it really could be anything). The code is a bit verbose for a simple example, but it tries to cover some different use cases to make the container more flexible. The overlay widget could be a QToolBar, but doesn't have to be.

The main technique here is that the overlay widget is not positioned in a layout, but instead its geometry is managed "manually" by the parent widget (see positionToolbar() in the code). This geometry needs to be re-adjusted whenever the sizes of the container or overlay change. The most convenient "hook" for this is the QWidget::resizeEvent() method which we re-implement in the example. We also monitor the overlay widget for size changes, eg. when child items are added/removed or its styling is modified.

A different direction could be to write a custom QLayout subclass which essentially does the same thing (in QLayoutItem::setGeometry() override). It would be a bit more involved, but also more flexible since it could be used in any widget or as a sub-layout.

UPDATE: I have created such a layout manager, it is called OverlayStackLayout (docs). Also a simple but functional image viewer example app, inspired by this short one.

ToolbarOverlayWidget.h

#include <QEvent>
#include <QPointer>
#include <QToolBar>
#include <QWidget>

class ToolbarOverlayWidget : public QWidget
{
    Q_OBJECT
  public:
    ToolbarOverlayWidget(QWidget *parent = nullptr) :
      QWidget(parent)
    {
      // WA_LayoutOnEntireRect will ensure that any QLayout set on this widget will
      // ignore QWidget::contentsMargins(), which allows us to use them for toolbar
      // margins/positioning instead. This does not affect any layout()->contentsMargins()
      // which can still be used to pad anything the main layout itself contains.
      setAttribute(Qt::WA_LayoutOnEntireRect);
      // create a default toolbar
      setToolbar(new QToolBar(this));
    }

    ~ToolbarOverlayWidget() override
    {
      // don't delete the toolbar widget if we don't own it
      if (m_toolbar && !m_ownTbWidget)
        m_toolbar->setParent(nullptr);
    }

    // Returns toolbar widget instance as a QToolBar.
    // Returns nullptr if no toolbar widget is set, or widget does not inherit QToolBar.
    QToolBar *toolbar() const { return qobject_cast<QToolBar*>(m_toolbar.data()); }

    // Set a widget to be used as a toolbar. ToolbarOverlayWidget takes ownership of toolbar.
    void setToolbar(QWidget *toolbar)
    {
      // dispose of old toolbar?
      if (m_toolbar) {
        m_toolbar->removeEventFilter(this);
        m_toolbar->disconnect(this);
        if (m_ownTbWidget)
          m_toolbar->deleteLater();
        else
          m_toolbar->setParent(nullptr);
        m_toolbar.clear();
      }
      if (!toolbar)
        return;

      m_toolbar = toolbar;
      // toolbar's parent should be this widget, also keep track of if we owned it originally
      m_ownTbWidget = (m_toolbar->parent() == this);
      if (!m_ownTbWidget)
        m_toolbar->setParent(this);

      m_toolbar->setAutoFillBackground(true);  // ensure a background if otherwise unstyled
      m_toolbar->installEventFilter(this);     // see eventFilter()
      if (QToolBar *tb = qobject_cast<QToolBar*>(toolbar)) {
        // reposition toolbar if icon size or button style change
        connect(tb, &QToolBar::iconSizeChanged,        this, [this](const QSize &) {
          positionToolbar(); });
        connect(tb, &QToolBar::toolButtonStyleChanged, this, [this](Qt::ToolButtonStyle) {
          positionToolbar(); });
      }
      if (isVisible())
        positionToolbar();
    }

    QSize sizeHint() const override
    {
      if (m_toolbar.isNull())
          return QWidget::sizeHint();
      // ensure a reasonable size hint if we have a toolbar which is larger than any contents
      return QWidget::sizeHint().expandedTo(m_toolbar->sizeHint());
    }

  protected:
    void resizeEvent(QResizeEvent *e) override
    {
      QWidget::resizeEvent(e);
      // keep the toolbar properly positioned
      positionToolbar();
    }

    // filter is installed on the toolbar widget
    bool eventFilter(QObject *w, QEvent *e) override
    {
      if (!m_toolbar.isNull() && w == m_toolbar) {
        switch (e->type()) {
          // reposition the toolbar if its size hint (possibly) changed
          case QEvent::ChildAdded:
          case QEvent::ChildRemoved:
          case QEvent::StyleChange:
          case QEvent::FontChange:
            if (isVisible())
              positionToolbar();
            break;

          default:
            break;
        }
      }
      return QWidget::eventFilter(w, e);
    }

  private slots:
    // Keep the toolbar properly positioned and sized
    void positionToolbar() const
    {
      if (m_toolbar.isNull())
        return;

      const QRect rect = contentsRect();  // available geometry for toolbar
      QRect tbRect(rect.topLeft(), m_toolbar->sizeHint());  // default TB position and size
      // expand to full width?
      if (m_toolbar->sizePolicy().expandingDirections() & Qt::Horizontal)
        tbRect.setWidth(rect.width());
      // constrain width if it is too wide to fit
      else if (tbRect.width() > rect.width())
        tbRect.setWidth(rect.width());
      // otherwise center the toolbar if it is narrower than available width
      else if (tbRect.width() < rect.width())
        tbRect.moveLeft(rect.x() + (rect.width() - tbRect.width()) / 2);

      // constrain height
      if (tbRect.height() > rect.height())
        tbRect.setHeight(rect.height());

      // Set position and size of the toolbar.
      m_toolbar->setGeometry(tbRect);
      // Make sure the toolbar stacks on top
      m_toolbar->raise();
    }

  private:
    QPointer<QWidget> m_toolbar;
    bool m_ownTbWidget = true;
};

Implementation example: This shows a couple styling options, both using CSS. In the first, the toolbar is minimum width and centered in the available area, and in the second the toolbar is styled as full width with centered buttons and a partially transparent background. The second version uses a plain QWidget instead of QToolBar because it's more flexible to style (QToolBar has some "quirks" to put it nicely).

Toolbar is minimum width and centered in the available area. Toolbar is full width with centered buttons and a partially transparent background.

main.cpp

int main(int argc, char *argv[])
{
  QApplication a(argc, argv);

  // Use a stack widget as top-level for demo. This will have two pages.
  QStackedWidget stack;
  stack.resize(640, 480);

  // common style for tool buttons
  const QString commonCss(QStringLiteral(
    "QToolButton {"
    "  font: bold normal 14px sans-serif;"
    "  color: #62777F;"
    "  background: transparent;"
    "  border-radius: 12px;"
    "  padding: 3px 6px 4px;"
    "}"
    "QToolButton:checked, QToolButton:hover {"
    "  color: #D5F2E5;"
    "  background-color: #62777F;"
    "}"
    "QToolButton:pressed { background-color: #72AF95; }"
  ));

  // creates a new ToolbarOverlayWidget holding one scalable image label
  auto imageWidget = [&stack](const QString &img) {
    ToolbarOverlayWidget *w = new ToolbarOverlayWidget(&stack);
    w->setLayout(new QVBoxLayout);
    w->layout()->setContentsMargins(0,0,0,0);
    QLabel *lbl = new QLabel(w);
    lbl->setPixmap(QPixmap(img));
    lbl->setScaledContents(true);
    lbl->setMinimumSize(160, 120);
    w->layout()->addWidget(lbl);
    return w;
  };

  // Page 1: The first stack page uses a default QToolBar, which is simpler but less flexible.
  {
    ToolbarOverlayWidget *widget = imageWidget("../../images/image1.jpg");
    // Set toolbar appearance
    widget->setContentsMargins(0, 10, 0, 0);  // 10px above toolbar, works better than CSS margin
    widget->toolbar()->setStyleSheet(commonCss + QLatin1String(
      "QToolBar {"
      "  background: #B5CAC1;"
      "  border-radius: 14px;"
      "  padding: 4px;"    // can only set one padding for all sides of a qtoolbar
      "  spacing: 12px;"   // between items
      "}"
      "QToolBar::separator { width: 1px; background-color: #72AF95; }"
    ));

    // Add items to toolbar

    QActionGroup *viewGrp = new QActionGroup(widget);
    auto addViewAction = [viewGrp, widget](const QString &ttl, bool chk = false) {
      QAction *act = widget->toolbar()->addAction(ttl);
      act->setCheckable(true);
      act->setChecked(chk);
      viewGrp->addAction(act);
      return act;
    };

    addViewAction("Years");
    addViewAction("Months");
    addViewAction("Days");
    addViewAction("All Photos", true);
    widget->toolbar()->addSeparator();
    // page stack "push" action
    QObject::connect(widget->toolbar()->addAction("view >"), &QAction::triggered, [&stack]() {
      stack.setCurrentIndex(1);
    });

    stack.addWidget(widget);
  }

  // Page 2: This page uses a plain widget for a toolbar.
  {
    ToolbarOverlayWidget *widget = imageWidget("../../images/image1.jpg");
    // Create a custom toolbar-style widget
    QWidget *toolbar = new QWidget(widget);
    toolbar->setLayout(new QHBoxLayout);
    toolbar->layout()->setContentsMargins(3, 14, 3, 28);
    toolbar->layout()->setSpacing(18);
    toolbar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
    toolbar->setObjectName("ToolbarWidget");
    toolbar->setStyleSheet(commonCss + QLatin1String(
      "#ToolbarWidget {"
      "  background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop: 0 black, stop: 1 transparent);"
      "}"
      "QToolButton {"
      "  color: #D5F2E5;"
      "  background-color: #62777F;"
      "}"
      "QToolButton:checked, QToolButton:hover:!pressed {"
      "  color: #62777F;"
      "  background-color: #D5F2E5;"
      "}"
    ));

    // Add items to toolbar

    auto addButton = [toolbar](const QString &ttl, QLayout *lo, bool chk = false) {
      QToolButton *tb = new QToolButton(toolbar);
      tb->setText(ttl);
      tb->setCheckable(chk);
      lo->addWidget(tb);
      return tb;
    };

    // left expander to keep buttons centered
    toolbar->layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Ignored));
    // page stack "pop" action
    QObject::connect(addButton("< back", toolbar->layout()), &QToolButton::clicked, [&stack]() {
      stack.setCurrentIndex(0);
    });
    addButton("Adjust", toolbar->layout());
    addButton("Select", toolbar->layout(), true);
    // zoom buttons, new sub-layout w/out spacing
    QHBoxLayout *zoomBtnLayout = new QHBoxLayout;
    zoomBtnLayout->setSpacing(0);
    const QString zoomCss = 
        QStringLiteral("QToolButton { border-top-%1-radius: 0; border-bottom-%1-radius: 0; }");
    addButton("-", zoomBtnLayout)->setStyleSheet(zoomCss.arg("right"));
    addButton("+", zoomBtnLayout)->setStyleSheet(zoomCss.arg("left"));
    toolbar->layout()->addItem(zoomBtnLayout);

    // right expander to keep buttons centered
    toolbar->layout()->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Ignored));

    // Use the custom widget as toolbar
    widget->setToolbar(toolbar);
    stack.addWidget(widget);
  }

  stack.show();
  return a.exec();
}
#include "main.moc"

Related questions:

Upvotes: 3

Related Questions