继承 QPaintEngine 利用 QSvgRenderer 从SVG 图片中提取路径(QPainterPath)的方法

SVG 作为为可缩放矢量图形(Scalable Vector Graphics),易于编辑和维护,基于XML的文本文件存储,在网页设计、图标制作、数据可视化和其他图形相关的领域应用广泛。在应用工程中总会有动态修改路径、绘制颜色等需求,这就需要能够动态的解析 svg 文件,获取对应的路径、颜色等参数。有许多解析 svg 文件的类库,例如:svgHelper。svgHelper 通过 QDomDocument 对 svg 文件进行了解析并提取出了路径和颜色信息,虽然该方法可行,但是具体的解析过程还是相当繁琐,稍有不慎就会出现错误。另外,渐变色、画刷类型、字体、是否显示等属性该库就无法表示。下面通过对SVG库源码进行分析来找到一种通过 render() 方法就可以获取到解析后的路径信息。

QT 提供了svg 库用于svg 的解析,但是大部分代码被封装起来,只提供了 QSvgRenderer 类和 QSvgGenerator 类给用户使用。而 QSvgRenderer 类能使用的只有 render() 方法。从 svg 库的文件目录可以看出来,svg 图像对应的节点、样式、字体等信息都有对应的描述类。

QSvgRenderer 加载 svg 文件后,由 QSvgTinyDocument 类负责解析文件。QSvgTinyDocument 通过 QSvgHandler 执行具体的解析工作,QSvgTinyDocument 存储了解析后的 SVG 结构、样式等信息。

QSvgTinyDocument 提供了 size()、 width() 、height() 、viewBox()、QSvgNode *namedNode(const QString &id) const; QSvgPaintStyleProperty *namedStyle(const QString &id) const;等方法,可以获取 SVG 解析后的相关信息。但是这些方法都在私有头文件中隐藏了起来。

如果要使用 QT 的 svg 库只有通过 render() 方法,该方法的具体执行由 QSvgTinyDocument 类的 draw() 方法负责。

QSvgTinyDocument 类的 draw() 方法调用 QSvgNode 类的 draw() 方法绘制具体的 node,而 QSvgNode 类的 draw() 方法中通过虚函数 drawCommand() 将具体的图形绘制工作交给了其子类。QSvgNode 子类定义在 qsvggraphics_p.h 文件中,其定义了 SVG 支持的各种图形、路径、动画等的描述类,这些类都继承自 QSvgNode 并实现了虚函数 drawCommand(QPainter *p, QSvgExtraStates &states) 定义了对应图像如何在 QPainter 上绘制。

复制代码
void QSvgTinyDocument::draw(QPainter *p, const QString &id,
                            const QRectF &bounds)
{
    QSvgNode *node = scopeNode(id);

    if (!node) {
        qCDebug(lcSvgHandler, "Couldn't find node %s. Skipping rendering.", qPrintable(id));
        return;
    }
    if (m_time == 0)
        m_time = QDateTime::currentMSecsSinceEpoch();

    if (node->displayMode() == QSvgNode::NoneMode)
        return;

    p->save();

    const QRectF elementBounds = node->transformedBounds();

    mapSourceToTarget(p, bounds, elementBounds);
    QTransform originalTransform = p->worldTransform();

    //XXX set default style on the painter
    QPen pen(Qt::NoBrush, 1, Qt::SolidLine, Qt::FlatCap, Qt::SvgMiterJoin);
    pen.setMiterLimit(4);
    p->setPen(pen);
    p->setBrush(Qt::black);
    p->setRenderHint(QPainter::Antialiasing);
    p->setRenderHint(QPainter::SmoothPixmapTransform);

    QStack<QSvgNode*> parentApplyStack;
    QSvgNode *parent = node->parent();
    while (parent) {
        parentApplyStack.push(parent);
        parent = parent->parent();
    }

    for (int i = parentApplyStack.size() - 1; i >= 0; --i)
        parentApplyStack[i]->applyStyle(p, m_states);

    // Reset the world transform so that our parents don't affect
    // the position
    QTransform currentTransform = p->worldTransform();
    p->setWorldTransform(originalTransform);
    // 此处调用节点的 draw() 方法
    node->draw(p, m_states);

    p->setWorldTransform(currentTransform);

    for (int i = 0; i < parentApplyStack.size(); ++i)
        parentApplyStack[i]->revertStyle(p, m_states);

    //p->fillRect(bounds.adjusted(-5, -5, 5, 5), QColor(0, 0, 255, 100));

    p->restore();
}

