QPushButton icon aligned left with text centered
Asked Answered
B

3

22

In my Qt 5.7.1 application I've some buttons, I want to align button's icons to left and centre text, but there is no option in designer to do this.

Standard algin

I can align icon and text left by adding to button stylesheet this code:

text-align:left;

Align left

But it's not what I want to achieve. So, could you please tell me, If there is any option to align icon to left, and keep text aligned center? Thank you for help.

Boring answered 20/5, 2017 at 22:19 Comment(0)
A
9

Simply specialize QPushButton and override paintEvent and sizeHint, as proposed by cbuchart here. Then use it as a regular QPushButton.

MyButton declaration and implementation:

mybutton.h:

#pragma once

#include <QPushButton>

class MyButton : public QPushButton
{
public:
    explicit MyButton(QWidget* parent = nullptr);
    virtual ~MyButton();

    void setPixmap(const QPixmap& pixmap);

    virtual QSize sizeHint() const override;

protected:
    virtual void paintEvent(QPaintEvent* e) override;

private:
    QPixmap m_pixmap;
};

mybutton.cpp:

#include "mybutton.h"

#include <QPainter>

MyButton::MyButton(QWidget* parent) : QPushButton(parent)
{
}

MyButton::~MyButton()
{
}

QSize MyButton::sizeHint() const
{
    const auto parentHint = QPushButton::sizeHint();
    // add margins here if needed
    return QSize(parentHint.width() + m_pixmap.width(), std::max(parentHint.height(), m_pixmap.height()));
}

void MyButton::setPixmap(const QPixmap& pixmap)
{
    m_pixmap = pixmap;
}

void MyButton::paintEvent(QPaintEvent* e)
{
    QPushButton::paintEvent(e);

    if (!m_pixmap.isNull())
    {
        const int y = (height() - m_pixmap.height()) / 2; // add margin if needed
        QPainter painter(this);
        painter.drawPixmap(5, y, m_pixmap); // hardcoded horizontal margin
    }
}

Exemple of usage:

Here is an example where "Promote widget" feature in Qt Designer was used to create MyButton from .ui files:

mainframe.ui:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="MyButton" name="button1">
      <property name="text">
       <string>Button</string>
      </property>
     </widget>
    </item>
    <item>
     <widget class="MyButton" name="button2">
      <property name="text">
       <string>Other button</string>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menuBar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>400</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QToolBar" name="mainToolBar">
   <attribute name="toolBarArea">
    <enum>TopToolBarArea</enum>
   </attribute>
   <attribute name="toolBarBreak">
    <bool>false</bool>
   </attribute>
  </widget>
  <widget class="QStatusBar" name="statusBar"/>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <customwidgets>
  <customwidget>
   <class>MyButton</class>
   <extends>QPushButton</extends>
   <header>mybutton.h</header>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

mainwindow.h:

#pragma once

#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
};

mainwindow.cpp:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QStyle>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QStyle* style = qApp->style();
    // set buttons pixmaps:
    ui->button1->setPixmap( style->standardPixmap(QStyle::SP_ComputerIcon) );
    ui->button2->setPixmap( style->standardPixmap(QStyle::SP_TrashIcon) );
}

MainWindow::~MainWindow()
{
    delete ui;
}

main.cpp:

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

Results in:

  • Compared to docsteer answer, this solution makes it possible to apply the new style to only some buttons of your project. And the code is also smaller.
  • Compared to IGHOR answer, you can still use the button as a regular QPushButton (using QPushButton::setText), you don't need to keep a reference to a mockup QLabel to change button's text.
Admonition answered 31/5, 2017 at 19:4 Comment(12)
Hello, I'm trying to implement this, but after promoting widget, nothing change :( How should I implement this?Boring
Have you called setPixmap? Is paintEvent called (try to put a breakpoint here)Admonition
@km2442: Added an example of usage through a .uiAdmonition
will check this out toomorow at home :)Boring
Ok, It's working well, but icon is pixelised after scalling to proper size... Is there a option to set this pixmap from designer?Boring
@km2442: Dunno how to do that. Sorry.To avoid pixelation, you may rescale the pixmap using doc.qt.io/qt-5/qpixmap.html#scaled with option Qt::SmoothTransformationAdmonition
Ok, now it looks good - thanks. One more question, I'm using grid layout, how can I set horizontal margin width? With 25x25 pixmap icon don't fit to button... When I used creator and added big icon, creator automatically adjusted vertical margin :/ i.imgur.com/0578ghT.jpgBoring
@km2442: Just try to extend button height in MyButton::sizeHint()Admonition
Done, now it's ok. I have found another problem. I have to resize and move Label inside Button, because now text is not centered, and text is covered by pixmap :/ i.imgur.com/jiyMun1.jpg Is there a option to enable word wrap for button?Boring
@km2442: Word wrap won't know there is an image being displayed...so this won't help. You could try to consider hidding the pixmap if there is not enough space to show it...Admonition
Let us continue this discussion in chat.Boring
Thank you for your answers. Your solution is the best. It doesnt break entire stylesheet and can be applied for selected PushButtins :)Boring
S
12

