Qt 在 Windows 下默认字体比较丑,但是我们有办法修改它

栏目: C++ · 发布时间: 7年前

版权声明: 本博客所有内容除特殊说明外,均系原创,允许转载,但需注明来源。

一直觉得 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 )。

我们看看修改后的效果。我保留了原版和修改过的版本,这样放在一起容易看出差别。可以看出,新版的字体比原来更加丰满圆润,我个人觉得顺眼多了。你觉得呢?

Qt 在 Windows 下默认字体比较丑,但是我们有办法修改它

Qt 在 Windows 下默认字体比较丑,但是我们有办法修改它

无关的吐槽:就在写作本文时,Visual Studio 2017 更新 15.8.7 再次搞坏了我的 Qt 构建,本来 15.8.6 还是完全没有问题的......我以为 VS2017 经过这么多更新应该已经很稳定了,没想到还是和 Win10 一个尿性。好吧,用回 VS2015,至少不会给我搞什么幺蛾子。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Code

Code

Charles Petzold / Microsoft Press / 2000-10-21 / USD 29.99

Paperback Edition What do flashlights, the British invasion, black cats, and seesaws have to do with computers? In CODE, they show us the ingenious ways we manipulate language and invent new means of ......一起来看看 《Code》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试