想起之前在公司做的关于 HighDPI 的适配,在 Qt4 下可以说是比较繁琐,代码敲到手疼。早就听说 Qt5.6 开始支持了 HighDPI ,一直没机会看详细的代码。一直到开始做 Gal ,才刚好在 Qt5 下需要 HighDPI 支持。用过之后,真的感叹,用起来太方便了。故看了一下详细实现。不过比较遗憾的是代码中有一个小瑕疵。
使用 其实想得到 Qt 给予的 HighDPI 支持,是非常之简单。只要在 QApplication
构造之前,开启 Qt::AA_EnableHighDpiScaling
这个属性。其实在代码中使用这个属性,等于环境中开启 QT_AUTO_SCREEN_SCALE_FACTOR
环境变量。还有另外的环境变量支持其他的 HighDPI 功能。这个参考文档即可
这里有一个小 tip :HighDPI 只是是根据显示器的像素密度来调整大小。在 Qt 中,用过 QFont
的人都会知道。QFont
中有两个方法:setPixelSize
、setPointSize
很多人对此不是很明白,为什么要设置这两个方法。这里便可以找到答案。设置字体的Pixel Size ,则会根据显示器的像素密度去改变字体大小;而设置字体的Point Size 则不会更改,因为Point Size 是基于显示器的物理单元。
关于 HighDPI ,一个比较良好的代码习惯,其实在 Qt 的 HighDPI 文档部分中有提到:
Always use the qreal versions of the QPainter drawing API.
Size windows and dialogs in relation to the screen size.
Replace hard-coded sizes in layouts and drawing code by values calculated from font metrics or screen size.
总而言之,使用的时候只要一个开关即可开启 HighDPI 支持,这一点让我还是十分好奇的。迫不及待地翻看了源码。
代码实现 其实关于 HighDPI 的代码,基本就在两部分中。
一 其中一部分在 qtbase\src\gui\kernel 目录下 qhighdpiscaling_p.h
、qhighdpiscaling.cpp
这两个文件中的 QHighDpiScaling
类里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Q_GUI_EXPORT QHighDpiScaling {public : static void initHighDpiScaling () ; static void updateHighDpiScaling () ; static void setGlobalFactor (qreal factor) ; static void setScreenFactor (QScreen *window, qreal factor) ; static bool isActive () { return m_active; } static qreal factor (const QWindow *window) ; static qreal factor (const QScreen *screen) ; static qreal factor (const QPlatformScreen *platformScreen) ; static QPoint origin (const QScreen *screen) ; static QPoint origin (const QPlatformScreen *platformScreen) ; static QPoint mapPositionFromNative (const QPoint &pos, const QPlatformScreen *platformScreen) ; static QPoint mapPositionToNative (const QPoint &pos, const QPlatformScreen *platformScreen) ; static QDpi logicalDpi () ; private : static qreal screenSubfactor (const QPlatformScreen *screen) ; static qreal m_factor; static bool m_active; static bool m_usePixelDensity; static bool m_globalScalingActive; static bool m_pixelDensityScalingActive; static bool m_screenFactorSet; static QDpi m_logicalDpi; };
这个类最大的特色可以说就是纯静态的了。不过按照逻辑来说,也是合理的。其中 initHighDpiScaling()
、updateHighDpiScaling()
可以说是两个比较重要的方法了,这两个方法掌管着整个 HighDPI 支持的命脉。其实里边的内容只是一些方法的简单包装。只看堆栈调用的话:
1 2 3 4 5 6 7 8 > Qt5Guid.dll!QHighDpiScaling::initHighDpiScaling() 行 254 C++ Qt5Guid.dll!QGuiApplicationPrivate::createPlatformIntegration() 行 1301 C++ Qt5Guid.dll!QGuiApplicationPrivate::createEventDispatcher() 行 1403 C++ Qt5Widgetsd.dll!QApplicationPrivate::createEventDispatcher() 行 187 C++ Qt5Cored.dll!QCoreApplicationPrivate::init() 行 859 C++ Qt5Guid.dll!QGuiApplicationPrivate::init() 行 1431 C++ Qt5Widgetsd.dll!QApplicationPrivate::init() 行 569 C++ Qt5Widgetsd.dll!QApplication::QApplication(int & argc, char * * argv, int _internal) 行 556 C++
可以看出,在 QApplication
构造的时候,会走入 HighDPI 的相关逻辑,这也是文档中要求要在构造之前开启开关是一致的,因为构造的时候就要检查这个属性的状态。
实际计算缩放因子的方法,应该是这个:
1 qreal QHighDpiScaling::screenSubfactor (const QPlatformScreen *screen)
逻辑也是十分简单了。不用做过多解释。不过这里边有一个pixelDensity()
的调用,内容还挺有意思的。
1 2 3 4 5 6 7 8 qreal QWindowsScreen::pixelDensity () const { return qMax (1 , qRound (logicalDpi ().first / 96 )); }
这里边的逻辑可以明显地看到,当我们在 Windows 系统下使用类似 125% 的缩放比例的时候,这里边计算到的缩放比例还是 1。然后去 Qt BugReport 看了一下。QTBUG-70721 就是这个问题。
二 上边说到,代码实现有两部分,另外一部分则是在 qtbase\src\widgets\styles 目录下的qstylehelper_p.h
、qstylehelper.cpp
中的QStyleHelper
命名空间中。
1 2 3 4 5 6 7 8 9 10 qreal dpiScaled (qreal value) {#ifdef Q_OS_MAC return value; #else static const qreal scale = qreal (qt_defaultDpiX ()) / 96.0 ; return value * scale; #endif }
如果在 Qt4 下有做过 HighDPI 的相关逻辑,想必对这个方法是不陌生的。至此基本上 Qt HighDPI 支持的代码逻辑基本找全。
小瑕疵 上边我提到过代码中的小瑕疵。就在上边那段代码上。不难看出这个scale
是一个函数中的静态变量,后续对这个函数再次调用已经不改变scale
的值了。
看到这里会觉得,大概是个隐患,然后再来看qt_defaultDpiX()
这个方法:(这个方法在 qtbase\src\gui\text 目录的qfont.cpp
文件中)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Q_GUI_EXPORT int qt_defaultDpiX () { if (QCoreApplication::instance ()->testAttribute (Qt::AA_Use96Dpi)) return 96 ; if (!qt_is_gui_used) return 75 ; if (const QScreen *screen = QGuiApplication::primaryScreen ()) return qRound (screen->logicalDotsPerInchX ()); return 100 ; }
看到这里也就只有第三个 if 会导致这个方法的返回值不确定。
那很自然的就会想到,如果当 dpiScaled
调用的时候第三个 if 不起作用,那将是可怕的结果。所以紧接着探究这个 screen
。这部分过程略过,直接说结论。screen 能正常取到的前提是 QGuiApplicationPrivate::screen_list
这个列表是有内容的。而这个列表第一次被添加的时机堆栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 > Qt5Guid.dll!QPlatformIntegration::screenAdded(QPlatformScreen * ps, bool isPrimary) 行 478 C++ qwindowsd.dll!QWindowsIntegration::emitScreenAdded(QPlatformScreen * s, bool isPrimary) 行 110 C++ qwindowsd.dll!QWindowsScreenManager::handleScreenChanges() 行 546 C++ qwindowsd.dll!QWindowsIntegration::QWindowsIntegration(const QStringList & paramList) 行 276 C++ qwindowsd.dll!QWindowsGdiIntegration::QWindowsGdiIntegration(const QStringList & paramList) 行 59 C++ qwindowsd.dll!QWindowsIntegrationPlugin::create(const QString & system, const QStringList & paramList, int & __formal, char * * __formal) 行 114 C++ Qt5Guid.dll!qLoadPlugin<QPlatformIntegration,QPlatformIntegrationPlugin,QStringList const & __ptr64,int & __ptr64,char * __ptr64 * __ptr64 & __ptr64>(const QFactoryLoader * loader, const QString & key, const QStringList & <args_0>, int & <args_1>, char * * & <args_2>) 行 108 C++ Qt5Guid.dll!QPlatformIntegrationFactory::create(const QString & platform, const QStringList & paramList, int & argc, char * * argv, const QString & platformPluginPath) 行 72 C++ Qt5Guid.dll!init_platform(const QString & pluginNamesWithArguments, const QString & platformPluginPath, const QString & platformThemeName, int & argc, char * * argv) 行 1179 C++ Qt5Guid.dll!QGuiApplicationPrivate::createPlatformIntegration() 行 1383 C++ Qt5Guid.dll!QGuiApplicationPrivate::createEventDispatcher() 行 1403 C++ Qt5Widgetsd.dll!QApplicationPrivate::createEventDispatcher() 行 187 C++ Qt5Cored.dll!QCoreApplicationPrivate::init() 行 859 C++ Qt5Guid.dll!QGuiApplicationPrivate::init() 行 1431 C++ Qt5Widgetsd.dll!QApplicationPrivate::init() 行 569 C++ Qt5Widgetsd.dll!QApplication::QApplication(int & argc, char * * argv, int _internal) 行 556 C++
从这里可以看到,是在 QApplication
构造的时候。
所以可以得出一个结论,当在QApplication
构造的之前调用QStyleHelper::dpiScaled
得到的结果则可能不是准确的,也会导致,在以后得到结果都是错误的。没有经验的人也许会觉得在QApplication
构造之前调用这个是没意义的,所以认为这个调用并不常见。此处我举一例以供参考。
很多人习惯提前定义一些比较固定的量,在某个 cpp 中,也许我们能看到这样一种代码,它有可能是直接写成,也有可能在实现 HighDPI 过程中更改而成
1 2 3 4 5 namespace { qreal testa_width = QStyleHelper::dpiScaled (1 ); } static qreal testb_width = QStyleHelper::dpiScaled (1 );
如果在代码的上方出现了这种,则它们就属于是一种比较可怕的代码,可以影响全局调用dpiScaled
得不到正确结果。
总结 不过即使有一点点小瑕疵,但是 Qt 对 HighDPI 的实现,以及调用设计还是有很多值得借鉴之处的。本文也只是对 Qt HighDPI 支持比较简要的分析,还有很多细节,限于篇幅,并没有展开来说……