【Qt源码笔记】简要说说 Qt5 中的 HighDPI 支持

2021-07-27 10:10:13 浏览数 (1)

想起之前在公司做的关于 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 中有两个方法:setPixelSizesetPointSize很多人对此不是很明白,为什么要设置这两个方法。这里便可以找到答案。设置字体的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 的代码,基本就在两部分中。

其中一部分在 qtbasesrcguikernel 目录下 qhighdpiscaling_p.hqhighdpiscaling.cpp这两个文件中的 QHighDpiScaling 类里。

代码语言:javascript复制
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 支持的命脉。其实里边的内容只是一些方法的简单包装。只看堆栈调用的话:

代码语言:javascript复制
>   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 的相关逻辑,这也是文档中要求要在构造之前开启开关是一致的,因为构造的时候就要检查这个属性的状态。

实际计算缩放因子的方法,应该是这个:

代码语言:javascript复制
qreal QHighDpiScaling::screenSubfactor(const QPlatformScreen *screen)

逻辑也是十分简单了。不用做过多解释。不过这里边有一个pixelDensity()的调用,内容还挺有意思的。

代码语言:javascript复制
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 就是这个问题。

上边说到,代码实现有两部分,另外一部分则是在 qtbasesrcwidgetsstyles 目录下的qstylehelper_p.hqstylehelper.cpp中的QStyleHelper命名空间中。

代码语言:javascript复制
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()这个方法:(这个方法在 qtbasesrcguitext 目录的qfont.cpp文件中)

代码语言:javascript复制
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 这个列表是有内容的。而这个列表第一次被添加的时机堆栈:

代码语言:javascript复制
>	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 过程中更改而成

代码语言:javascript复制
namespace
{
    qreal testa_width = QStyleHelper::dpiScaled(1);
}
static qreal testb_width = QStyleHelper::dpiScaled(1);

如果在代码的上方出现了这种,则它们就属于是一种比较可怕的代码,可以影响全局调用dpiScaled得不到正确结果。

总结

不过即使有一点点小瑕疵,但是 Qt 对 HighDPI 的实现,以及调用设计还是有很多值得借鉴之处的。本文也只是对 Qt HighDPI 支持比较简要的分析,还有很多细节,限于篇幅,并没有展开来说……

0 人点赞