Рисуем карту сервисов при помощи Qt Quick и GraphViz

от автора

Решил запрототипировать два представления в дополнение к стандартному Jaeger UI. Это

  • построение карты сервисов по трейсу;

  • просмотрщик логов без пиксельхантинга и разворачивания спанов.

Для Qt Widgets есть обертка в виде nbergont/qgv, а хочется сделать на Qt Quick.

Как это выглядит со стороны Qt Quick:

Flickable {     topMargin: 80     leftMargin: 80     bottomMargin: 80     rightMargin: 80      contentWidth: svcMap.width     contentHeight: svcMap.height      ServiceMap {         id: svcMap         visible: true                  graph: item.graph         delegate: Rectangle {             implicitHeight: content.height + 10             implicitWidth: content.width + 10              visible: true             border.color: "black"             border.width: 1              ColumnLayout {                 id: content                 x: 5                 y: 5                  Text {                     text: node.name                     Layout.minimumWidth: 100                     font.bold: true                 }                  Rectangle {                     visible: node.hasEdges                     height: 1                     width: 10                     Layout.fillWidth: true                     color: "gray"                 }                  Repeater {                     model: node.operations                      Text {                         text: modelData                     }                 }             }             MouseArea {                 anchors.fill: parent                 onClicked: {                     nodeItem.setNode(node);                 }             }         }     } }

ServiceMap помещаем во Flickable на случай если граф не влезет в отображаемые границы. Делегат вычисляет размер исходя из содержимого, которое зависит от переданного свойства node.На стороне С++ следующая последовательность шагов:

  • пробежаться по вершинам и ребрам графа, создать QQuickItem со свойством node, для получения размера;

  • пробежаться по вершинам и ребрам графа, создать Agnode_t, Agedge_t в GraphViz, настроить параметры отображения;

  • выполнить расчет графа в GraphViz;

  • выставить вершинам и ребрам рассчитанные параметры.

Интерфейс ServiceMap, для делегата используется тип QQmlComponent:

struct ServiceMapCtx;  class ServiceMap : public QQuickItem {     Q_OBJECT     Q_PROPERTY(TraceGraph graph READ getGraph WRITE setGraph NOTIFY notifyGraphChanged)     Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY notifyDelegateChanged) public:     static constexpr qreal DPI = 72.0; //https://graphviz.org/doc/info/attrs.html      explicit ServiceMap(QQuickItem *parent = nullptr);     ~ServiceMap();      const TraceGraph &getGraph() const;     void setGraph(const TraceGraph &data);      QQmlComponent *delegate() const;     void setDelegate(QQmlComponent *delegate);  signals:      void notifyGraphChanged();     void notifyDelegateChanged();  private:     void makeServiceGraph();     void makeQuickNodes();     void computeLayout();     void resetGraph();  private:     std::unique_ptr<ServiceMapCtx> m_ctx;     TraceGraph m_trace;     QQmlComponent *m_delegate;      QVector<ServiceMapNode> m_nodes;     QVector<ServiceMapEdge> m_edges; };

Немножко хелперов, GraphViz использует строки, для настройки свойств:

namespace { struct ContextDeleter {     void operator()(GVC_t *ctx) const     {         gvFinalize(ctx);         if (gvFreeContext(ctx) != 0) {             qWarning() << "gvFreeContext != 0";         }     } };  struct GraphDeleter {     void operator()(Agraph_t *graph) const     {         if (agclose(graph) != 0) {             qWarning() << "agclose != 0";         }     } };  using GVContextPtr = std::unique_ptr<GVC_t, ContextDeleter>; using GVGraphPtr = std::unique_ptr<Agraph_t, GraphDeleter>;  template<typename NodeType> void setAttribute(NodeType *node, const QString &key, const QString &value) {     char empty[] = "";      auto k = key.toLatin1();     auto v = value.toLatin1();     agsafeset(node, k.data(), v.data(), empty); }   } // namespace //... struct ServiceMapCtx {     ServiceMapCtx()         : ctx(gvContext())         , graph(agopen("service_map", Agdirected, NULL))     {         setGraphAttribute("label", "service map");          setGraphAttribute("rankdir", "LR");         setGraphAttribute("nodesep", "0.5");         //setGraphAttribute("splines", "ortho");          setNodeAttribute("shape", "box");         setEdgeAttribute("minlen", "3");     }      ~ServiceMapCtx() { gvFreeLayout(ctx.get(), graph.get()); }      void setNodeAttribute(const QString &name, const QString &value)     {         if (graph) {             agattr(graph.get(), AGNODE, name.toLocal8Bit().data(), value.toLocal8Bit().data());         }     }      void setGraphAttribute(const QString &name, const QString &value)     {         if (graph) {             agattr(graph.get(), AGRAPH, name.toLocal8Bit().data(), value.toLocal8Bit().data());         }     }      void setEdgeAttribute(const QString &name, const QString &value)     {         if (graph) {             agattr(graph.get(), AGEDGE, name.toLocal8Bit().data(), value.toLocal8Bit().data());         }     }      GVContextPtr ctx;     GVGraphPtr graph; };  ServiceMap::ServiceMap(QQuickItem *parent)     : QQuickItem(parent)     , m_ctx(std::make_unique<ServiceMapCtx>())     , m_delegate(nullptr) {     setFlag(QQuickItem::ItemHasContents); }  ServiceMap::~ServiceMap() {}

