我们程序里面定义了某个结构体(这里简单描述为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);
});