QComboBox and QPushButton in a grid layout do not line up properly in macOS
-
To all: here is another screenshot that further demonstrates the extent of this problem, plus why it is necessary for my to use QGridLayout:
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:
- QComboBoxes need to be moved two pixels to the right
- 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.
-
Hi
Its not a bug. Its just drawn that way.
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.
-
@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.
(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) -
Ah
Found out why it reservers the pixels.
Its for the "default" feature and if you change the offset, it breaks visually.
So if you use this feature, customPaint might be a no go. -
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 tooYou 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!
-
Checkout the answer I have given here:
https://forum.qt.io/topic/105191/why-isn-t-a-qcombobox-positioned-correctly-in-a-layout