想起之前在公司做的关于 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
类里。
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 支持的命脉。其实里边的内容只是一些方法的简单包装。只看堆栈调用的话:
> 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 的相关逻辑,这也是文档中要求要在构造之前开启开关是一致的,因为构造的时候就要检查这个属性的状态。
实际计算缩放因子的方法,应该是这个:
qreal QHighDpiScaling::screenSubfactor(const QPlatformScreen *screen)
逻辑也是十分简单了。不用做过多解释。不过这里边有一个 pixelDensity()
的调用,内容还挺有意思的。
qreal QWindowsScreen::pixelDensity() const
{
// QTBUG-49195: Use logical DPI instead of physical DPI to calculate
// the pixel density since it is reflects the Windows UI scaling.
// High DPI auto scaling should be disabled when the user chooses
// small fonts on a High DPI monitor, resulting in lower logical DPI.
return qMax(1, qRound(logicalDpi().first / 96));
}
这里边的逻辑可以明显地看到,当我们在 Windows 系统下使用类似 125% 的缩放比例的时候,这里边计算到的缩放比例还是 1。然后去 Qt BugReport 看了一下。QTBUG-70721 就是这个问题。
二
上边说到,代码实现有两部分,另外一部分则是在 qtbase\src\widgets\styles 目录下的qstylehelper_p.h
、qstylehelper.cpp
中的QStyleHelper
命名空间中。
qreal dpiScaled(qreal value)
{
// On mac the DPI is always 72 so we should not scale it
return value;
static const qreal scale = qreal(qt_defaultDpiX()) / 96.0;
return value * scale;
}
如果在 Qt4 下有做过 HighDPI 的相关逻辑,想必对这个方法是不陌生的。至此基本上 Qt HighDPI 支持的代码逻辑基本找全。
小瑕疵
上边我提到过代码中的小瑕疵。就在上边那段代码上。不难看出这个scale
是一个函数中的静态变量,后续对这个函数再次调用已经不改变scale
的值了。
看到这里会觉得,大概是个隐患,然后再来看 qt_defaultDpiX()
这个方法:(这个方法在 qtbase\src\gui\text 目录的qfont.cpp
文件中)
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());
//PI has not been initialised, or it is being initialised. Give a default dpi
return 100;
}
看到这里也就只有第三个 if 会导致这个方法的返回值不确定。
那很自然的就会想到,如果当 dpiScaled
调用的时候第三个 if 不起作用,那将是可怕的结果。所以紧接着探究这个 screen
。这部分过程略过,直接说结论。screen 能正常取到的前提是 QGuiApplicationPrivate::screen_list
这个列表是有内容的。而这个列表第一次被添加的时机堆栈:
> 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 过程中更改而成
namespace
{
qreal testa_width = QStyleHelper::dpiScaled(1);
}
static qreal testb_width = QStyleHelper::dpiScaled(1);
如果在代码的上方出现了这种,则它们就属于是一种比较可怕的代码,可以影响全局调用dpiScaled
得不到正确结果。
总结
不过即使有一点点小瑕疵,但是 Qt 对 HighDPI 的实现,以及调用设计还是有很多值得借鉴之处的。本文也只是对 Qt HighDPI 支持比较简要的分析,还有很多细节,限于篇幅,并没有展开来说……