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


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

    1. struct AStruct {
    2. QFont ft;
    3. };

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

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

    1. struct AStruct {
    2. QFont ft;
    3. QString serialize() {
    4. QByteArray buf;
    5. QDataStream in(&buf, QIODevice::WriteOnly);
    6. in << font;
    7. return buf.toBase64();
    8. }
    9. void deserialize(const QString& d) {
    10. QByteArray buf = QByteArray::fromBase64(d.toLatin1());
    11. QDataStream out(&buf, QIODevice::ReadOnly);
    12. out >> font;
    13. }
    14. };

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

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

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

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

      工程文件里面的:      AAAAEnm5U2tOZmzViExOZnuAT1MACv9ARAAAAAAAAP8FAAEAMhAAAAEAAAAAAAAAAAAAAAAAA=

     从QPainter的QFont序列化出来的:

    AAAAEnm5U2tOZmzViExOZnuAT1MACv9ARAAAAAAAAP8FAAEAMhAAAAEAAAAAAAAAAAAAAAAAAQAAABJ5uVNrTmZs1YhMTmZ7gE9TAAo=

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

    1. QDataStream &operator<<(QDataStream &s, const QFont &font)
    2. {
    3. if (s.version() == 1) {
    4. s << font.d->request.family.toLatin1();
    5. } else {
    6. s << font.d->request.family;
    7. if (s.version() >= QDataStream::Qt_5_4)
    8. s << font.d->request.styleName;
    9. }
    10. if (s.version() >= QDataStream::Qt_4_0) {
    11. // 4.0
    12. double pointSize = font.d->request.pointSize;
    13. qint32 pixelSize = font.d->request.pixelSize;
    14. s << pointSize;
    15. s << pixelSize;
    16. } else if (s.version() <= 3) {
    17. qint16 pointSize = (qint16) (font.d->request.pointSize * 10);
    18. if (pointSize < 0) {
    19. pointSize = (qint16)QFontInfo(font).pointSize() * 10;
    20. }
    21. s << pointSize;
    22. } else {
    23. s << (qint16) (font.d->request.pointSize * 10);
    24. s << (qint16) font.d->request.pixelSize;
    25. }
    26. s << (quint8) font.d->request.styleHint;
    27. if (s.version() >= QDataStream::Qt_3_1) {
    28. // Continue writing 8 bits for versions < 5.4 so that we don't write too much,
    29. // even though we need 16 to store styleStrategy, so there is some data loss.
    30. if (s.version() >= QDataStream::Qt_5_4)
    31. s << (quint16) font.d->request.styleStrategy;
    32. else
    33. s << (quint8) font.d->request.styleStrategy;
    34. }
    35. s << (quint8) 0
    36. << (quint8) font.d->request.weight
    37. << get_font_bits(s.version(), font.d.data());
    38. if (s.version() >= QDataStream::Qt_4_3)
    39. s << (quint16)font.d->request.stretch;
    40. if (s.version() >= QDataStream::Qt_4_4)
    41. s << get_extended_font_bits(font.d.data());
    42. if (s.version() >= QDataStream::Qt_4_5) {
    43. s << font.d->letterSpacing.value();
    44. s << font.d->wordSpacing.value();
    45. }
    46. if (s.version() >= QDataStream::Qt_5_4)
    47. s << (quint8)font.d->request.hintingPreference;
    48. if (s.version() >= QDataStream::Qt_5_6)
    49. s << (quint8)font.d->capital;
    50. if (s.version() >= QDataStream::Qt_5_13)
    51. s << font.d->request.families;
    52. return s;
    53. }

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

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

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

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

    1. /** QPainter::setFont() ***************************************/
    2. void QPainter::setFont(const QFont &font)
    3. {
    4. Q_D(QPainter);
    5. #ifdef QT_DEBUG_DRAW
    6. if (qt_show_painter_debug_output)
    7. printf("QPainter::setFont(), family=%s, pointSize=%d\n", font.family().toLatin1().constData(), font.pointSize());
    8. #endif
    9. if (!d->engine) {
    10. qWarning("QPainter::setFont: Painter not active");
    11. return;
    12. }
    13. d->state->font = QFont(font.resolve(d->state->deviceFont), device());
    14. if (!d->extended)
    15. d->state->dirtyFlags |= QPaintEngine::DirtyFont;
    16. }
    17. /** QFont::resolve() *****************************************/
    18. QFont QFont::resolve(const QFont &other) const
    19. {
    20. if (resolve_mask == 0 || (resolve_mask == other.resolve_mask && *this == other)) {
    21. QFont o(other);
    22. o.resolve_mask = resolve_mask;
    23. return o;
    24. }
    25. QFont font(*this);
    26. font.detach();
    27. font.d->resolve(resolve_mask, other.d.data());
    28. return font;
    29. }
    30. /** QFontPrivate::resolve() *************************/
    31. void QFontPrivate::resolve(uint mask, const QFontPrivate *other)
    32. {
    33. Q_ASSERT(other != nullptr);
    34. dpi = other->dpi;
    35. if ((mask & QFont::AllPropertiesResolved) == QFont::AllPropertiesResolved) return;
    36. // assign the unset-bits with the set-bits of the other font def
    37. if (! (mask & QFont::FamilyResolved))
    38. request.family = other->request.family;
    39. if (!(mask & QFont::FamiliesResolved)) {
    40. request.families = other->request.families;
    41. // Prepend the family explicitly set so it will be given
    42. // preference in this case
    43. if (mask & QFont::FamilyResolved)
    44. request.families.prepend(request.family);
    45. }
    46. if (! (mask & QFont::StyleNameResolved))
    47. request.styleName = other->request.styleName;
    48. if (! (mask & QFont::SizeResolved)) {
    49. request.pointSize = other->request.pointSize;
    50. request.pixelSize = other->request.pixelSize;
    51. }
    52. ..........................................
    53. }

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

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

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

    1. if ((mask & QFont::AllPropertiesResolved) == QFont::AllPropertiesResolved) return;
    2. // assign the unset-bits with the set-bits of the other font def
    3. if (! (mask & QFont::FamilyResolved))
    4. request.family = other->request.family;
    5. if (!(mask & QFont::FamiliesResolved)) {
    6. request.families = other->request.families;
    7. // Prepend the family explicitly set so it will be given
    8. // preference in this case
    9. if (mask & QFont::FamilyResolved)
    10. request.families.prepend(request.family);
    11. }

    说人话就是:

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

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

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

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

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

    1. QDataStream &operator>>(QDataStream &s, QFont &font)
    2. {
    3. font.d = new QFontPrivate;
    4. font.resolve_mask = QFont::AllPropertiesResolved;
    5. quint8 styleHint, charSet, weight, bits;
    6. quint16 styleStrategy = QFont::PreferDefault;
    7. if (s.version() == 1) {
    8. QByteArray fam;
    9. s >> fam;
    10. font.d->request.family = QString::fromLatin1(fam);
    11. } else {
    12. s >> font.d->request.family;
    13. if (s.version() >= QDataStream::Qt_5_4)
    14. s >> font.d->request.styleName;
    15. }
    16. if (s.version() >= QDataStream::Qt_4_0) {
    17. // 4.0
    18. double pointSize;
    19. qint32 pixelSize;
    20. s >> pointSize;
    21. s >> pixelSize;
    22. font.d->request.pointSize = qreal(pointSize);
    23. font.d->request.pixelSize = pixelSize;
    24. } else {
    25. qint16 pointSize, pixelSize = -1;
    26. s >> pointSize;
    27. if (s.version() >= 4)
    28. s >> pixelSize;
    29. font.d->request.pointSize = qreal(pointSize / 10.);
    30. font.d->request.pixelSize = pixelSize;
    31. }
    32. s >> styleHint;
    33. if (s.version() >= QDataStream::Qt_3_1) {
    34. if (s.version() >= QDataStream::Qt_5_4) {
    35. s >> styleStrategy;
    36. } else {
    37. quint8 tempStyleStrategy;
    38. s >> tempStyleStrategy;
    39. styleStrategy = tempStyleStrategy;
    40. }
    41. }
    42. s >> charSet;
    43. s >> weight;
    44. s >> bits;
    45. font.d->request.styleHint = styleHint;
    46. font.d->request.styleStrategy = styleStrategy;
    47. font.d->request.weight = weight;
    48. set_font_bits(s.version(), bits, font.d.data());
    49. if (s.version() >= QDataStream::Qt_4_3) {
    50. quint16 stretch;
    51. s >> stretch;
    52. font.d->request.stretch = stretch;
    53. }
    54. if (s.version() >= QDataStream::Qt_4_4) {
    55. quint8 extendedBits;
    56. s >> extendedBits;
    57. set_extended_font_bits(extendedBits, font.d.data());
    58. }
    59. if (s.version() >= QDataStream::Qt_4_5) {
    60. int value;
    61. s >> value;
    62. font.d->letterSpacing.setValue(value);
    63. s >> value;
    64. font.d->wordSpacing.setValue(value);
    65. }
    66. if (s.version() >= QDataStream::Qt_5_4) {
    67. quint8 value;
    68. s >> value;
    69. font.d->request.hintingPreference = QFont::HintingPreference(value);
    70. }
    71. if (s.version() >= QDataStream::Qt_5_6) {
    72. quint8 value;
    73. s >> value;
    74. font.d->capital = QFont::Capitalization(value);
    75. }
    76. if (s.version() >= QDataStream::Qt_5_13) {
    77. QStringList value;
    78. s >> value;
    79. font.d->request.families = value;
    80. }
    81. return s;
    82. }

    看,这个函数第二行就把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还有一些坑,这里我直接给出我的代码。很简单:

    1. AStruct astru;
    2. connect(ui.fontComboBox, &QFontComboBox::currentFontChanged, this, [this, astru](const QFont& ft) {
    3. /*有问题的写法: astru.font中的families属性为空
    4. astru.font = ft;
    5. */
    6. /*修正后的写法: 通过调用resolve给families属性赋值*/
    7. astru.font = QFont(ft.family()).resolve(ft);
    8. });

            

  • 相关阅读:
    网络标准之:永远是1.0版本的MIME
    带网络变压器的RJ45网口连接器/集成RJ45网口连接器
    Jenkins使用pipeline部署服务到远程服务器
    Linux ARM平台开发系列讲解(IIC) 2.7.1 IIC总线驱动框架分析
    【华为OD机试】删除目录
    基于springboot+vue的信息技术知识赛系统
    Linux驱动开发(十三)---USB驱动HID开发学习(鼠标)
    电销外呼系统哪家好用?客户说了算
    C++编程法则365天一天一条(323)main函数执行之前和之后的动作
    如何让6自由度双足机器人实现翻跟头的动作?
  • 原文地址:https://blog.csdn.net/wangw8507/article/details/128078247