From 5c610fff0b2bdc0259516a88656201762d18289a Mon Sep 17 00:00:00 2001 From: dmatetelki Date: Fri, 1 Aug 2014 14:09:31 +0200 Subject: [PATCH] Qt visualization of graph, from the Elastic Nodes Example --- lib/qtgraph/CMakeLists.txt | 16 +++ lib/qtgraph/edge.cpp | 95 ++++++++++++++++++ lib/qtgraph/edge.hpp | 33 ++++++ lib/qtgraph/graphwidget.cpp | 195 ++++++++++++++++++++++++++++++++++++ lib/qtgraph/graphwidget.hpp | 35 +++++++ lib/qtgraph/main.cpp | 27 +++++ lib/qtgraph/node.cpp | 147 +++++++++++++++++++++++++++ lib/qtgraph/node.hpp | 41 ++++++++ 8 files changed, 589 insertions(+) create mode 100644 lib/qtgraph/CMakeLists.txt create mode 100644 lib/qtgraph/edge.cpp create mode 100644 lib/qtgraph/edge.hpp create mode 100644 lib/qtgraph/graphwidget.cpp create mode 100644 lib/qtgraph/graphwidget.hpp create mode 100644 lib/qtgraph/main.cpp create mode 100644 lib/qtgraph/node.cpp create mode 100644 lib/qtgraph/node.hpp diff --git a/lib/qtgraph/CMakeLists.txt b/lib/qtgraph/CMakeLists.txt new file mode 100644 index 0000000..2e89a80 --- /dev/null +++ b/lib/qtgraph/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required (VERSION 2.6) +project (PROJECT_QGRAPH) + +set (CXX_FLAGS "-Wall -Wextra -pedantic -Wshadow " + "-Wpointer-arith -Wcast-qual " + "-ggdb " + "--std=c++0x " ) + +add_definitions( ${CXX_FLAGS} ) + +FIND_PACKAGE(Qt4 REQUIRED) +INCLUDE(${QT_USE_FILE}) +QT4_WRAP_CPP(qtgraph_HEADERS_MOC node.hpp edge.hpp graphwidget.hpp) + +add_executable ( qtgraph main.cpp edge.cpp node.cpp graphwidget.cpp ${qtgraph_HEADERS_MOC} ) +target_link_libraries ( qtgraph ${QT_LIBRARIES} ) diff --git a/lib/qtgraph/edge.cpp b/lib/qtgraph/edge.cpp new file mode 100644 index 0000000..bb5afa8 --- /dev/null +++ b/lib/qtgraph/edge.cpp @@ -0,0 +1,95 @@ +#include "edge.hpp" +#include "node.hpp" + +#include + +#include + +static const double Pi = 3.14159265358979323846264338327950288419717; +static double TwoPi = 2.0 * Pi; + +Edge::Edge(Node *sourceNode, Node *destNode) + : arrowSize(10) +{ + setAcceptedMouseButtons(0); + source = sourceNode; + dest = destNode; + source->addEdge(this); + dest->addEdge(this); + adjust(); +} + +Node *Edge::sourceNode() const +{ + return source; +} + +Node *Edge::destNode() const +{ + return dest; +} + +void Edge::adjust() +{ + if (!source || !dest) + return; + + QLineF line(mapFromItem(source, 0, 0), mapFromItem(dest, 0, 0)); + qreal length = line.length(); + + prepareGeometryChange(); + + if (length > qreal(20.)) { + QPointF edgeOffset((line.dx() * 10) / length, (line.dy() * 10) / length); + sourcePoint = line.p1() + edgeOffset; + destPoint = line.p2() - edgeOffset; + } else { + sourcePoint = destPoint = line.p1(); + } +} + +QRectF Edge::boundingRect() const +{ + if (!source || !dest) + return QRectF(); + + qreal penWidth = 1; + qreal extra = (penWidth + arrowSize) / 2.0; + + return QRectF(sourcePoint, QSizeF(destPoint.x() - sourcePoint.x(), + destPoint.y() - sourcePoint.y())) + .normalized() + .adjusted(-extra, -extra, extra, extra); +} + +void Edge::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + if (!source || !dest) + return; + + QLineF line(sourcePoint, destPoint); + if (qFuzzyCompare(line.length(), qreal(0.))) + return; + + // Draw the line itself + painter->setPen(QPen(Qt::black, 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); + painter->drawLine(line); + + // Draw the arrows + double angle = ::acos(line.dx() / line.length()); + if (line.dy() >= 0) + angle = TwoPi - angle; + + QPointF sourceArrowP1 = sourcePoint + QPointF(sin(angle + Pi / 3) * arrowSize, + cos(angle + Pi / 3) * arrowSize); + QPointF sourceArrowP2 = sourcePoint + QPointF(sin(angle + Pi - Pi / 3) * arrowSize, + cos(angle + Pi - Pi / 3) * arrowSize); + QPointF destArrowP1 = destPoint + QPointF(sin(angle - Pi / 3) * arrowSize, + cos(angle - Pi / 3) * arrowSize); + QPointF destArrowP2 = destPoint + QPointF(sin(angle - Pi + Pi / 3) * arrowSize, + cos(angle - Pi + Pi / 3) * arrowSize); + + painter->setBrush(Qt::black); + painter->drawPolygon(QPolygonF() << line.p1() << sourceArrowP1 << sourceArrowP2); + painter->drawPolygon(QPolygonF() << line.p2() << destArrowP1 << destArrowP2); +} \ No newline at end of file diff --git a/lib/qtgraph/edge.hpp b/lib/qtgraph/edge.hpp new file mode 100644 index 0000000..a6b13ab --- /dev/null +++ b/lib/qtgraph/edge.hpp @@ -0,0 +1,33 @@ +#ifndef EDGE_H +#define EDGE_H + +#include + +class Node; + +class Edge : public QGraphicsItem +{ +public: + Edge(Node *sourceNode, Node *destNode); + + Node *sourceNode() const; + Node *destNode() const; + + void adjust(); + + enum { Type = UserType + 2 }; + int type() const { return Type; } + +protected: + QRectF boundingRect() const; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + +private: + Node *source, *dest; + + QPointF sourcePoint; + QPointF destPoint; + qreal arrowSize; +}; + +#endif diff --git a/lib/qtgraph/graphwidget.cpp b/lib/qtgraph/graphwidget.cpp new file mode 100644 index 0000000..7ffe947 --- /dev/null +++ b/lib/qtgraph/graphwidget.cpp @@ -0,0 +1,195 @@ +#include "graphwidget.hpp" + +#include "edge.hpp" +#include "node.hpp" + +#include + +#include + +GraphWidget::GraphWidget(QWidget *parent) + : QGraphicsView(parent), timerId(0) +{ + QGraphicsScene *scene = new QGraphicsScene(this); + scene->setItemIndexMethod(QGraphicsScene::NoIndex); + scene->setSceneRect(-200, -200, 400, 400); + setScene(scene); + setCacheMode(CacheBackground); + setViewportUpdateMode(BoundingRectViewportUpdate); + setRenderHint(QPainter::Antialiasing); + setTransformationAnchor(AnchorUnderMouse); + scale(qreal(0.8), qreal(0.8)); + setMinimumSize(400, 400); + setWindowTitle(tr("Elastic Nodes")); + + Node *node1 = new Node(this); + Node *node2 = new Node(this); + Node *node3 = new Node(this); + Node *node4 = new Node(this); + centerNode = new Node(this); + Node *node6 = new Node(this); + Node *node7 = new Node(this); + Node *node8 = new Node(this); + Node *node9 = new Node(this); + scene->addItem(node1); + scene->addItem(node2); + scene->addItem(node3); + scene->addItem(node4); + scene->addItem(centerNode); + scene->addItem(node6); + scene->addItem(node7); + scene->addItem(node8); + scene->addItem(node9); + scene->addItem(new Edge(node1, node2)); + scene->addItem(new Edge(node2, node3)); + scene->addItem(new Edge(node2, centerNode)); + scene->addItem(new Edge(node3, node6)); + scene->addItem(new Edge(node4, node1)); + scene->addItem(new Edge(node4, centerNode)); + scene->addItem(new Edge(centerNode, node6)); + scene->addItem(new Edge(centerNode, node8)); + scene->addItem(new Edge(node6, node9)); + scene->addItem(new Edge(node7, node4)); + scene->addItem(new Edge(node8, node7)); + scene->addItem(new Edge(node9, node8)); + + node1->setPos(-50, -50); + node2->setPos(0, -50); + node3->setPos(50, -50); + node4->setPos(-50, 0); + centerNode->setPos(0, 0); + node6->setPos(50, 0); + node7->setPos(-50, 50); + node8->setPos(0, 50); + node9->setPos(50, 50); +} + +void GraphWidget::itemMoved() +{ + if (!timerId) + timerId = startTimer(1000 / 25); +} + +void GraphWidget::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Up: + centerNode->moveBy(0, -20); + break; + case Qt::Key_Down: + centerNode->moveBy(0, 20); + break; + case Qt::Key_Left: + centerNode->moveBy(-20, 0); + break; + case Qt::Key_Right: + centerNode->moveBy(20, 0); + break; + case Qt::Key_Plus: + zoomIn(); + break; + case Qt::Key_Minus: + zoomOut(); + break; + case Qt::Key_Space: + case Qt::Key_Enter: + shuffle(); + break; + default: + QGraphicsView::keyPressEvent(event); + } +} + +void GraphWidget::timerEvent(QTimerEvent *event) +{ + Q_UNUSED(event); + + QList nodes; + foreach (QGraphicsItem *item, scene()->items()) { + if (Node *node = qgraphicsitem_cast(item)) + nodes << node; + } + + foreach (Node *node, nodes) + node->calculateForces(); + + bool itemsMoved = false; + foreach (Node *node, nodes) { + if (node->advance()) + itemsMoved = true; + } + + if (!itemsMoved) { + killTimer(timerId); + timerId = 0; + } +} + +void GraphWidget::wheelEvent(QWheelEvent *event) +{ + scaleView(pow((double)2, -event->delta() / 240.0)); +} + +void GraphWidget::drawBackground(QPainter *painter, const QRectF &rect) +{ + Q_UNUSED(rect); + + // Shadow + QRectF sceneRect = this->sceneRect(); + QRectF rightShadow(sceneRect.right(), sceneRect.top() + 5, 5, sceneRect.height()); + QRectF bottomShadow(sceneRect.left() + 5, sceneRect.bottom(), sceneRect.width(), 5); + if (rightShadow.intersects(rect) || rightShadow.contains(rect)) + painter->fillRect(rightShadow, Qt::darkGray); + if (bottomShadow.intersects(rect) || bottomShadow.contains(rect)) + painter->fillRect(bottomShadow, Qt::darkGray); + + // Fill + QLinearGradient gradient(sceneRect.topLeft(), sceneRect.bottomRight()); + gradient.setColorAt(0, Qt::white); + gradient.setColorAt(1, Qt::lightGray); + painter->fillRect(rect.intersect(sceneRect), gradient); + painter->setBrush(Qt::NoBrush); + painter->drawRect(sceneRect); + + // Text + QRectF textRect(sceneRect.left() + 4, sceneRect.top() + 4, + sceneRect.width() - 4, sceneRect.height() - 4); + QString message(tr("Click and drag the nodes around, and zoom with the mouse " + "wheel or the '+' and '-' keys")); + + QFont font = painter->font(); + font.setBold(true); + font.setPointSize(14); + painter->setFont(font); + painter->setPen(Qt::lightGray); + painter->drawText(textRect.translated(2, 2), message); + painter->setPen(Qt::black); + painter->drawText(textRect, message); +} + +void GraphWidget::scaleView(qreal scaleFactor) +{ + qreal factor = transform().scale(scaleFactor, scaleFactor).mapRect(QRectF(0, 0, 1, 1)).width(); + if (factor < 0.07 || factor > 100) + return; + + scale(scaleFactor, scaleFactor); +} + +void GraphWidget::shuffle() +{ + foreach (QGraphicsItem *item, scene()->items()) { + if (qgraphicsitem_cast(item)) + item->setPos(-150 + qrand() % 300, -150 + qrand() % 300); + } +} + +void GraphWidget::zoomIn() +{ + scaleView(qreal(1.2)); +} + +void GraphWidget::zoomOut() +{ + scaleView(1 / qreal(1.2)); +} diff --git a/lib/qtgraph/graphwidget.hpp b/lib/qtgraph/graphwidget.hpp new file mode 100644 index 0000000..2f1a79f --- /dev/null +++ b/lib/qtgraph/graphwidget.hpp @@ -0,0 +1,35 @@ +#ifndef GRAPHWIDGET_H +#define GRAPHWIDGET_H + +#include + +class Node; + +class GraphWidget : public QGraphicsView +{ + Q_OBJECT + +public: + GraphWidget(QWidget *parent = 0); + + void itemMoved(); + +public slots: + void shuffle(); + void zoomIn(); + void zoomOut(); + +protected: + void keyPressEvent(QKeyEvent *event); + void timerEvent(QTimerEvent *event); + void wheelEvent(QWheelEvent *event); + void drawBackground(QPainter *painter, const QRectF &rect); + + void scaleView(qreal scaleFactor); + +private: + int timerId; + Node *centerNode; +}; + +#endif diff --git a/lib/qtgraph/main.cpp b/lib/qtgraph/main.cpp new file mode 100644 index 0000000..7746d3c --- /dev/null +++ b/lib/qtgraph/main.cpp @@ -0,0 +1,27 @@ +#include +#include +#include + +#include + + +#include "graphwidget.hpp" + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + qsrand(QTime(0,0,0).secsTo(QTime::currentTime())); + + GraphWidget *widget = new GraphWidget; + + QMainWindow mainWindow; + mainWindow.setCentralWidget(widget); + + + mainWindow.menuBar()->addAction("Shuffle", widget, SLOT(shuffle())); + mainWindow.menuBar()->addAction("Zoom In", widget, SLOT(zoomIn())); + mainWindow.menuBar()->addAction("Zoom Out", widget, SLOT(zoomOut())); + + mainWindow.show(); + return app.exec(); +} diff --git a/lib/qtgraph/node.cpp b/lib/qtgraph/node.cpp new file mode 100644 index 0000000..e06452e --- /dev/null +++ b/lib/qtgraph/node.cpp @@ -0,0 +1,147 @@ +#include "edge.hpp" +#include "node.hpp" +#include "graphwidget.hpp" + +#include +#include +#include +#include + + +Node::Node(GraphWidget *graphWidget) + : graph(graphWidget) +{ + setFlag(ItemIsMovable); + setFlag(ItemSendsGeometryChanges); + setCacheMode(DeviceCoordinateCache); + setZValue(-1); +} + +void Node::addEdge(Edge *edge) +{ + edgeList << edge; + edge->adjust(); +} + +QList Node::edges() const +{ + return edgeList; +} + +void Node::calculateForces() +{ + if (!scene() || scene()->mouseGrabberItem() == this) { + newPos = pos(); + return; + } + + // Sum up all forces pushing this item away + qreal xvel = 0; + qreal yvel = 0; + foreach (QGraphicsItem *item, scene()->items()) { + Node *node = qgraphicsitem_cast(item); + if (!node) + continue; + + QPointF vec = mapToItem(node, 0, 0); + qreal dx = vec.x(); + qreal dy = vec.y(); + double l = 2.0 * (dx * dx + dy * dy); + if (l > 0) { + xvel += (dx * 150.0) / l; + yvel += (dy * 150.0) / l; + } + } + + // Now subtract all forces pulling items together + double weight = (edgeList.size() + 1) * 10; + foreach (Edge *edge, edgeList) { + QPointF vec; + if (edge->sourceNode() == this) + vec = mapToItem(edge->destNode(), 0, 0); + else + vec = mapToItem(edge->sourceNode(), 0, 0); + xvel -= vec.x() / weight; + yvel -= vec.y() / weight; + } + + if (qAbs(xvel) < 0.1 && qAbs(yvel) < 0.1) + xvel = yvel = 0; + + QRectF sceneRect = scene()->sceneRect(); + newPos = pos() + QPointF(xvel, yvel); + newPos.setX(qMin(qMax(newPos.x(), sceneRect.left() + 10), sceneRect.right() - 10)); + newPos.setY(qMin(qMax(newPos.y(), sceneRect.top() + 10), sceneRect.bottom() - 10)); +} + +bool Node::advance() +{ + if (newPos == pos()) + return false; + + setPos(newPos); + return true; +} + +QRectF Node::boundingRect() const +{ + qreal adjust = 2; + return QRectF( -10 - adjust, -10 - adjust, + 23 + adjust, 23 + adjust); +} + +QPainterPath Node::shape() const +{ + QPainterPath path; + path.addEllipse(-10, -10, 20, 20); + return path; +} + +void Node::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) +{ + painter->setPen(Qt::NoPen); + painter->setBrush(Qt::darkGray); + painter->drawEllipse(-7, -7, 20, 20); + + QRadialGradient gradient(-3, -3, 10); + if (option->state & QStyle::State_Sunken) { + gradient.setCenter(3, 3); + gradient.setFocalPoint(3, 3); + gradient.setColorAt(1, QColor(Qt::yellow).light(120)); + gradient.setColorAt(0, QColor(Qt::darkYellow).light(120)); + } else { + gradient.setColorAt(0, Qt::yellow); + gradient.setColorAt(1, Qt::darkYellow); + } + painter->setBrush(gradient); + + painter->setPen(QPen(Qt::black, 0)); + painter->drawEllipse(-10, -10, 20, 20); +} + +QVariant Node::itemChange(GraphicsItemChange change, const QVariant &value) +{ + switch (change) { + case ItemPositionHasChanged: + foreach (Edge *edge, edgeList) + edge->adjust(); + graph->itemMoved(); + break; + default: + break; + }; + + return QGraphicsItem::itemChange(change, value); +} + +void Node::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + update(); + QGraphicsItem::mousePressEvent(event); +} + +void Node::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + update(); + QGraphicsItem::mouseReleaseEvent(event); +} \ No newline at end of file diff --git a/lib/qtgraph/node.hpp b/lib/qtgraph/node.hpp new file mode 100644 index 0000000..3ebfb5f --- /dev/null +++ b/lib/qtgraph/node.hpp @@ -0,0 +1,41 @@ +#ifndef NODE_H +#define NODE_H + +#include +#include + +class Edge; +class GraphWidget; +class QGraphicsSceneMouseEvent; + +class Node : public QGraphicsItem +{ +public: + Node(GraphWidget *graphWidget); + + void addEdge(Edge *edge); + QList edges() const; + + enum { Type = UserType + 1 }; + int type() const { return Type; } + + void calculateForces(); + bool advance(); + + QRectF boundingRect() const; + QPainterPath shape() const; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); + +protected: + QVariant itemChange(GraphicsItemChange change, const QVariant &value); + + void mousePressEvent(QGraphicsSceneMouseEvent *event); + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); + +private: + QList edgeList; + QPointF newPos; + GraphWidget *graph; +}; + +#endif