## 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 1. **Model-View Approach**: Graph structure defined by classes derived from `AbstractGraphModel` 2. **Headless Mode**: Can create/modify graphs without GUI (useful for testing/processing) 3. **Two Main Workflows**: - Pure graph visualization (AbstractGraphModel) - Data propagation (DataFlowGraphModel) ### CMake Integration ```cmake # 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 ```cpp 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 data, PortIndex port) override; std::shared_ptr 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 ```cpp auto registry = std::make_shared(); registry->registerModel("Category"); ``` #### 3. Create DataFlowGraphModel ```cpp auto model = std::make_shared(registry); ``` ### Port Management #### Port Types ```cpp enum class PortType { In, Out }; ``` #### Defining Ports ```cpp 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: ```cpp portsAboutToBeInserted(nodeId, PortType::Out, startIndex, endIndex); // Modify underlying data portsInserted(); portsAboutToBeDeleted(nodeId, PortType::In, startIndex, endIndex); // Modify underlying data portsDeleted(); ``` ### Connection Management #### Connection Structure ```cpp struct ConnectionId { NodeId outNodeId; PortIndex outPortIndex; NodeId inNodeId; PortIndex inPortIndex; }; ``` #### Creating Connections ```cpp // Programmatically ConnectionId connId{sourceNode, 0, targetNode, 0}; model->addConnection(connId); // Check if possible if (model->connectionPossible(connId)) { model->addConnection(connId); } ``` #### Connection Policies ```cpp enum class ConnectionPolicy { One, // Only one connection per port Many // Multiple connections allowed }; // Return from nodeData with PortRole::ConnectionPolicyRole ``` ### Data Propagation #### Data Flow Chain 1. Source node emits `dataUpdated(PortIndex)` 2. `DataFlowGraphModel::onOutPortDataUpdated()` triggered 3. Data fetched via `outData(portIndex)` 4. Data set to connected nodes via `setInData(data, portIndex)` 5. Signal `inPortDataWasSet()` emitted #### Custom Data Types ```cpp class MyAudioData : public NodeData { public: NodeDataType type() const override { return {"audio", "Audio Stream"}; } // Your data members std::vector 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 ```cpp #include auto& nodeStyle = StyleCollection::nodeStyle(); auto& connStyle = StyleCollection::connectionStyle(); ``` #### JSON Style Format ```json { "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 ```cpp // 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 ```cpp #include #include #include auto registry = registerMyNodes(); auto model = std::make_shared(registry); auto scene = new DataFlowGraphicsScene(*model); auto view = new GraphicsView(scene); view->show(); ``` #### Undo/Redo Support ```cpp // 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 ```cpp // 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 ```cpp 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 ```cpp NodeFlags MyModel::nodeFlags(NodeId nodeId) const override { auto flags = DataFlowGraphModel::nodeFlags(nodeId); if (shouldLock) flags |= NodeFlag::Locked; return flags; } ``` #### Disable Connection Detaching ```cpp bool MyModel::detachPossible(ConnectionId const& id) const override { return false; // Prevent detaching } ``` #### Embedded Widgets ```cpp 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 operations - `examples/dynamic_ports/`: Adding/removing ports at runtime - `examples/styles/`: Custom styling - `examples/vertical_layout/`: Vertical node layout - `examples/lock_nodes_and_connections/`: Node locking - `examples/headless_main.cpp`: Using without GUI ### Key Classes Summary - `AbstractGraphModel`: Base graph model - `DataFlowGraphModel`: Graph with data propagation - `NodeDelegateModel`: Custom node implementation - `NodeDelegateModelRegistry`: Node type registration - `BasicGraphicsScene`: Scene management - `DataFlowGraphicsScene`: Scene with data flow - `GraphicsView`: View widget - `NodeData`: Base class for data types - `StyleCollection`: Centralized styling ### Important Notes for Audio Routing 1. Use `ConnectionPolicy::Many` for audio ports (multiple connections) 2. Consider using `std::shared_ptr` for data sharing 3. Implement proper sample rate/buffer size handling in data types 4. Use embedded widgets for gain controls, meters, etc. 5. Consider headless mode for audio processing thread 6. Implement proper serialization for saving/loading patches