mr-edd.co.uk :: horsing around with the C++ programming language

Setting Qt::WA_MacSmallSize automatically for the whole application

[2nd December 2010]

In recent years Qt has made significant advancements in ensuring it looks and behaves natively on OS X. It still has some way to go, but it's undoubtedly heading in the right direction.

However, with a little bit of care I think you can just about get to a point where even the most hardened Mac zealot would struggle to tell that Cocoa isn't being used underneath.

One thing that hinders Qt's ability to blend in to its surroundings on an OS X desktop is the default font size used in many of the widgets. It's possible to fix this by calling widget->setAttribute(Qt::WA_MacSmallSize) (or similar) on the widgets in question, but it sucks to have to write that everywhere.

So today I decided to see if I could find a workaround using Qt's styling system. Unfortunately, I started to go cross-eyed rather quickly. However, I did eventually come up with a solution via another mechanism. It's a bit of a hack and time will tell whether or not it should remain in our application's code, but it might be useful to share it because it does at least work.

The first thing we need is a way to tell if a particular QWidget should have its Qt::WA_MacSmallSize attribute set:

// It's safe to call this function on any platform.
// It will only have an effect on the Mac.
void set_smaller_text_osx(QWidget *w)
{
    assert(w != 0);

    // By default, none of these size attributes are set.
    // If any has been set explicitly, we'll leave the widget alone.
    if (!w->testAttribute(Qt::WA_MacMiniSize) &&
        !w->testAttribute(Qt::WA_MacSmallSize) &&
        !w->testAttribute(Qt::WA_MacNormalSize) &&
        !w->testAttribute(Qt::WA_MacVariableSize))
    {
        // Is the widget is one of a number of types whose default
        // text size is too large?
        if (w->inherits("QLabel") ||
            w->inherits("QLineEdit") ||
            w->inherits("QComboBox") ||
            w->inherits("QCheckBox") ||
            w->inherits("QRadioButton") ||
            w->inherits("QAbstractItemView"))
            // Others could be added here...
        {
            // make the text the 'normal' size
            w->setAttribute(Qt::WA_MacSmallSize);
        }
    }
}

You might prefer to use dynamic_cast instead of QObject::inherits(). If you really wanted to go to town, you could create some kind of type registration system, but I don't think there's much to be gained from doing that.

Also, the list of widget types probably isn't exhaustive, so go ahead and add any others that you find wanting the same treatment. I've omitted QPushButton because it tends to have the larger text size most of the time in native user interfaces. QToolButton is also absent because its default font size is already small.

Now we can override Qt's global event delivery system in order to inject this function at opportune moments (did I mention this was a bit of a hack?).

To do this, we create a derived QApplication and reimplement the notify() virtual function:

class NativeLookingApplication : public QApplication
{
    public:
        NativeLookingApplication(int &argc, char **argv) :
            QApplication(argc, argv)
        {
        }

        virtual bool notify(QObject *receiver, QEvent *event)
        {
            if (event->type() == QEvent::Polish &&
                receiver && 
                receiver->isWidgetType())
            {
                set_smaller_text_osx(static_cast<QWidget *>(receiver));
            }

            return QApplication::notify(receiver, event);
        }
};

The polish event was chosen because it's specifically meant for delayed initialization. The event is delivered to each widget after it has been constructed but before it is displayed for the first time.

The normal way to hook in to a polish event is to override QWidget::event() in a derived class and look for QEvent::Polish in there. But hooking in to the global event delivery mechanism saves us from creating a whole bunch of derived QWidgets specifically for that purpose.

Now all that remains is to use a NativeLookingApplication instead of a QApplication and we're good to go.

A note on style sheets

Another method I tried was to install a global style sheet on the QApplication:

// Create a temporary label with the smaller font size so we can access the font
QLabel temp("");
temp.setAttribute(Qt::WA_MacSmallSize);

QFont small_font = temp.font();
const qreal pointsz = small_font.pointSizeF();
const int pixelsz = small_font.pixelSize(); 
const QString sz = (pointsz != -1) ? 
                   QString::number(pointsz) + "pt" :
                   QString::number(pixelsl) + "px";

QApplication app(argc, argv);

// You could probably use a more specific selector than this,
// but it illustrates the idea:
app.setStyleSheet("* { font-size: " + sz + "; }");

The down side to this approach is that in order to revert the font size of a particular widget object back to Qt::WA_MacNormalSize, one would have to install a custom mac-specifc stylesheet on that widget, which has to be knowledgeable of the point- or pixel-size corresponding to Qt::WA_MacNormalSize. It's do-able, but on balance I find the event delivery system a little cleaner.

Comments

anonymous

[12/04/2012 at 16:06:25]

I tried this fix and had problems that the popup menu of QComboBox had an incorrect size when opening. However using QEvent::PolishRequest as hook instead of QEvent::Polish fixed this issue.

anonymous

[03/01/2013 at 22:49:01]

You could also use qobject_cast instead of inherits or dynamic_cast

(optional)
(optional)
(required, hint)

Links can be added like [this one -> http://www.mr-edd.co.uk], to my homepage.
Phrases and blocks of code can be enclosed in {{{triple braces}}}.
Any HTML markup will be escaped.