0

I have created a widget class (in C++) ToolTray which is basically few QToolButtons added in QHboxLayout. These buttons are used by the user for operations like Save, Open etc. Initially, the buttons are added with toolButtonStyle set to Qt::ToolButtonTextBesideIcon.

I want to change the toolButtonStyle to Qt::ToolButtonIconOnly in resizeEvent if the new size is not sufficient to display text and icons of QToolButton. See the picture below:

enter image description here

If the window is resized and if the new size is sufficient to display the text and icon of all the QToolButton, then toolButtonStyle should be changed back to Qt::ToolButtonTextBesideIcon.

enter image description here

I tried to achieve this with following code:

void ToolTray::resizeEvent(QResizeEvent *event)
{
    int totalWidth = 0;
    bool mode = runBtn_->toolButtonStyle() == Qt::ToolButtonStyle::ToolButtonIconOnly;
    // Mode => True => For ICON Only
    // Mode => False => For ICON + Text
    for (auto btn: toolBtns_) {
        if (btn->isVisible())
            totalWidth += btn->size().width();
    }

    qDebug() << "Total Width: " << totalWidth ;
    qDebug() << "Event Size: " << event->size() << " Old size " << event->oldSize();

    if (event->oldSize().isEmpty()) // Ignore ResizeEvent for QSize(-1,-1)
        return;

    if (mode) { // Already Small
        if (event->size().width() < preferedFullWidth_)
            return;
        for (auto btn: toolBtns_) {
            if (btn == moreBtn_)
                continue;
            btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
        }
        return;
    }
    // The QToolButtons are Text Beside Icon
    if (event->size().width() >= totalWidth)
        return;

    qDebug() << "Here";
    for (auto btn: toolBtns_)
        btn->setToolButtonStyle(Qt::ToolButtonIconOnly);
    preferedFullWidth_ = totalWidth;
}

However, I am unable to achieve what I wanted:

  1. Whenever I try to shrink the size of the window, the QToolButton's Text first start clipping and after some more size reduction, the ToolButtonStyle is changed to Qt::ToolButtonIconOnly. I don't want clipping to happen.

  2. I also want to add some margin (or hysteresis) in my implementation. Like once the QToolButton is changed to Qt::ToolButtonIconOnly at a particular width, then the width should be greater than preferedFullWidth_ + certain margin to switch back to Qt::ToolButtonTextBesideIcon.

abhiarora
  • 9,743
  • 5
  • 32
  • 57

1 Answers1

1

Toolbars seem to be a theme this week... :)

So you basically have the right idea but the tricky part is figuring out the required size. Unfortunately the QToolBar layout is completely private so we have to figure stuff out on our own (even though it inherits from QLayout you can't get an instance of it via QToolBar::layout()).

This implementation is fairly basic and will probably not handle all cases (I only tested with basic actions, no custom widgets or such), but it does work for that (tested on Windows and Linux with various styles).

ADDED: I realize you weren't asking for a QToolBar specifically, but that's what you essentially described... I'm not sure why re-invent that wheel (a QToolBar can be placed in any layout, doesn't have to be in a main window), but if you were to implement your own version I think a lot of this example would still apply. I would also personally almost always use QActions for triggering UI events (vs. buttons) since they can be assigned to any number of UI elements (toolbar, manu bar, context menu, window shortcuts, etc.).

Adding a margin/hysteresis is left as an exercise for the reader... :) I don't think it needs one, but you could pad m_expandedSize in initSizes() with some arbitrary margin.

CollapsingToolBar

#include <QtWidgets>

class CollapsingToolBar : public QToolBar
{
    Q_OBJECT
  public:
    explicit CollapsingToolBar(QWidget *parent = nullptr) : CollapsingToolBar(QString(), parent) {}

    explicit CollapsingToolBar(const QString &title, QWidget *parent = nullptr) :
      QToolBar(title, parent)
    {
      initSizes();
      // If icon sizes change we need to recalculate all the size hints, but we need to wait until the buttons have adjusted themselves, so we queue the update.
      connect(this, &QToolBar::iconSizeChanged, [this](const QSize &) {
        QMetaObject::invokeMethod(this, "recalcExpandedSize", Qt::QueuedConnection);
      });
      // The drag handle can mess up our sizing, update preferred size if it changes.
      connect(this, &QToolBar::movableChanged, [this](bool movable) {
        const int handleSz = style()->pixelMetric(QStyle::PM_ToolBarHandleExtent, nullptr, this);;
        m_expandedSize = (movable ? m_expandedSize + handleSz : m_expandedSize - handleSz);
        adjustForSize();
      });
    }

  protected:

    // Monitor action events to keep track of required size.
    void actionEvent(QActionEvent *e) override
    {
      QToolBar::actionEvent(e);

      int width = 0;
      switch (e->type())
      {
        case QEvent::ActionAdded:
          // Personal pet-peeve... optionally set buttons with menus to have instant popups instead of splits with the main button doing nothing.
          //if (QToolButton *tb = qobject_cast<QToolButton *>(widgetForAction(e->action())))
          //    tb->setPopupMode(QToolButton::InstantPopup);
          //Q_FALLTHROUGH;
        case QEvent::ActionChanged:
          width = widthForAction(e->action());
          if (width <= 0)
            return;

          if (e->type() == QEvent::ActionAdded || !m_actionWidths.contains(e->action()))
            m_expandedSize += width + m_spacing;
          else
            m_expandedSize = m_expandedSize - m_actionWidths.value(e->action()) + width;
          m_actionWidths.insert(e->action(), width);
          break;

        case QEvent::ActionRemoved:
          if (!m_actionWidths.contains(e->action()))
            break;
          width = m_actionWidths.value(e->action());
          m_expandedSize -= width + m_spacing;
          m_actionWidths.remove(e->action());
          break;

        default:
          return;
      }
      adjustForSize();
    }

    bool event(QEvent *e) override
    {
      // Watch for style change
      if (e->type() == QEvent::StyleChange)
        recalcExpandedSize();
      return QToolBar::event(e);
    }

    void resizeEvent(QResizeEvent *e) override
    {
      adjustForSize();
      QToolBar::resizeEvent(e);
    }

  private slots:

    // Here we do the actual switching of tool button style based on available width.
    void adjustForSize()
    {
      int availableWidth = contentsRect().width();
      if (!isVisible() || m_expandedSize <= 0 || availableWidth <= 0)
        return;

      switch (toolButtonStyle()) {
        case Qt::ToolButtonIconOnly:
          if (availableWidth > m_expandedSize)
            setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
          break;

        case Qt::ToolButtonTextBesideIcon:
          if (availableWidth <= m_expandedSize)
            setToolButtonStyle(Qt::ToolButtonIconOnly);
          break;

        default:
          break;
      }
    }

    // Loops over all previously-added actions and re-calculates new size (eg. after icon size change)
    void recalcExpandedSize()
    {
      if (m_actionWidths.isEmpty())
        return;
      initSizes();
      int width = 0;
      QHash<QAction *, int>::iterator it = m_actionWidths.begin();
      for ( ; it != m_actionWidths.end(); ++it) {
        width = widthForAction(it.key());
        if (width <= 0)
          continue;
        m_expandedSize += width + m_spacing;
        it.value() = width;
      }
      adjustForSize();
    }

  private:
    void initSizes()
    {
      // Preload some sizes based on style settings.
      // This is the spacing between items
      m_spacing = style()->pixelMetric(QStyle::PM_ToolBarItemSpacing, nullptr, this);
      // Size of a separator
      m_separatorWidth = style()->pixelMetric(QStyle::PM_ToolBarSeparatorExtent, nullptr, this);
      // The layout margins (we can't even get the private QToolBarLayout via layout() so we figure it out like it does)
      m_expandedSize = (style()->pixelMetric(QStyle::PM_ToolBarItemMargin, nullptr, this) + style()->pixelMetric(QStyle::PM_ToolBarFrameWidth, nullptr, this)) * 2;
      // And the size of the drag handle if we have one
      if (isMovable())
        m_expandedSize += style()->pixelMetric(QStyle::PM_ToolBarHandleExtent, nullptr, this);
    }

