QComboBox and QPushButton in a grid layout do not line up properly in macOS



  • I'm working on a Qt 5.9.1 app in macOS 10.13.2. I have an issue in one of my windows where a QPushButton and a QComboBox are in the same column of a grid layout, but their left sides do not line up properly. Here is a screen shot:

    Picture of widgets not lining up properly

    It's a subtle issue, but the left side of the button is one pixel too far to the right.

    I try very hard to make my interfaces professional looking, which includes having the widgets line up nicely. However I don't know what I can do to address this. If I use a style sheet on either widget to try and adjust its margins then they will no longer render as standard macOS UI elements.

    Why is this happening? And what can I do? Any help is much appreciated!



  • Hi,

    Have you tried the same with QVBoxLayout and QHBoxLayout? I have always felt much control over the layout items when I use those rather than grid layout.

    Regards,
    San



  • The screenshot only shows a small portion of my UI. There are more controls not pictured that require me to use a grid layout so that everything stays in proper alignment.

    If necessary I could just a combination of many nested horizontal and vertical layouts to accomplish the same thing, but it would be a lot of work.

    I'm more curious why Qt doesn't automatically line up these two widgets properly, when nearly every other widget does line up. Perhaps this is a Qt bug?



  • i cannot understand what you say, can you draw me a picture to show the correct layout what you want.
    i want to help you.


  • Lifetime Qt Champion

    Hi,

    Are you using a QGridLayout ?

    Did you already took a look at QFromLayout which seems to be more in line with the UI you are showing ?



  • To all: here is another screenshot that further demonstrates the extent of this problem, plus why it is necessary for my to use QGridLayout:

    0_1517327056357_Screen Shot 2018-01-30 at 10.41.59 AM.png

    First, note that I have several different UI elements whose left and/or right sides are aligned throughout various columns. In order to maintain a good and clean look for this UI, it is important that I use a QGridLayout, as otherwise these elements would not be aligned. Furthermore, you can see how using a QFormLayout does not work.

    And it's not just QPushButton that's out of alignment. In order for these elements to be properly aligned given the spacing I supplied the QGridLayout, the following adjustments need to be made:

    1. QComboBoxes need to be moved two pixels to the right
    2. QCheckBoxes need to be moved one pixel to the right.

    That only applies on a retina display, though. On a non-retina display, the QComboBoxes need to be moved one pixel to the right.

    I'm thinking this is a Qt bug and that I'll need to make a bug report, but if there's any good way to work around this in the meantime, I'd love to hear it.


  • Qt Champions 2016

    Hi
    Its not a bug. Its just drawn that way.
    alt text

    Both are PushButtons but red one, is filling whole area with custom paintEvent.
    So its just visually they are off. The actual clientrect is not.



  • @mrjj Well I would say that it's not so much that the bug is in QGridLayout but rather the drawing of these widgets. QGridLayout is indeed laying out everything in proper alignment. But the widgets themselves are not drawing in proper alignment.

    Ordinarily I could fix this easily using a style sheet but just adding something like a pixel or two of negative margins to shift the element over. But as soon as you apply a style sheet to these widgets in macOS, they stop rendering as standard macOS elements are revert to Qt's default style that looks more like elements in X11 from fifteen years ago.


  • Qt Champions 2016

    @Guy-Gizmo
    Hi
    I am assuming its the same as in windows and macOS that the QStyle
    used subtract the pixel before drawing the buttons/checkbox.

    Yeah, sadly is stylesheet all or nothing but all is not lost yet its only 2 widgets that you seems to have this issue with and you could do

    #ifndef FLATBUT_H
    #define FLATBUT_H
    
    #include <QPainter>
    #include <QPushButton>
    #include <QStyleOptionButton>
    #include <QStylePainter>
    
    class Flatbut : public QPushButton { // sorry about the name :)
      Q_OBJECT
    public:
      explicit Flatbut(QWidget* parent = nullptr) : QPushButton(parent) {}
    protected:
      virtual void paintEvent(QPaintEvent* event) override {
        QStylePainter p(this);
        QStyleOptionButton option;
        initStyleOption(&option);
        option.rect = rect().adjusted(-1, 0, -1, 0);
        p.drawControl(QStyle::CE_PushButton, option);
      }
    };
    #endif // FLATBUT_H
    

    And make it draw how you want.
    alt text
    (top one is the red one from last picture with a new custom paint (stolen from QPushbutton))

    The downside is that you must promote all your buttons and checkboxes to apply the fix.
    There might be a way via QProxyStyle if the QStyle has a setting for it. (the offset)( didnt check yet)


  • Qt Champions 2016

    Ah
    Found out why it reservers the pixels.
    Its for the "default" feature and if you change the offset, it breaks visually.
    alt text
    So if you use this feature, customPaint might be a no go.



  • @Guy-Gizmo

    i think the reason is that, some pixel is transparent.

    for exzample:

    a button , which the size is 90x90, may be when it display at mac os , have only 86x86.
    in the top\left\right\buttom have 2 pixel which display nothing.

    so the only method to solved this problem is use custom stylesheet.(or send a bug to digia).
    do not use spacer or margin to fix it, because maybe in the different version of mac os , you could saw different graphics result.



  • @Mr-Aloof I believe, this may be due to the Focus-Rectangle that MacOs wants to apply to QPushButtons.
    IIRC the F-Rectangle turns the border (1px wide) blue and extends 1 px out too

    You could use a QToolButton and see if it changes anything.



  • Okay, after lots of work and a fair amount of hair pulling, I finally got all of my widgets lining up properly in a QGridLayout.

    @mrjj, I essentially used the solution you proposed, but with a few tweaks to improve the rendering of widgets beyond just their alignment. Fortunately in macOS, unlike Windows (and probably Linux too) there is lots of margins around various macOS widgets, meaning there's no issue with rendering them one or two pixels to the side of where they normally would be. So this solution won't work in anything other than macOS, but as far as I know, the problem doesn't exist anywhere other than macOS!

    So for posterity and such, here is the code I wrote to fix the issue:

    // QtAdjustedWidgets.h
    
    #include <QComboBox>
    #include <QPushButton>
    #include <QRadioButton>
    #include <QLineEdit>
    #include <QSpinBox.h>
    #include <QDoubleSpinBox.h>
    #include <QPixmap>
    #include <QMarginsF>
    
    class AdjustedWidgetHelper {
    public:
        AdjustedWidgetHelper() : pixmap(nullptr) {};
        ~AdjustedWidgetHelper();
        QRectF calculateDrawRect(QWidget *widget, const QMarginsF &nonRetinaAdjustment, const QMarginsF &retinaAdjustment);
        QPixmap * setupPixmap(QRectF drawRect, int devicePixelRatio);
    protected:
        QPixmap *pixmap;
    };
    
    class QtAdjustedComboBox : public QComboBox {
        Q_OBJECT
    public:
        explicit QtAdjustedComboBox(QWidget *parent = nullptr) : QComboBox(parent) {};
    protected:
        void paintEvent(QPaintEvent *);
        AdjustedWidgetHelper helper;
        static QMarginsF nonRetinaAdjustment;
        static QMarginsF retinaAdjustment;
    };
    
    class QtAdjustedPushButton : public QPushButton {
        Q_OBJECT
    public:
        explicit QtAdjustedPushButton(QWidget *parent = nullptr) : QPushButton(parent) {};
    protected:
        void paintEvent(QPaintEvent *);
        AdjustedWidgetHelper helper;
        static QMarginsF nonRetinaAdjustment;
        static QMarginsF retinaAdjustment;
    };
    
    class QtAdjustedRadioButton : public QRadioButton {
        Q_OBJECT
    public:
        explicit QtAdjustedRadioButton(QWidget *parent = nullptr) : QRadioButton(parent) {};
    protected:
        void paintEvent(QPaintEvent *);
        static QMarginsF nonRetinaAdjustment;
        static QMarginsF retinaAdjustment;
    };
    
    class QtAdjustedLineEdit : public QLineEdit {
        Q_OBJECT
    public:
        explicit QtAdjustedLineEdit(QWidget *parent = nullptr) : QLineEdit(parent) {};
    protected:
        void paintEvent(QPaintEvent *);
        AdjustedWidgetHelper helper;
        static QMarginsF nonRetinaAdjustment;
        static QMarginsF retinaAdjustment;
    };
    
    class QtAdjustedSpinBox : public QSpinBox {
        Q_OBJECT
    public:
        explicit QtAdjustedSpinBox(QWidget *parent = nullptr) : QSpinBox(parent) {};
    protected:
        void paintEvent(QPaintEvent *);
        AdjustedWidgetHelper helper;
        static QMarginsF nonRetinaAdjustment;
        static QMarginsF retinaAdjustment;
    };
    
    class QtAdjustedDoubleSpinBox : public QDoubleSpinBox {
        Q_OBJECT
    public:
        explicit QtAdjustedDoubleSpinBox(QWidget *parent = nullptr) : QDoubleSpinBox(parent) {};
    protected:
        void paintEvent(QPaintEvent *);
        AdjustedWidgetHelper helper;
        static QMarginsF nonRetinaAdjustment;
        static QMarginsF retinaAdjustment;
    };
    
    // QtAdjustedWidgets.cpp
    #include <QPainter>
    #include <QStylePainter>
    #include "QtAdjustedWidgets.h"
    
    QMarginsF QtAdjustedComboBox::nonRetinaAdjustment(-1.0, 0.0, 0.0, 0.0);
    QMarginsF QtAdjustedComboBox::retinaAdjustment(-0.5, 0.0, 0.5, 0.0);
    
    QMarginsF QtAdjustedPushButton::nonRetinaAdjustment(0.0, 0.0, 0.0, 0.0);
    QMarginsF QtAdjustedPushButton::retinaAdjustment(0.5, 0.0, 0.5, 0.0);
    
    QMarginsF QtAdjustedRadioButton::nonRetinaAdjustment(-1.0, 0.0, 1.0, 0.0);
    QMarginsF QtAdjustedRadioButton::retinaAdjustment(-1.0, 0.0, 1.0, 0.0);
    
    QMarginsF QtAdjustedLineEdit::nonRetinaAdjustment(0.0, 0.0, 0.0, 0.0);
    QMarginsF QtAdjustedLineEdit::retinaAdjustment(-1.5, 0.0, -1.5, 0.0);
    
    QMarginsF QtAdjustedSpinBox::nonRetinaAdjustment(1.0, 0.0, 1.0, 0.0);
    QMarginsF QtAdjustedSpinBox::retinaAdjustment(1.5, 0.0, 1.5, 0.0);
    
    QMarginsF QtAdjustedDoubleSpinBox::nonRetinaAdjustment(1.0, 0.0, 1.0, 0.0);
    QMarginsF QtAdjustedDoubleSpinBox::retinaAdjustment(1.5, 0.0, 1.5, 0.0);
    
    AdjustedWidgetHelper::~AdjustedWidgetHelper()
    {
        if (pixmap) {
            delete pixmap;
        }
    }
    
    QRectF AdjustedWidgetHelper::calculateDrawRect(QWidget *widget, const QMarginsF &nonRetinaAdjustment, const QMarginsF &retinaAdjustment)
    {
        if (widget->devicePixelRatio() == 2) {
            return QRectF(widget->rect()) + retinaAdjustment;
        } else {
            return QRectF(widget->rect()) + nonRetinaAdjustment;
        }
    }
    
    QPixmap * AdjustedWidgetHelper::setupPixmap(QRectF drawRect, int devicePixelRatio)
    {
        QSize pixmapSize(drawRect.width() * devicePixelRatio, drawRect.height() * devicePixelRatio);
        
        if (!pixmap || pixmap->size() != pixmapSize || pixmap->devicePixelRatio() != devicePixelRatio) {
            if (pixmap) {
                delete pixmap;
            }
            
            pixmap = new QPixmap(pixmapSize);
            pixmap->setDevicePixelRatio(devicePixelRatio);
        }
        
        pixmap->fill(Qt::transparent);
        
        return pixmap;
    }
    
    void QtAdjustedComboBox::paintEvent(QPaintEvent *)
    {
        QRectF drawRect = helper.calculateDrawRect(this, nonRetinaAdjustment, retinaAdjustment);
        QPixmap *pixmap = helper.setupPixmap(drawRect, devicePixelRatio());
        QPainter painter(this);
        QStylePainter stylePainter(pixmap, this);
        QStyleOptionComboBox options;
        
        initStyleOption(&options);
        options.rect = QRect(0, 0, drawRect.width(), drawRect.height());
        stylePainter.setPen(palette().color(QPalette::Text));
        stylePainter.drawComplexControl(QStyle::CC_ComboBox, options);
        stylePainter.drawControl(QStyle::CE_ComboBoxLabel, options);
        
        painter.drawPixmap(drawRect, *pixmap, QRectF(pixmap->rect()));
    }
    
    void QtAdjustedPushButton::paintEvent(QPaintEvent *)
    {
        QRectF drawRect = helper.calculateDrawRect(this, nonRetinaAdjustment, retinaAdjustment);
        QPixmap *pixmap = helper.setupPixmap(drawRect, devicePixelRatio());
        QPainter painter(this);
        QStylePainter stylePainter(pixmap, this);
        QStyleOptionButton options;
        
        initStyleOption(&options);
        options.rect = QRect(0, 0, drawRect.width(), drawRect.height());
        stylePainter.setPen(palette().color(QPalette::Text));
        stylePainter.drawControl(QStyle::CE_PushButton, options);
        
        painter.drawPixmap(drawRect, *pixmap, QRectF(pixmap->rect()));
    }
    
    void QtAdjustedRadioButton::paintEvent(QPaintEvent *)
    {
        // For some odd reason, stylePainter does not render the label of QRadioButton correctly when
        // rendering into a QPixmap -- the font is slightly too heavy. Fortunately, we don't need to
        // adjust it by a single pixel on a retina display so we don't need the QPixmap.
        QStylePainter stylePainter(this);
        QStyleOptionButton options;
        initStyleOption(&options);
        
        if (devicePixelRatio() == 2) {
            options.rect = QRectF(options.rect).marginsAdded(retinaAdjustment).toRect();
        } else {
            options.rect = QRectF(options.rect).marginsAdded(nonRetinaAdjustment).toRect();
        }
        
        stylePainter.drawControl(QStyle::CE_RadioButton, options);
    }
    
    void QtAdjustedLineEdit::paintEvent(QPaintEvent *event)
    {
        QLineEdit::paintEvent(event);
        
        // Qt renders the lighter rectangle on the outside of QLineEdit's frame, so in that case
        // we'll render our own frame with the right colors.
        if (devicePixelRatio() == 2) {
            QPainter painter(this);
            
            painter.setPen(QPen(QBrush(QColor(177, 177, 177)), 0.5));
            painter.setBrush(Qt::transparent);
            painter.drawRect(QRectF(rect()).adjusted(0, 0, -0.5, -0.5));
            
            painter.setPen(QPen(QBrush(QColor(240, 240, 240)), 0.5));
            painter.drawRect(QRectF(rect()).adjusted(0.5, 0.5, -1, -1));
        }
    }
    
    void QtAdjustedSpinBox::paintEvent(QPaintEvent *)
    {
        QRectF drawRect = helper.calculateDrawRect(this, nonRetinaAdjustment, retinaAdjustment);
        QPixmap *pixmap = helper.setupPixmap(drawRect, devicePixelRatio());
        QPainter painter(this);
        QStylePainter stylePainter(pixmap, this);
        QStyleOptionSpinBox options;
        
        initStyleOption(&options);
        options.rect = QRect(0, 0, drawRect.width(), drawRect.height());
        
        // Qt leaves a gap between the frame a line edit control when rendering a spin box, so we'll
        // fill it in with its background color:
        QRectF fieldRect = QRectF(style()->subControlRect(QStyle::CC_SpinBox, &options, QStyle::SC_SpinBoxEditField)).adjusted(-1, -1, 1, 1);
        stylePainter.setPen(palette().color(QPalette::Base));
        stylePainter.setBrush(palette().color(QPalette::Base));
        stylePainter.drawRect(fieldRect);
        
        stylePainter.drawComplexControl(QStyle::CC_SpinBox, options);
        painter.drawPixmap(drawRect, *pixmap, QRectF(pixmap->rect()));
    }
    
    void QtAdjustedDoubleSpinBox::paintEvent(QPaintEvent *)
    {
        QRectF drawRect = helper.calculateDrawRect(this, nonRetinaAdjustment, retinaAdjustment);
        QPixmap *pixmap = helper.setupPixmap(drawRect, devicePixelRatio());
        QPainter painter(this);
        QStylePainter stylePainter(pixmap, this);
        QStyleOptionSpinBox options;
        
        initStyleOption(&options);
        options.rect = QRect(0, 0, drawRect.width(), drawRect.height());
        
        // Qt leaves a gap between the frame a line edit control when rendering a spin box, so we'll
        // fill it in with its background color:
        QRectF fieldRect = QRectF(style()->subControlRect(QStyle::CC_SpinBox, &options, QStyle::SC_SpinBoxEditField)).adjusted(-1, -1, 1, 1);
        stylePainter.setPen(palette().color(QPalette::Base));
        stylePainter.setBrush(palette().color(QPalette::Base));
        stylePainter.drawRect(fieldRect);
        
        stylePainter.drawComplexControl(QStyle::CC_SpinBox, options);
        painter.drawPixmap(drawRect, *pixmap, QRectF(pixmap->rect()));
    }
    
    

    Admittedly I'm not the biggest fan of how Qt often requires you to subclass everything in order to get customized behavior, and I tried a number of different alternative approaches, including something that could be applied automatically to any set of widgets. But in the end, subclassing was the best way to go.

    In case anyone is wondering why the objects render to QPixmaps, the reason is because some of the widgets required being moved over a fraction of a point, which would be one pixel on a retina display. When using QRect as opposed to QRectF, there's no way to accomplish this and is consequently a limitation of QStylePainter / QStyleOption. The workaround is to render into a QPixmap with a device pixel ratio of 2 and then compositing it into the widget at the correct retina pixel coordinates.



  • @Guy-Gizmo said in QComboBox and QPushButton in a grid layout do not line up properly in macOS:

    options

    congratulations´╝ü

    if i do this , i'll use custom stylesheet

    QPushButton
    {
    padding: 0px;
    margin:0px;
    image:url(xxx);
    image-position:center;
    border-image: url(xxxxxx) 2 2 2 2;
    }

    maybe fix it.
    i suggest that if you use qt, use stylesheet to redraw all the controls.
    because qt draw every control itself, maybe undefined behavior happened.

    but at last, congratulations for u!


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.