我们程序里面定义了某个结构体(这里简单描述为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的属性填充有个判断条件:

也就是说当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);
- });