Overview of QtNodes
masterAbstractGraphModel. It supports a "headless" mode, allowing you to manipulate the graph model without attaching it to a QGraphicsScene or QGraphicsView.repository·master·Indexed 25 days ago
https://github.com/paceholder/nodeeditorA Qt-based library for creating node editors supporting general graph visualization and dataflow programming using a Model-View architecture. It provides core classes like AbstractGraphModel and DataFlowGraphModel for graph logic, and BasicGraphicsScene and DataFlowGraphicsScene for visual representation. The library includes tools for custom node painting, geometry, styling, and a comprehensive undo/redo system via command classes.
AbstractGraphModel. It supports a "headless" mode, allowing you to manipulate the graph model without attaching it to a QGraphicsScene or QGraphicsView.QtNodes uses a Model-View architecture to separate graph data from its visual representation. This separation allows you to run graph logic in headless mode (without a GUI), maintain multiple views of the same data, and test graph logic independently of the UI.
AbstractGraph (e.g., AbstractGraphModel or DataFlowGraphModel) that holds your data like nodes, connections, and positions.BasicGraphicsScene (inheriting from QGraphicsScene) and a GraphicsView (inheriting from QGraphicsView) that visualize the model.The following examples demonstrate specific features of QtNodes:
examples/calculator/): Full data flow application with number sources, operators, and display. Demonstrates embedded widgets, save/load, and menu integration.examples/simple_graph_model/): Minimal custom graph model implementation by subclassing AbstractGraphModel and using BasicGraphicsScene.examples/styles/): Custom styling with different color schemes and visual effects.examples/connection_colors/): Data-type-based connection coloring where each data type has its own color.examples/vertical_layout/): Top-to-bottom node arrangement with ports on top and bottom edges.examples/dynamic_ports/): Adding and removing ports at runtime using the two-phase port modification API.examples/resizable_images/): Nodes with embedded image widgets that can be resized by dragging.examples/lock_nodes_and_connections/): Preventing node movement and connection detachment.examples/node_validation/): Demonstrating NodeValidationState and NodeProcessingStatus for error/warning states and processing indicators.examples/custom_painter/): Custom node and connection rendering with gradients and arrows.examples/text/): Simple text propagation between nodes.You can configure the build using CMake. Use the USE_QT6 flag to switch between Qt5 and Qt6, and BUILD_SHARED_LIBS to toggle between shared and static libraries.
To build with Qt6:
mkdir build && cd build && cmake .. -DUSE_QT6=onTo build with Qt5:
mkdir build && cd build && cmake .. -DUSE_QT6=offTo build a static library:
cmake .. -DBUILD_SHARED_LIBS=offTo disable testing (if Catch2 is not installed):
cmake .. -DBUILD_TESTING=OFFmkdir build && cd build && cmake .. -DUSE_QT6=onTo color connections based on the data type they carry, follow these two steps:
UseDataDefinedColors in your global ConnectionStyle.NodeDataType implementation.When a connection is made between ports of a specific data type, the connection line will use the color defined in that type's type() method.
// 1. Enable data-defined colors globally
ConnectionStyle::setConnectionStyle(R"({
"ConnectionStyle": {
"UseDataDefinedColors": true
}
})");
// 2. Define the color in your NodeDataType
NodeDataType NumberData::type() const
{
return NodeDataType{
"number", // id
"Number", // name
QColor(0, 128, 255) // color for connections
};
}To ensure that deleted nodes can be restored via the undo system, your model must implement saveNode() and loadNode(). These methods allow the undo framework to serialize the node's state before deletion and reconstruct it during an undo operation.
QJsonObject MyModel::saveNode(NodeId nodeId) const override
{
// Save all data needed to recreate this node
QJsonObject json;
json["id"] = static_cast<qint64>(nodeId);
QPointF pos = nodeData(nodeId, NodeRole::Position).toPointF();
json["position"] = QJsonObject{{"x", pos.x()}, {"y", pos.y()}};
// Save your custom data too
json["internal-data"] = getNodeInternalData(nodeId);
return json;
}
void MyModel::loadNode(QJsonObject const& json) override
{
// Recreate node from saved data
NodeId nodeId = static_cast<NodeId>(json["id"].toInt());
_nextId = std::max(_nextId, nodeId + 1);
_nodes.insert(nodeId);
emit nodeCreated(nodeId);
// Restore position
auto pos = json["position"].toObject();
setNodeData(nodeId, NodeRole::Position,
QPointF(pos["x"].toDouble(), pos["y"].toDouble()));
// Restore custom data
restoreNodeInternalData(nodeId, json["internal-data"].toObject());
}The data flow model automates data propagation between nodes. When a node's output changes, connected nodes automatically receive the new data. The system consists of three components:
To implement a data flow application, you must define a custom NodeData type, create a NodeDelegateModel for your node logic, and register these nodes in a NodeDelegateModelRegistry before passing the registry to a DataFlowGraphModel.
#include <QtNodes/NodeData>
#include <QtNodes/NodeDelegateModel>
#include <QtNodes/NodeDelegateModelRegistry>
// 1. Define data type
class NumberData : public QtNodes::NodeData {
public:
NumberData(double value = 0.0) : _value(value) {}
QtNodes::NodeDataType type() const override { return {"number", "Number"}; }
double value() const { return _value; }
private:
double _value;
};
// 2. Create node delegate
class AdditionNode : public QtNodes::NodeDelegateModel {
Q_OBJECT
public:
QString caption() const override { return "Add"; }
QString name() const override { return "Addition"; }
unsigned int nPorts(PortType type) const override { return type == PortType::In ? 2 : 1; }
NodeDataType dataType(PortType, PortIndex) const override { return NumberData{}.type(); }
void setInData(std::shared_ptr<NodeData> data, PortIndex port) override;
std::shared_ptr<NodeData> outData(PortIndex) override;
QWidget* embeddedWidget() override { return nullptr; }
private:
std::shared_ptr<NumberData> _input1, _input2, _result;
};
// 3. Register and create model
auto registry = std::make_shared<NodeDelegateModelRegistry>();
registry->registerModel<NumberSourceNode>("Sources");
registry->registerModel<AdditionNode>("Operators");
registry->registerModel<DisplayNode>("Outputs");
DataFlowGraphModel model(registry);
DataFlowGraphicsScene scene(model);To display your graph model in a window, follow these steps:
AbstractGraphModel implementation.QtNodes::BasicGraphicsScene passing your model to the constructor.QtNodes::GraphicsView passing the scene to the constructor.#include <QApplication>
#include <QtNodes/GraphicsView>
#include <QtNodes/BasicGraphicsScene>
#include "SimpleGraphModel.hpp"
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
// 1. Create the graph model
SimpleGraphModel model;
// 2. Create a scene that visualizes the model
auto* scene = new QtNodes::BasicGraphicsScene(model);
// 3. Create a view to display the scene
QtNodes::GraphicsView view(scene);
view.setWindowTitle("My First Node Graph");
view.resize(800, 600);
view.show();
return app.exec();
}The QtNodes library uses the Catch2 testing framework. You can build and execute the test suite from your build directory using make commands.
To build the tests, use make test_nodes. To run the entire test suite, execute the test_nodes binary located in the bin/ directory.
# Build tests
make test_nodes
# Run all tests
./bin/test_nodesQtNodes provides two distinct approaches depending on whether you need simple visualization or active data processing:
AbstractGraphModel and BasicGraphicsScene. This approach is best when you want to manage all graph data yourself and focus on visualization or custom graph logic.DataFlowGraphModel and DataFlowGraphicsScene. This approach is best for visual programming, calculators, or pipelines, as the library manages node delegates and data routing automatically.Once the project is built, each example is available as a standalone executable in the bin/ directory of your build folder.
# From build directory
./bin/calculator
./bin/simple_graph_model
./bin/stylesWhen registering models in the NodeDelegateModelRegistry, you must now provide a category string. In v2, registerModel<T>() did not require this argument.
auto registry = std::make_shared<NodeDelegateModelRegistry>();
registry->registerModel<MyNode>("Category"); // Category now required