Less code way without breaking UI style

pushButton->setIcon(QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion));
pushButton->setStyleSheet("text-align:left;");
pushButton->setLayout(new QGridLayout);

QLabel* textLabel = new QLabel("Hello world!");
textLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); // or center
textLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);

pushButton->layout()->addWidget(textLabel);

Remember to send setText signals to textLabel instead of pushButton

Scoff answered 31/5, 2017 at 18:32 Comment(1)
This is a very interesting workaround. I like how this can be easily translated by pyQt5 users too. The accepted answer is a bit tougher to read when you're not a C coder, even though there is only really mybutton.cpp to translate.Prasad
L
10

To get this level of control you will need to write some code to override the style for your platform. This is best done using a QProxyStyle. In this case we are looking for when the style is asked to draw a CE_PushButtonLabel (the label includes the icon, and they are hard coded in Qt to be aligned together).

You need to implement a QProxyStyle and override the drawControl() method. The code for the bulk of it is copied directly from the Qt source code for the default drawcontrol method (in qcommonstyle.cpp) - so although it looks lengthy, it is mostly doing what Qt already does. I put extra /****/ markers around the sections I modified. This won't show up in Qt Designer, but will work at runtime.

Final result (shown on mac, should match platform you're on)

button with text left

main.cpp:

QApplication a(argc, argv);
a.setStyle(new MyStyle);
...

mystyle.h

 class MyStyle : public QProxyStyle
 {
 public:

 virtual void drawControl(ControlElement element, const QStyleOption *opt,
               QPainter *p, const QWidget *widget = Q_NULLPTR) const;
 };

mystyle.cpp

// Copied from Qt source code..
static QWindow *qt_getWindow(const QWidget *widget)
{
    return widget ? widget->window()->windowHandle() : 0;
}

void MyStyle::drawControl(ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *widget) const
{
    if(element==CE_PushButtonLabel)
    {
        if (const QStyleOptionButton *button = qstyleoption_cast<const QStyleOptionButton *>(opt))
        {
                QRect textRect = button->rect;
                uint tf = Qt::AlignVCenter | Qt::TextShowMnemonic;
                if (!proxy()->styleHint(SH_UnderlineShortcut, button, widget))
                    tf |= Qt::TextHideMnemonic;

                if (!button->icon.isNull()) {
                    QRect iconRect;
                    QIcon::Mode mode = button->state & State_Enabled ? QIcon::Normal : QIcon::Disabled;
                    if (mode == QIcon::Normal && button->state & State_HasFocus)
                        mode = QIcon::Active;
                    QIcon::State state = QIcon::Off;
                    if (button->state & State_On)
                        state = QIcon::On;

                    QPixmap pixmap = button->icon.pixmap(qt_getWindow(widget), button->iconSize, mode, state);

                    int pixmapWidth = pixmap.width() / pixmap.devicePixelRatio();
                    int pixmapHeight = pixmap.height() / pixmap.devicePixelRatio();
                    int labelWidth = pixmapWidth;
                    int labelHeight = pixmapHeight;
                    int iconSpacing = 4;//### 4 is currently hardcoded in QPushButton::sizeHint()
                    int textWidth = button->fontMetrics.boundingRect(opt->rect, tf, button->text).width();
                    if (!button->text.isEmpty())
                        labelWidth += (textWidth + iconSpacing);

                    /*************************************************************/
                    // Make the icon rectangle always be 10px in from the left edge
                    /*************************************************************/
                    iconRect = QRect(10,
                                     textRect.y() + (textRect.height() - labelHeight) / 2,
                                     pixmapWidth, pixmapHeight);

                    iconRect = visualRect(button->direction, textRect, iconRect);

                    /***********************************/
                    // Always horizontal align the text
                    /***********************************/
                    tf |= Qt::AlignHCenter;


                    if (button->state & (State_On | State_Sunken))
                        iconRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, opt, widget),
                                           proxy()->pixelMetric(PM_ButtonShiftVertical, opt, widget));
                    p->drawPixmap(iconRect, pixmap);
                } else {
                    tf |= Qt::AlignHCenter;
                }
                if (button->state & (State_On | State_Sunken))
                    textRect.translate(proxy()->pixelMetric(PM_ButtonShiftHorizontal, opt, widget),
                                 proxy()->pixelMetric(PM_ButtonShiftVertical, opt, widget));

                if (button->features & QStyleOptionButton::HasMenu) {
                    int indicatorSize = proxy()->pixelMetric(PM_MenuButtonIndicator, button, widget);
                    if (button->direction == Qt::LeftToRight)
                        textRect = textRect.adjusted(0, 0, -indicatorSize, 0);
                    else
                        textRect = textRect.adjusted(indicatorSize, 0, 0, 0);
                }
                proxy()->drawItemText(p, textRect, tf, button->palette, (button->state & State_Enabled),
                             button->text, QPalette::ButtonText);
            }
            return;
    }

    // For all other controls, draw the default
    QProxyStyle::drawControl(element, opt, p, widget);
}
Leggett answered 31/5, 2017 at 2:0 Comment(0)
A
9