Для визуальных QQuickItem нужно задавать флаг QQuickItem::ItemHasContents в true, означает что item имеет детей, которых нужно отрисовывать. Создаем визуальные элементы вершин и ребер:

void ServiceMap::makeQuickNodes() {     for (auto &node : m_nodes) {         auto creationCtx = m_delegate->creationContext();         auto ctx = new QQmlContext(creationCtx ? creationCtx : qmlContext(this));          auto item = m_delegate->beginCreate(ctx);         if (item) {             ctx->setContextProperty("node", QVariant::fromValue(node));              auto quickItem = qobject_cast<QQuickItem *>(item);              quickItem->setParentItem(this);             quickItem->setZ(1);             node.qmlObject = quickItem;         } else {             qCritical() << "failed create QQuickItem from delegate" << m_delegate->errors();         }         m_delegate->completeCreate();     }      for (auto &edge : m_edges) {         edge.qmlObject = new EdgeItem;         edge.qmlObject->setZ(2);         edge.qmlObject->setParentItem(this);     } }

Вершина создается в несколько этапов через beginCreate/completeCreate, для установки свойства node. Создаем вершины и узлы в GraphViz, длины задаются в дюймах, при этом зашито 72 DPI:

void applySize(ServiceMapNode &node, qreal DPI) {     if (node.qmlObject) {         auto size = node.qmlObject->size();         auto widthIn = QString::number(qreal(size.width()) / DPI);         auto heightIn = QString::number(qreal(size.height()) / DPI);         setAttribute(node.gvNode, "width", widthIn);         setAttribute(node.gvNode, "height", heightIn);         setAttribute(node.gvNode, "fixedsize", "true");     } }  //... auto graph = m_ctx->graph.get(); QHash<graph::Process *, Agnode_t *> nodeMap; for (auto &node : m_nodes) {     node.gvNode = agnode(graph, NULL, true);     nodeMap.insert(node.process, node.gvNode);     applySize(node, DPI); }  for (auto &edge : m_edges) {     auto from = nodeMap[edge.from];     auto to = nodeMap[edge.to];     edge.gvEdge = agedge(graph, from, to, NULL, TRUE); }

Сам расчет графа осуществляется вызовом функции gvLayout из gvc.h(libgvc). В этой библиотеке содержатся функции расчета и рендеринга изображения:

if (gvLayout(m_ctx->ctx.get(), graph, "dot") != 0) {     qCritical() << "Layout render error" << agerrors() << QString::fromLocal8Bit(aglasterr()); }

Выставляем размер ServiceMap, где UR это координаты верхнего правого угла typedef struct { pointf LL, UR; } boxf:

qreal gvGraphHeight = GD_bb(graph).UR.y; qreal gvGraphWidth = GD_bb(graph).UR.x; setImplicitHeight(gvGraphHeight); setImplicitWidth(gvGraphWidth);

Дальше нужно расставить вершины QQuickItem по координатам, который рассчитал GraphViz. В GraphViz ось Y идет снизу вверх, а в Qt сверху в низ. А координата вершины GraphViz находится в центре фигуры. Поэтому разворачиваем и смещаем координаты:

QPointF centerToOrigin(const QPointF &p, qreal width, qreal height) {     return QPointF(p.x() - width / 2, p.y() - height / 2); } //... for (auto &node : m_nodes) {     auto gvPos = ND_coord(node.gvNode);     QPoint pt(gvPos.x, gvGraphHeight - gvPos.y);     auto org = centerToOrigin(pt, node.qmlObject->width(), node.qmlObject->height());     node.qmlObject->setPosition(org); }

С ребрами немного сложнее. Вытаскиваем точки, по которым будем рисовать линию:

for (auto &edge : m_edges) {     auto spline = ED_spl(edge.gvEdge);     QVector<QPointF> points;      if (spline->size != 0) {         bezier bez = spline->list[0];         points.reserve(bez.size);          for (int i = 0; i < bez.size; ++i) {             auto &p = bez.list[i];             points << QPointF(p.x, gvGraphHeight - p.y);         }         points << QPointF(spline->list->ep.x, gvGraphHeight - spline->list->ep.y);     }      edge.qmlObject->setPoints(points);

Осталось нарисовать объект с необычной геометрией. В документации Qt Quick Examples and Tutorials есть интересующие нас вещи. От туда потребуется примеры работы с графом сцены(Scene Graph), из которого возьмем два примера Graph и Custom Geometry. Т.к. это прототип, то для ребер написал код на выброс:

class EdgeItem : public QQuickItem {     Q_OBJECT     QML_ELEMENT public:     explicit EdgeItem(QQuickItem *parent = nullptr);     QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override;      void setPoints(const QVector<QPointF> &points);  private:     QVector<QPointF> m_points;     QSGGeometryNode *m_arrowNode; }; ///... EdgeItem::EdgeItem(QQuickItem *parent)     : QQuickItem(parent)     , m_arrowNode(nullptr) {     setFlag(ItemHasContents); }  QSGNode *EdgeItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {     if (!m_arrowNode) {         m_arrowNode = new QSGGeometryNode;         auto geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 3);         geometry->setLineWidth(1);         geometry->setDrawingMode(QSGGeometry::DrawTriangles);         m_arrowNode->setGeometry(geometry);         m_arrowNode->setFlag(QSGNode::OwnsGeometry);         auto *material = new QSGFlatColorMaterial;         material->setColor(QColor("black"));         m_arrowNode->setMaterial(material);         m_arrowNode->setFlag(QSGNode::OwnsMaterial);         geometry->allocate(3);     }      QSGGeometryNode *node = nullptr;     QSGGeometry *geometry = nullptr;      if (!oldNode) {         node = new QSGGeometryNode;         geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(),                                    std::max(m_points.size() - 1, qsizetype(0)));         geometry->setLineWidth(1);         geometry->setDrawingMode(QSGGeometry::DrawLineStrip);         node->setGeometry(geometry);         node->setFlag(QSGNode::OwnsGeometry);         auto *material = new QSGFlatColorMaterial;         material->setColor(QColor("black"));         node->setMaterial(material);         node->setFlag(QSGNode::OwnsMaterial);          node->appendChildNode(m_arrowNode);     } else {         node = static_cast<QSGGeometryNode *>(oldNode);         geometry = node->geometry();         geometry->allocate(m_points.size() - 1);     }      QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();      for (int i = 0; i < m_points.size() - 1; ++i) {         vertices[i].set(m_points[i].x(), m_points[i].y());     }      if (!m_points.isEmpty()) {         QLineF line(m_points[m_points.size() - 2], m_points[m_points.size() - 1]);         QLineF n = line.normalVector();         QPointF o(n.dx() / 3.0, n.dy() / 3.0);          auto arrVertices = m_arrowNode->geometry()->vertexDataAsPoint2D();         arrVertices[0].set(line.p1().x() + o.x(), line.p1().y() + o.y());         arrVertices[1].set(line.p2().x(), line.p2().y());         arrVertices[2].set(line.p1().x() - o.x(), line.p1().y() - o.y());     }      node->markDirty(QSGNode::DirtyGeometry);     return node; }  void EdgeItem::setPoints(const QVector<QPointF> &points) {     m_points = points;     update(); }

Этого достаточно. Если задать свойство ребер setGraphAttribute("splines", "ortho"), то получим результат:

Код доступен на RPG-18/jgv и может отличаться от кода в статье.


ссылка на оригинал статьи https://habr.com/ru/post/689496/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *