QFont-使用外部字体文件的问题

        我们程序里面定义了某个结构体(这里简单描述为AStruct),AStruct包含了一个QFont 类型的成员变量:

struct AStruct {
    QFont ft;
};

        在具体业务上,AStruct中的QFont会被传递给QPainter去绘制文本。           

        保存工程/加载工程时时,会对AStruct对象进行序列化/反序列化操作:

        
struct AStruct {
    QFont ft;

    QString serialize() {
        QByteArray buf;
		QDataStream in(&buf, QIODevice::WriteOnly);
		in << font;
        return buf.toBase64();
    }

    void deserialize(const QString& d) {
        QByteArray buf = QByteArray::fromBase64(d.toLatin1());
        QDataStream out(&buf, QIODevice::ReadOnly);
		out >> font;
    }
};

        正常情况下,这套序列化/反序列化以及QPainter绘制文本都没有什么问题。但是客户在自己机器上安装了一个新的字体文件后(字体X),问题来了:

        在AStruct的编辑界面,用户在QFontComboBox里面选择了X字体,QPainter绘制正常。然后保存工程(序列化AStruct),再重新打开工程(反序列化AStruct),QPainter绘制异常,没有使用X字体来绘制文本。

        当时的第一反应就是可能序列化或者反序列出问题了,到底哪里出问题了呢?

       先排查序列化吧!  由于在第一次在编辑界面对AStruct设置为X字体后,QPainter绘制是正确的,说明那一次QPainter使用的字体是正确的,所以我把QPainter中的字体获取出来后,使用和AStruct中同样的方法对QFont进行序列化,得到一个序列化后的字符串,再和我工程里面存储的字符串进行比较,发现发现二者在后面一段有差异(标红的部分):

  工程文件里面的:      AAAAEnm5U2tOZmzViExOZnuAT1MACv9ARAAAAAAAAP8FAAEAMhAAAAEAAAAAAAAAAAAAAAAAA=

 从QPainter的QFont序列化出来的:

AAAAEnm5U2tOZmzViExOZnuAT1MACv9ARAAAAAAAAP8FAAEAMhAAAAEAAAAAAAAAAAAAAAAAAQAAABJ5uVNrTmZs1YhMTmZ7gE9TAAo=

想要知道这部分数据存的到底是什么,只能看QFont的的源码了:

QDataStream &operator<<(QDataStream &s, const QFont &font)
{
    if (s.version() == 1) {
        s << font.d->request.family.toLatin1();
    } else {
        s << font.d->request.family;
        if (s.version() >= QDataStream::Qt_5_4)
            s << font.d->request.styleName;
    }

    if (s.version() >= QDataStream::Qt_4_0) {
        // 4.0
        double pointSize = font.d->request.pointSize;
        qint32 pixelSize = font.d->request.pixelSize;
        s << pointSize;
        s << pixelSize;
    } else if (s.version() <= 3) {
        qint16 pointSize = (qint16) (font.d->request.pointSize * 10);
        if (pointSize < 0) {
            pointSize = (qint16)QFontInfo(font).pointSize() * 10;
        }
        s << pointSize;
    } else {
        s << (qint16) (font.d->request.pointSize * 10);
        s << (qint16) font.d->request.pixelSize;
    }

    s << (quint8) font.d->request.styleHint;
    if (s.version() >= QDataStream::Qt_3_1) {
        // Continue writing 8 bits for versions < 5.4 so that we don't write too much,
        // even though we need 16 to store styleStrategy, so there is some data loss.
        if (s.version() >= QDataStream::Qt_5_4)
            s << (quint16) font.d->request.styleStrategy;
        else
            s << (quint8) font.d->request.styleStrategy;
    }
    s << (quint8) 0
      << (quint8) font.d->request.weight
      << get_font_bits(s.version(), font.d.data());
    if (s.version() >= QDataStream::Qt_4_3)
        s << (quint16)font.d->request.stretch;
    if (s.version() >= QDataStream::Qt_4_4)
        s << get_extended_font_bits(font.d.data());
    if (s.version() >= QDataStream::Qt_4_5) {
        s << font.d->letterSpacing.value();
        s << font.d->wordSpacing.value();
    }
    if (s.version() >= QDataStream::Qt_5_4)
        s << (quint8)font.d->request.hintingPreference;
    if (s.version() >= QDataStream::Qt_5_6)
        s << (quint8)font.d->capital;
    if (s.version() >= QDataStream::Qt_5_13)
        s << font.d->request.families;
    return s;
}

