
Решил запрототипировать два представления в дополнение к стандартному 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/
Добавить комментарий