Simply specialize QPushButton and override paintEvent and sizeHint, as proposed by cbuchart here. Then use it as a regular QPushButton.

MyButton declaration and implementation:

mybutton.h:

#pragma once

#include <QPushButton>

class MyButton : public QPushButton
{
public:
    explicit MyButton(QWidget* parent = nullptr);
    virtual ~MyButton();

    void setPixmap(const QPixmap& pixmap);

    virtual QSize sizeHint() const override;

protected:
    virtual void paintEvent(QPaintEvent* e) override;

private:
    QPixmap m_pixmap;
};

mybutton.cpp:

#include "mybutton.h"

#include <QPainter>

MyButton::MyButton(QWidget* parent) : QPushButton(parent)
{
}

MyButton::~MyButton()
{
}

QSize MyButton::sizeHint() const
{
    const auto parentHint = QPushButton::sizeHint();
    // add margins here if needed
    return QSize(parentHint.width() + m_pixmap.width(), std::max(parentHint.height(), m_pixmap.height()));
}

void MyButton::setPixmap(const QPixmap& pixmap)
{
    m_pixmap = pixmap;
}

void MyButton::paintEvent(QPaintEvent* e)
{
    QPushButton::paintEvent(e);

    if (!m_pixmap.isNull())
    {
        const int y = (height() - m_pixmap.height()) / 2; // add margin if needed
        QPainter painter(this);
        painter.drawPixmap(5, y, m_pixmap); // hardcoded horizontal margin
    }
}

Exemple of usage:

Here is an example where "Promote widget" feature in Qt Designer was used to create MyButton from .ui files:

mainframe.ui:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="MyButton" name="button1">
      <property name="text">
       <string>Button</string>
      </property>
     </widget>
    </item>
    <item>
     <widget class="MyButton" name="button2">
      <property name="text">
       <string>Other button</string>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menuBar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>400</width>
     <height>20</height>
    </rect>
   </property>
  </widget>
  <widget class="QToolBar" name="mainToolBar">
   <attribute name="toolBarArea">
    <enum>TopToolBarArea</enum>
   </attribute>
   <attribute name="toolBarBreak">
    <bool>false</bool>
   </attribute>
  </widget>
  <widget class="QStatusBar" name="statusBar"/>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <customwidgets>
  <customwidget>
   <class>MyButton</class>
   <extends>QPushButton</extends>
   <header>mybutton.h</header>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

mainwindow.h:

#pragma once

#include <QMainWindow>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
};

mainwindow.cpp:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QStyle>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QStyle* style = qApp->style();
    // set buttons pixmaps:
    ui->button1->setPixmap( style->standardPixmap(QStyle::SP_ComputerIcon) );
    ui->button2->setPixmap( style->standardPixmap(QStyle::SP_TrashIcon) );
}

MainWindow::~MainWindow()
{
    delete ui;
}

main.cpp:

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

Results in:

  • Compared to docsteer answer, this solution makes it possible to apply the new style to only some buttons of your project. And the code is also smaller.
  • Compared to IGHOR answer, you can still use the button as a regular QPushButton (using QPushButton::setText), you don't need to keep a reference to a mockup QLabel to change button's text.
Admonition answered 31/5, 2017 at 19:4 Comment(12)
Hello, I'm trying to implement this, but after promoting widget, nothing change :( How should I implement this?Boring
Have you called setPixmap? Is paintEvent called (try to put a breakpoint here)Admonition
@km2442: Added an example of usage through a .uiAdmonition
will check this out toomorow at home :)Boring
Ok, It's working well, but icon is pixelised after scalling to proper size... Is there a option to set this pixmap from designer?Boring
@km2442: Dunno how to do that. Sorry.To avoid pixelation, you may rescale the pixmap using doc.qt.io/qt-5/qpixmap.html#scaled with option Qt::SmoothTransformationAdmonition
Ok, now it looks good - thanks. One more question, I'm using grid layout, how can I set horizontal margin width? With 25x25 pixmap icon don't fit to button... When I used creator and added big icon, creator automatically adjusted vertical margin :/ i.imgur.com/0578ghT.jpgBoring
@km2442: Just try to extend button height in MyButton::sizeHint()Admonition
Done, now it's ok. I have found another problem. I have to resize and move Label inside Button, because now text is not centered, and text is covered by pixmap :/ i.imgur.com/jiyMun1.jpg Is there a option to enable word wrap for button?Boring
@km2442: Word wrap won't know there is an image being displayed...so this won't help. You could try to consider hidding the pixmap if there is not enough space to show it...Admonition
Let us continue this discussion in chat.Boring
Thank you for your answers. Your solution is the best. It doesnt break entire stylesheet and can be applied for selected PushButtins :)Boring

© 2022 - 2024 — McMap. All rights reserved.