QFont-使用外部字体文件的问题
创始人
2024-02-25 04:58:16

        我们程序里面定义了某个结构体(这里简单描述为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.0double 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;elses << (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_DRAWif (qt_show_painter_debug_output)printf("QPainter::setFont(), family=%s, pointSize=%d\n", font.family().toLatin1().constData(), font.pointSize());
#endifif (!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 defif (! (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 caseif (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 defif (! (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 caseif (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.0double 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);
});

        

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...