drawCommand 方法通过 QPainter 提供的绘制函数进行图形绘制。QPainter 的绘制动作由 QPaintDevice 通过 QPaintEngine 实现。

通过以上分析可知,如果实现一个 SVG 的绘制引擎,那么所有的绘制动作都可以被该引擎截获并重新解释。GitHub项目 Compelling Data Designer 通过继承 QPaintEngine 实现了 SVG 的绘制引擎,通过 QSvgRenderer 的 render()方法重新获取了 QPainterPath 及其填充、线条等属性。

复制代码
// svgpathdevice.h
class SvgEngine : public QPaintEngine
{
public:
    SvgEngine();
    QList<PainterPathEx> getSvgPath();
    // QPaintEngine interface
    bool begin(QPaintDevice *pdev) override;
    bool end() override;
    void updateState(const QPaintEngineState &state) override;
    void drawPath(const QPainterPath &path) override;
    void drawPolygon(const QPointF *points, int pointCount, PolygonDrawMode mode) override;
    void drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr) override;
    void drawTextItem(const QPointF &p, const QTextItem &textItem) override;
    QPaintEngine::Type type() const override;
private:
    QList<PainterPathEx> pathList;

};

class SvgPathDevice : public QPaintDevice
{
public:

    SvgPathDevice(int w, int h);
    SvgPathDevice(const QSize &size);
    ~SvgPathDevice();
    QList<PainterPathEx> getSvgPath();

    qreal devicePixelRatio() const;
    void setDevicePixelRatio(qreal scaleFactor);
    // QPaintDevice interface
    QPaintEngine *paintEngine() const override;
    int devType() const override;

protected:
    int metric(PaintDeviceMetric metric) const override;
private:
    SvgEngine *engine;
    qreal pixelRatio{1};
    int width;
    int height;
    int qt_defaultDpiX() const;
    int qt_defaultDpiY() const;
};

// endtypefactory.cpp
QList<PainterPathEx> EndTypeFactory::extractPath(QSvgRenderer *render, QString id)
{
    if (!render->elementExists(id)) {
        return QList<PainterPathEx>();
    }
    render->setAspectRatioMode(Qt::KeepAspectRatio);
    auto size = render->defaultSize();
    SvgPathDevice svgPath(size);
    QPainter p(&svgPath);
    render->render(&p, id, QRectF{0, 0, size.width()*1.0, size.height()*1.0});
    p.end();
    return svgPath.getSvgPath();
}

该项目中线条的终端形状全部使用 SVG 文件进行定义,使用 EndTypeFactory 类作为终端形状的工厂类管理所有 svg 图形。EndTypeFactory 类加载 svg 图像时,通过 SvgPathDevice 类获取了图像的 PainterPath,保证了后续绘制过程中使用矢量图进行绘制。具体代码可以查看 plugins/lineplugin 目录下的 svgpathdevice 和 endtypefactory 类文件。

相关推荐
小小龙学IT2 小时前
FlatBuffers 深度解析:Google 开源零拷贝序列化库完全指南
c++·开源
wuminyu3 小时前
JDK21中虚拟线程和FFM协同实现高并发源码剖析
java·linux·c语言·jvm·c++
SWAGGY..3 小时前
【C++进阶】:(1)继承机制详解
java·jvm·c++
听取WA声一片(无恶意)3 小时前
CSP-J/CSP-S 深度优先搜索(DFS)完全讲义
c++·算法·深度优先
码匠许师傅3 小时前
【C++ 面试真题】27. 聊聊 C++ 的内存泄漏与内存布局
java·c++·面试
加油码5 小时前
动静态库的链接底层原理(进阶版)
linux·c++
一木 之林5 小时前
三、内存管理、指针与堆栈
c语言·jvm·c++
程与留5 小时前
06_Qt 常用数据类型与容器:QString、QList、QVector、QMap 的性能与实现分析
数据结构·qt
初夏睡觉6 小时前
原创题目 - 空壳恋爱
c++·算法·dp·oi·信息学·原创题目·值得挑战的题目
间歇性努力持续性发呆的野生快乐选手6 小时前
stl容器使用和算法简介
数据结构·c++