通过调试发现,序列化AStruct中的QFont和序列化QPainter中的QFont,差异就在上面这个函数的最后一个if:

 if (s.version() >= QDataStream::Qt_5_13)
        s << font.d->request.families;

AStruct序列化时,request.families为空,QPainter的QFont序列化时request.families不为空。

但是QPainter的字体明明是通过setFont()方法把AStruct的QFont设置进去的,怎么序列化就不一样了呢? 莫非是QPainter里面有偷偷摸摸干了啥? 一查代码,还真是:

/** QPainter::setFont() ***************************************/
void QPainter::setFont(const QFont &font)
{
    Q_D(QPainter);

#ifdef QT_DEBUG_DRAW
    if (qt_show_painter_debug_output)
        printf("QPainter::setFont(), family=%s, pointSize=%d\n", font.family().toLatin1().constData(), font.pointSize());
#endif

    if (!d->engine) {
        qWarning("QPainter::setFont: Painter not active");
        return;
    }

    d->state->font = QFont(font.resolve(d->state->deviceFont), device());
    if (!d->extended)
        d->state->dirtyFlags |= QPaintEngine::DirtyFont;
}

/** QFont::resolve() *****************************************/
QFont QFont::resolve(const QFont &other) const
{
    if (resolve_mask == 0 || (resolve_mask == other.resolve_mask && *this == other)) {
        QFont o(other);
        o.resolve_mask = resolve_mask;
        return o;
    }

    QFont font(*this);
    font.detach();
    font.d->resolve(resolve_mask, other.d.data());

    return font;
}

/** QFontPrivate::resolve() *************************/
void QFontPrivate::resolve(uint mask, const QFontPrivate *other)
{
    Q_ASSERT(other != nullptr);

    dpi = other->dpi;

    if ((mask & QFont::AllPropertiesResolved) == QFont::AllPropertiesResolved) return;

    // assign the unset-bits with the set-bits of the other font def
    if (! (mask & QFont::FamilyResolved))
        request.family = other->request.family;

    if (!(mask & QFont::FamiliesResolved)) {
        request.families = other->request.families;
        // Prepend the family explicitly set so it will be given
        // preference in this case
        if (mask & QFont::FamilyResolved)
            request.families.prepend(request.family);
    }

    if (! (mask & QFont::StyleNameResolved))
        request.styleName = other->request.styleName;

    if (! (mask & QFont::SizeResolved)) {
        request.pointSize = other->request.pointSize;
        request.pixelSize = other->request.pixelSize;
    }
    ..........................................
}

上面贴出了调用QPainter::setFont()时和字体相关的几个关键函数,调用时序为:

QPainter::setFont() -> QFont::resovle() ->QFontPrivate::resolve()

看QFontPrivate::resolve(),里面有这么一段:

if ((mask & QFont::AllPropertiesResolved) == QFont::AllPropertiesResolved) return;

    // assign the unset-bits with the set-bits of the other font def
    if (! (mask & QFont::FamilyResolved))
        request.family = other->request.family;

    if (!(mask & QFont::FamiliesResolved)) {
        request.families = other->request.families;
        // Prepend the family explicitly set so it will be given
        // preference in this case
        if (mask & QFont::FamilyResolved)
            request.families.prepend(request.family);
    }

说人话就是:

如果字体的属性掩码不是QFont::AllPropertiesResolved,那么就需要根据属性掩码对没有复制的属性进行复制。于是马上有了对FamiliesResolved属性的处理: 如果FamiliesResolved没有赋值,那么就用Family属性填充families。   

既然QPainter每次设置字体都会调用一遍QFont::resolve()来填充families字段,按理说重新打开工程之后,使用反序列化得到的QFont设置给QPainter时,也会自动填充families才对啊,为什么绘制就不对了呢?

注意上面对QFont的属性填充有个判断条件:

QFont-使用外部字体文件的问题_第1张图片

 也就是说当mask没有覆盖了所有字体属性时,才会进入到下面的逻辑。那么这个mask是如何赋值的呢?

