秤陷曲 发表于 2025-6-2 22:49:21

继承 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 图像对应的节点、样式、字体等信息都有对应的描述类。
https://img2024.cnblogs.com/blog/1596700/202504/1596700-20250407112353614-1714485844.png
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->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->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();
}该项目(https://github.com/lsyeei/dashboard)中线条的终端形状全部使用 SVG 文件进行定义,使用 EndTypeFactory 类作为终端形状的工厂类管理所有 svg 图形。EndTypeFactory 类加载 svg 图像时,通过 SvgPathDevice 类获取了图像的 PainterPath,保证了后续绘制过程中使用矢量图进行绘制。具体代码可以查看common目录下的 svgpathdevice 和 plugins/lineplugin 目录下的 endtypefactory 类文件。


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 继承 QPaintEngine 利用 QSvgRenderer 从SVG 图片中提取路径(QPainterPath)的方法