版权声明: 本博客所有内容除特殊说明外,均系原创,允许转载,但需注明来源。
一直觉得 Qt 在 Windows 系统上的默认字体不太好看,不过自己写程序时自己去指定字体也很方便,就没怎么在意。这几个月专门用 Qt 写了一些程序,发现这还真的是个问题,因为包括官方的 Qt Creator 在内,都没有开放给用户自定义默认字体的设置,天天看着实在不怎么舒服。本来 Windows 系统是允许用户自定义桌面字体的,Win10 不知是出于去桌面化还是什么考虑,把这个功能又拿掉了。
当然,通过修改注册表还是可以修改系统默认字体的,而且我知道确实有这样的第三方工具。不过鉴于 Win10 对桌面系统日益后妈化的现实,这个接口说不定哪天也会被关掉,所以我个人并不怎么希望走这个路子。然而我也明白这确实是当前最简单、也不需要任何编程的手段。如果读者希望用这个方法的话,请自行搜索类似 Font Changer
之类的关键字,下面的文字就不需要再看了。用编程的方法则比较麻烦,需要自行修改一些代码,愿意自己动手的朋友请继续阅读。
要解决该问题,首先请阅读 QTBUG-58610 ,我也是找资料时偶然发现这条信息的。按照该 bug 的描述,该问题的基本原因在于 Qt
在获取字体时使用了教早的 Win32 函数 GetStockObject
,而较新的系统中应该使用 SystemParametersInfo
。看起来只是修改一个系统调用,似乎不难解决,但审核记录却显示修正会放到下一个主要版本(6.0)。这意味着即将到来的 LTS 版本(5.12)不会解决该问题。或许是出于审慎和兼容性考虑吧,不过我对这个结果是有点失望的。好在 Qt
是开源的,并且问题看起来也很简单,我决心自己看一下能不能自己修改源码来启用新的字体。
我使用的是当前 Qt
的最新正式版 5.11.2。首先声明,本文描述的方法是我自行尝试的,并未得到官方验证,虽然我自己已经作了测试,并分析过可能受影响的相关代码,自信还是比较可靠的,但并不保证 100% 没有问题,同时也仅在 Windows 做了测试,所以内容仅供参考,对 Qt
有自行编译经验的朋友不妨尝试。
简单查找了一下,对上述接口的调用主要在如下位置: qtbase\src\platformsupport\fontdatabases\windows\qwindowsfontdatabase.cpp
,文件中又有两处稍有不同的实现,首先是 QWindowsFontDatabase::systemFont
:
// ### fixme Qt 6 (QTBUG-58610): See comment at QWindowsFontDatabase::systemDefaultFont() HFONT QWindowsFontDatabase::systemFont() { static const HFONT stock_sysfont = (HFONT)GetStockObject(DEFAULT_GUI_FONT); return stock_sysfont; }
看起来 程序员 已经标记出问题,但并未修改。注释告诉我们还要注意 QWindowsFontDatabase::systemDefaultFont()
,那接下来就看看这个函数:
QFont QWindowsFontDatabase::systemDefaultFont() { #if QT_VERSION >= 0x060000 // Qt 6: Obtain default GUI font (typically "Segoe UI, 9pt", see QTBUG-58610) NONCLIENTMETRICS ncm; ncm.cbSize = FIELD_OFFSET(NONCLIENTMETRICS, lfMessageFont) + sizeof(LOGFONT); SystemParametersInfo(SPI_GETNONCLIENTMETRICS, ncm.cbSize , &ncm, 0); const QFont systemFont = QWindowsFontDatabase::LOGFONT_to_QFont(ncm.lfMessageFont); #else LOGFONT lf; GetObject(QWindowsFontDatabase::systemFont(), sizeof(lf), &lf); QFont systemFont = QWindowsFontDatabase::LOGFONT_to_QFont(lf); // "MS Shell Dlg 2" is the correct system font >= Win2k if (systemFont.family() == QLatin1String("MS Shell Dlg")) systemFont.setFamily(QStringLiteral("MS Shell Dlg 2")); // Qt 5 by (Qt 4) legacy uses GetStockObject(DEFAULT_GUI_FONT) to // obtain the default GUI font (typically "MS Shell Dlg 2, 8pt"). This has been // long deprecated; the message font of the NONCLIENTMETRICS structure obtained by // SystemParametersInfo(SPI_GETNONCLIENTMETRICS) should be used instead (see // QWindowsTheme::refreshFonts(), typically "Segoe UI, 9pt"), which is larger. #endif // Qt 5 qCDebug(lcQpaFonts) << __FUNCTION__ << systemFont; return systemFont; }
嗯,修改代码已经给出,只是标记为从 V6 开始启用。要启用的话,这里只要切换到新代码即可,但上面的 systemFont
呢?是简单的废弃掉了,还是有别处代码继续调用?
好在 HFONT
是 Windows 特定类型,考虑到代码结构,该类应该只是在内部作为平台实现,不太可能由其他代码直接调用,因此我们可以把查找范围限定在 platformsupport
内部,避免搜索整个 Qt
库。搜索一下可知,除了上面已经看到的 systemDefaultFont()
之外,调用 systemFont()
的尚有三处。仔细观察代码可知,这些调用主要为另一个名为 QWindowsFontEngine
的类提供字体。
这就有点麻烦了。熟悉 Win32 API 的朋友应该知道, GetStockObject
返回的对象,不论是否调用 DeleteObject
都不会有不良后果,所以你可以放心大胆的调用而无需担心 GDI 资源泄露。而 SystemParametersInfo
就不同了,自己创建的字体,必须保证在正确的时间释放,否则要么丢失字体,要么泄露句柄。为保证修改安全,我们不能只考虑前面两处地方,而必须考虑到调用者是否、以及应该在何处释放资源的问题。
qwindowsfontdatabase.cpp
中引用到 sytsemFont()
方法的代码有两处。一处就在前面的 systemDefaultFont()
调用中,不过我们已经看到了,如果切换到 V6 的实现,那么该调用将不会再起作用,我们可以安全地忽略它。另一处在 createEngine()
方法中:
QFontEngine *QWindowsFontDatabase::createEngine(const QFontDef &request, const QString &faceName, int dpi, const QSharedPointer<QWindowsFontEngineData> &data) { ... if (request.stretch != 100) { HFONT hfont = CreateFontIndirect(&lf); if (!hfont) { qErrnoWarning("%s: CreateFontIndirect failed", __FUNCTION__); hfont = QWindowsFontDatabase::systemFont(); } HGDIOBJ oldObj = SelectObject(data->hdc, hfont); TEXTMETRIC tm; if (!GetTextMetrics(data->hdc, &tm)) qErrnoWarning("%s: GetTextMetrics failed", __FUNCTION__); else lf.lfWidth = tm.tmAveCharWidth * request.stretch / 100; SelectObject(data->hdc, oldObj); DeleteObject(hfont); } ...
上述代码会在处理完毕后调用 DeleteObject
。很好,这段代码是安全的,我们不用管它了。接下来看 QWindowsFontEngine
中的调用。
首先看 qwindowsfontengine.cpp
中的调用点:对 systemFont()
的调用的两处分别在 QWindowsFontEngine
的构造函数和析构函数。这是一个好现象,说明生命周期非常明确,但我们仍然要了解该调用生成的对象是如何管理的。首先看构造函数:
QWindowsFontEngine::QWindowsFontEngine(const QString &name, LOGFONT lf, const QSharedPointer<QWindowsFontEngineData> &fontEngineData) : QFontEngine(Win), m_fontEngineData(fontEngineData), _name(name), m_logfont(lf), ttf(0), hasOutline(0) { qCDebug(lcQpaFonts) << __FUNCTION__ << name << lf.lfHeight; hfont = CreateFontIndirect(&m_logfont); if (!hfont) { qErrnoWarning("%s: CreateFontIndirect failed for family '%s'", __FUNCTION__, qPrintable(name)); hfont = QWindowsFontDatabase::systemFont(); } HDC hdc = m_fontEngineData->hdc; SelectObject(hdc, hfont); ...
可见, QWindowsFontEngine
是将 systemFont()
作为一种后备机制,只有 CreateFontIndirect
不成功的情况下才会调用它。至于首选字体是什么可以不用关心它,但这样字体的来源有两种可能,这给我们判断是否应该删除增加了一点困难。再看析构函数:
QWindowsFontEngine::~QWindowsFontEngine() { if (designAdvances) free(designAdvances); if (widthCache) free(widthCache); // make sure we aren't by accident still selected SelectObject(m_fontEngineData->hdc, QWindowsFontDatabase::systemFont()); if (!DeleteObject(hfont)) qErrnoWarning("%s: QFontEngineWin: failed to delete font...", __FUNCTION__); qCDebug(lcQpaFonts) << __FUNCTION__ << _name; ... }
这里的处理逻辑有点绕,因为 hfont
本来就可能来自 systemFont()
,结果在释放时再一次选择了该字体。这种处理原来是没有问题的(因为 GetStockObject
返回的对象不需要删除),但对新字体的实现明显就会有泄露的风险了。我们看到,析构函数后面会调用 DeleteObject
,所以不论构造函数是如何生成字体的,这里确实会释放,不必担心。那么问题就在于上面的 SelectObject
怎么办。思考一番后,我决定这样:把这样的 systemFont()
改成原来的实现( GetStockObject
) ,这样就无需担心泄露了。鉴于 QWindowsFontEngineData
仅在 QWindowsFontEngine
内部使用,其 hdc
在释放以后应该不会再用于 Font Engine,所以这样做应该是安全的。
方法已经考虑清楚,接下来就是实现了。首先找到 QWindowsFontDatabase::systemDefaultFont()
,强行开启 V6 分支判断:
QFont QWindowsFontDatabase::systemDefaultFont() { //{{HACK_BEGIN #if 1 // QT_VERSION >= 0x060000 //}}HACK_END
上述代码是我自己的习惯,便于以后查找修改的地方,因为 Qt 代码实在是太庞大了,为一点小小的修改就上版本控制的话会非常慢。当然修改之前把原来的代码备份一次是个好习惯。
然后修改 QWindowFontDatabase::systemFont()
,使用新的字体查找方法:
// ### fixme Qt 6 (QTBUG-58610): See comment at QWindowsFontDatabase::systemDefaultFont() HFONT QWindowsFontDatabase::systemFont() { //{{HACK_BEGIN // static const HFONT stock_sysfont = (HFONT)GetStockObject(DEFAULT_GUI_FONT); // return stock_sysfont; NONCLIENTMETRICS ncm; ncm.cbSize = FIELD_OFFSET(NONCLIENTMETRICS, lfMessageFont) + sizeof(LOGFONT); SystemParametersInfo(SPI_GETNONCLIENTMETRICS, ncm.cbSize , &ncm, 0); return CreateFontIndirect(&ncm.lfMessageFont); //}}HACK_END }
这里和原来的处理有一点差别。原来的代码将字体作为静态变量,估计是希望优化性能吧。但看过对应的实现,我们知道调用方通常会在结束之后调用 DeleteObject()
,所以现在用静态变量是不太现实的。对当代计算机来说 Font 对象只要不泄露,多调用几次应该不会造成明显的性能问题。
然后是 qwindowsfontengine.cpp
。我们只需要替换析构函数中的实现就可以了:
QWindowsFontEngine::~QWindowsFontEngine() { ... // make sure we aren't by accident still selected //{{HACK_BEGIN // SelectObject(m_fontEngineData->hdc, QWindowsFontDatabase::systemFont()); SelectObject(m_fontEngineData->hdc, (HFONT)GetStockObject(DEFAULT_GUI_FONT)); //}}HACK_END
程序到此修改完毕,我们可以重新编译 Qt, 然后走开去做点别的(需要很长时间)。
编译完成后,你可以用生成的 DLL 替换原来的版本,即可生效。其实严格说来,我们所作的只是一个很小的修改,单独复制下列文件即可: plugins/platforms/qwindows.dll
(调试版本为 qwindowsd.dll
)。
我们看看修改后的效果。我保留了原版和修改过的版本,这样放在一起容易看出差别。可以看出,新版的字体比原来更加丰满圆润,我个人觉得顺眼多了。你觉得呢?
无关的吐槽:就在写作本文时,Visual Studio 2017 更新 15.8.7 再次搞坏了我的 Qt 构建,本来 15.8.6 还是完全没有问题的......我以为 VS2017 经过这么多更新应该已经很稳定了,没想到还是和 Win10 一个尿性。好吧,用回 VS2015,至少不会给我搞什么幺蛾子。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- CSS 字体:字体特性
- iOS 自定义字体设置与系统自带的字体
- ReactNative字体大小不随系统字体大小变化而变化
- 再谈中文字体的子集化与动态创建字体
- 可爱气质的中文字体,字体视界法棍体-开源免费下载
- 深度字体安装器 V1.0 正式发布,打造个性化字体库
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。