    int widthForAction(QAction *action) const
    {
      // Try to find how wide the action representation (widget/separator) is.
      if (action->isSeparator())
        return m_separatorWidth;

      if (QToolButton *tb = qobject_cast<QToolButton *>(widgetForAction(action))) {
        const Qt::ToolButtonStyle oldStyle = tb->toolButtonStyle();
        // force the widest size
        tb->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
        const int width = tb->sizeHint().width();
        tb->setToolButtonStyle(oldStyle);
        return width;
      }

      if (const QWidget *w = widgetForAction(action))
        return w->sizeHint().width();

      return 0;
    }

    int m_expandedSize = -1;   // The maximum size we need with all buttons expanded and allowing for margins/etc
    int m_spacing = 0;         // Layout spacing between items
    int m_separatorWidth = 0;  // Width of separators
    QHash<QAction *, int> m_actionWidths;  // Use this to track action additions/removals/changes
};

Test/demo


// An XPM icon ripped from QCommonStyle
static const char * const info_xpm[]={
"32 32 5 1",
". c None",
"c c #000000",
"* c #999999",
"a c #ffffff",
"b c #0000ff",
"...........********.............",
"........***aaaaaaaa***..........",
"......**aaaaaaaaaaaaaa**........",
".....*aaaaaaaaaaaaaaaaaa*.......",
"....*aaaaaaaabbbbaaaaaaaac......",
"...*aaaaaaaabbbbbbaaaaaaaac.....",
"..*aaaaaaaaabbbbbbaaaaaaaaac....",
".*aaaaaaaaaaabbbbaaaaaaaaaaac...",
".*aaaaaaaaaaaaaaaaaaaaaaaaaac*..",
"*aaaaaaaaaaaaaaaaaaaaaaaaaaaac*.",
"*aaaaaaaaaabbbbbbbaaaaaaaaaaac*.",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
".*aaaaaaaaaaabbbbbaaaaaaaaaac***",
".*aaaaaaaaaaabbbbbaaaaaaaaaac***",
"..*aaaaaaaaaabbbbbaaaaaaaaac***.",
"...caaaaaaabbbbbbbbbaaaaaac****.",
"....caaaaaaaaaaaaaaaaaaaac****..",
".....caaaaaaaaaaaaaaaaaac****...",
"......ccaaaaaaaaaaaaaacc****....",
".......*cccaaaaaaaaccc*****.....",
"........***cccaaaac*******......",
"..........****caaac*****........",
".............*caaac**...........",
"...............caac**...........",
"................cac**...........",
".................cc**...........",
"..................***...........",
"...................**..........."};

class MainWindow : public QMainWindow
{
    Q_OBJECT
  public:
    MainWindow(QWidget *parent = nullptr)
      : QMainWindow(parent)
    {
      QToolBar* tb = new CollapsingToolBar(this);
      tb->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
      tb->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea);

      QIcon icon = QIcon(QPixmap(info_xpm));
      for (int i=0; i < 6; ++i)
        tb->addAction(icon, QStringLiteral("Action %1").arg(i));

      addToolBar(tb);
      show();

      // Adding another action after show() may collapse all the actions if the new toolbar preferred width doesn't fit the window.
      // Only an issue if the toolbar size hint was what determined the window width to begin with.
      //tb->addAction(icon, QStringLiteral("Action After"));

      // Test setting button style after showing (comment out the one above)
      //tb->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

      // Test changing icon size after showing.
      //tb->setIconSize(QSize(48, 48));

      // Try this too...
      //tb->setMovable(false);
    }
};

int main(int argc, char *argv[])
{
  QApplication app(argc, argv);
  //QApplication::setStyle("Fusion");
  //QApplication::setStyle("windows");

  MainWindow w;
  return app.exec();
}

enter image description here enter image description here

Maxim Paperno
  • 4,485
  • 2
  • 18
  • 22