8.4 KiB
8.4 KiB
QtNodes (nodeeditor) Library Research - 2026-01-29
Library Overview
- Repository: https://github.com/paceholder/nodeeditor
- License: BSD-3-Clause
- Latest Version: 3.0.x (major rewrite from v2.x)
- Qt Support: Qt 5.15+ and Qt 6.x
- Documentation: https://qtnodes.readthedocs.io/
Key Architecture Concepts
- Model-View Approach: Graph structure defined by classes derived from
AbstractGraphModel - Headless Mode: Can create/modify graphs without GUI (useful for testing/processing)
- Two Main Workflows:
- Pure graph visualization (AbstractGraphModel)
- Data propagation (DataFlowGraphModel)
CMake Integration
# Build options
cmake .. -DUSE_QT6=on # or off for Qt5
cmake .. -DBUILD_SHARED_LIBS=off # for static lib
# Dependencies
- Qt >5.15
- CMake 3.8+
- Catch2 (for tests, optional)
Creating Custom Node Types
1. Derive from NodeDelegateModel
class MyCustomNode : public QtNodes::NodeDelegateModel {
Q_OBJECT
public:
QString name() const override { return "MyNodeType"; }
QString caption() const override { return "My Node"; }
// Define ports
unsigned int nPorts(PortType portType) const override;
NodeDataType dataType(PortType portType, PortIndex index) const override;
// Data handling
void setInData(std::shared_ptr<NodeData> data, PortIndex port) override;
std::shared_ptr<NodeData> outData(PortIndex port) override;
// Optional: Embed Qt widget
QWidget* embeddedWidget() override { return _myWidget; }
// Optional: Make resizable
bool resizable() const override { return true; }
};
2. Register with NodeDelegateModelRegistry
auto registry = std::make_shared<NodeDelegateModelRegistry>();
registry->registerModel<MyCustomNode>("Category");
3. Create DataFlowGraphModel
auto model = std::make_shared<DataFlowGraphModel>(registry);
Port Management
Port Types
enum class PortType { In, Out };
Defining Ports
unsigned int MyNode::nPorts(PortType portType) const {
if (portType == PortType::In) return 2; // 2 input ports
return 1; // 1 output port
}
NodeDataType MyNode::dataType(PortType portType, PortIndex index) const {
return {"audio", "Audio Stream"}; // {id, name}
}
Dynamic Ports
Use these signals to add/remove ports at runtime:
portsAboutToBeInserted(nodeId, PortType::Out, startIndex, endIndex);
// Modify underlying data
portsInserted();
portsAboutToBeDeleted(nodeId, PortType::In, startIndex, endIndex);
// Modify underlying data
portsDeleted();
Connection Management
Connection Structure
struct ConnectionId {
NodeId outNodeId;
PortIndex outPortIndex;
NodeId inNodeId;
PortIndex inPortIndex;
};
Creating Connections
// Programmatically
ConnectionId connId{sourceNode, 0, targetNode, 0};
model->addConnection(connId);
// Check if possible
if (model->connectionPossible(connId)) {
model->addConnection(connId);
}
Connection Policies
enum class ConnectionPolicy {
One, // Only one connection per port
Many // Multiple connections allowed
};
// Return from nodeData with PortRole::ConnectionPolicyRole
Data Propagation
Data Flow Chain
- Source node emits
dataUpdated(PortIndex) DataFlowGraphModel::onOutPortDataUpdated()triggered- Data fetched via
outData(portIndex) - Data set to connected nodes via
setInData(data, portIndex) - Signal
inPortDataWasSet()emitted
Custom Data Types
class MyAudioData : public NodeData {
public:
NodeDataType type() const override {
return {"audio", "Audio Stream"};
}
// Your data members
std::vector<float> samples;
int sampleRate;
};
Styling and Customization
Style Classes
NodeStyle: Node appearance (colors, borders, shadows)ConnectionStyle: Connection lines (colors, width)GraphicsViewStyle: Background grid colors
Accessing Styles
#include <QtNodes/StyleCollection>
auto& nodeStyle = StyleCollection::nodeStyle();
auto& connStyle = StyleCollection::connectionStyle();
JSON Style Format
{
"NodeStyle": {
"NormalBoundaryColor": [255, 255, 255],
"SelectedBoundaryColor": [255, 165, 0],
"GradientColor0": "gray",
"ShadowColor": [20, 20, 20],
"FontColor": "white",
"ConnectionPointColor": [169, 169, 169],
"PenWidth": 1.0,
"Opacity": 0.8
}
}
Event Handling
Context Menus
// GraphicsView level
void MyGraphicsView::contextMenuEvent(QContextMenuEvent* event) override;
// Node level - connect to signal
connect(scene, &BasicGraphicsScene::nodeContextMenu,
[](NodeId nodeId, QPointF scenePos) {
// Show custom menu
});
Node Events
Available signals from BasicGraphicsScene:
nodeClicked(NodeId)nodeDoubleClicked(NodeId)nodeContextMenu(NodeId, QPointF)nodeHovered(NodeId, QPoint)nodeHoverLeft(NodeId)
Scene Setup
Basic Setup
#include <QtNodes/DataFlowGraphModel>
#include <QtNodes/DataFlowGraphicsScene>
#include <QtNodes/GraphicsView>
auto registry = registerMyNodes();
auto model = std::make_shared<DataFlowGraphModel>(registry);
auto scene = new DataFlowGraphicsScene(*model);
auto view = new GraphicsView(scene);
view->show();
Undo/Redo Support
// Scene has built-in QUndoStack
scene->undoStack().undo();
scene->undoStack().redo();
// Keyboard shortcuts (built-in)
// Ctrl+Z: Undo
// Ctrl+Shift+Z: Redo
// Ctrl+D: Duplicate
Serialization
Save/Load Graph
// Save
QJsonObject json = model->save();
QJsonDocument doc(json);
QFile file("graph.json");
file.open(QIODevice::WriteOnly);
file.write(doc.toJson());
// Load
QFile file("graph.json");
file.open(QIODevice::ReadOnly);
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
model->load(doc.object());
Custom Node Serialization
QJsonObject MyNode::save() const override {
QJsonObject obj = NodeDelegateModel::save();
obj["custom-data"] = _myCustomValue;
return obj;
}
void MyNode::load(QJsonObject const& obj) override {
_myCustomValue = obj["custom-data"].toInt();
}
Layout Options
Horizontal (Default)
-------
o | Caption
o |
-------
Vertical
-------o-------------o-------
| PortCaption PortCaption |
| Node Caption |
| PortCaption |
--------------o--------------
Use DefaultVerticalNodeGeometry instead of DefaultHorizontalNodeGeometry.
Advanced Features
Locked Nodes
NodeFlags MyModel::nodeFlags(NodeId nodeId) const override {
auto flags = DataFlowGraphModel::nodeFlags(nodeId);
if (shouldLock) flags |= NodeFlag::Locked;
return flags;
}
Disable Connection Detaching
bool MyModel::detachPossible(ConnectionId const& id) const override {
return false; // Prevent detaching
}
Embedded Widgets
QWidget* MyNode::embeddedWidget() override {
if (!_widget) {
_widget = new QSlider(Qt::Horizontal);
connect(_widget, &QSlider::valueChanged, [this](int val) {
// Update and propagate data
Q_EMIT dataUpdated(0);
});
}
return _widget;
}
Example References
examples/calculator/: Basic data flow with math operationsexamples/dynamic_ports/: Adding/removing ports at runtimeexamples/styles/: Custom stylingexamples/vertical_layout/: Vertical node layoutexamples/lock_nodes_and_connections/: Node lockingexamples/headless_main.cpp: Using without GUI
Key Classes Summary
AbstractGraphModel: Base graph modelDataFlowGraphModel: Graph with data propagationNodeDelegateModel: Custom node implementationNodeDelegateModelRegistry: Node type registrationBasicGraphicsScene: Scene managementDataFlowGraphicsScene: Scene with data flowGraphicsView: View widgetNodeData: Base class for data typesStyleCollection: Centralized styling
Important Notes for Audio Routing
- Use
ConnectionPolicy::Manyfor audio ports (multiple connections) - Consider using
std::shared_ptr<AudioBuffer>for data sharing - Implement proper sample rate/buffer size handling in data types
- Use embedded widgets for gain controls, meters, etc.
- Consider headless mode for audio processing thread
- Implement proper serialization for saving/loading patches