mask的赋值有几种方式,一种是调用QFont的resolve()函数,一种是在QFont的几个构造函数中自动赋值,另一种则是QDataStream反序列化中赋值。我们要关心的正是QDataStream的反序列化:

QDataStream &operator>>(QDataStream &s, QFont &font)
{
    font.d = new QFontPrivate;
    font.resolve_mask = QFont::AllPropertiesResolved;

    quint8 styleHint, charSet, weight, bits;
    quint16 styleStrategy = QFont::PreferDefault;

    if (s.version() == 1) {
        QByteArray fam;
        s >> fam;
        font.d->request.family = QString::fromLatin1(fam);
    } else {
        s >> font.d->request.family;
        if (s.version() >= QDataStream::Qt_5_4)
            s >> font.d->request.styleName;
    }

    if (s.version() >= QDataStream::Qt_4_0) {
        // 4.0
        double pointSize;
        qint32 pixelSize;
        s >> pointSize;
        s >> pixelSize;
        font.d->request.pointSize = qreal(pointSize);
        font.d->request.pixelSize = pixelSize;
    } else {
        qint16 pointSize, pixelSize = -1;
        s >> pointSize;
        if (s.version() >= 4)
            s >> pixelSize;
        font.d->request.pointSize = qreal(pointSize / 10.);
        font.d->request.pixelSize = pixelSize;
    }
    s >> styleHint;
    if (s.version() >= QDataStream::Qt_3_1) {
        if (s.version() >= QDataStream::Qt_5_4) {
            s >> styleStrategy;
        } else {
            quint8 tempStyleStrategy;
            s >> tempStyleStrategy;
            styleStrategy = tempStyleStrategy;
        }
    }

    s >> charSet;
    s >> weight;
    s >> bits;

    font.d->request.styleHint = styleHint;
    font.d->request.styleStrategy = styleStrategy;
    font.d->request.weight = weight;

    set_font_bits(s.version(), bits, font.d.data());

    if (s.version() >= QDataStream::Qt_4_3) {
        quint16 stretch;
        s >> stretch;
        font.d->request.stretch = stretch;
    }

    if (s.version() >= QDataStream::Qt_4_4) {
        quint8 extendedBits;
        s >> extendedBits;
        set_extended_font_bits(extendedBits, font.d.data());
    }
    if (s.version() >= QDataStream::Qt_4_5) {
        int value;
        s >> value;
        font.d->letterSpacing.setValue(value);
        s >> value;
        font.d->wordSpacing.setValue(value);
    }
    if (s.version() >= QDataStream::Qt_5_4) {
        quint8 value;
        s >> value;
        font.d->request.hintingPreference = QFont::HintingPreference(value);
    }
    if (s.version() >= QDataStream::Qt_5_6) {
        quint8 value;
        s >> value;
        font.d->capital = QFont::Capitalization(value);
    }
    if (s.version() >= QDataStream::Qt_5_13) {
        QStringList value;
        s >> value;
        font.d->request.families = value;
    }
    return s;
}

看,这个函数第二行就把mask赋值成了font.resolve_mask = QFont::AllPropertiesResolved,但是又因为AStruct的QFont序列化时,request.families为空,所以在反序列化时没有任何数据解析出来。

至此,问题就比较明确了:

AStruct的QFont序列化时,families属性为空;反序列化时,families属性也为空,同时,QFont的mask被设置成了font.resolve_mask = QFont::AllPropertiesResolved;   于是当我将反序列化得到的AStruct的QFont设置给QPainter后,QPainter调用QFont::resolve()方法无法填充families。最后QPainter使用字体失败。

如何解决这个问题?

这个问题的根本原因在于AStruct中的QFont属性不完整,我们得想办法让他完整。其实QPainter已经给出了解决方案:调用QFont::resolve()来填充属性。 当然,调用resolve还有一些坑,这里我直接给出我的代码。很简单:

AStruct astru;
connect(ui.fontComboBox, &QFontComboBox::currentFontChanged, this, [this, astru](const QFont& ft) {

        /*有问题的写法: astru.font中的families属性为空 
        astru.font = ft;
        */

        /*修正后的写法: 通过调用resolve给families属性赋值*/
		astru.font = QFont(ft.family()).resolve(ft);
});

        

你可能感兴趣的:(QT,qt,java,开发语言)