A portable, extensible framework for building node-based visual editors with PyQt5.
PyQt Node Editor is a complete Python framework for creating visual node-based programming interfaces. Think Blender's node editor, Unreal Engine Blueprints, or audio software like Max/MSP – but for your own applications.
The framework is designed to be portable: simply copy the node_editor/ folder into your project and start building custom node-based tools.
- Portable Package: Self-contained, copy-paste ready
- Qt-Free Core: Domain logic independent of Qt runtime (graphics/widgets layers only)
- Stable IDs (ULID): Sortable unique identifiers for reliable persistence
- Theme Engine: Built-in dark/light themes with QSS stylesheet support
- 52 Built-in Nodes: Math, logic, string, list, time, file I/O operations
- Node Registry: Decorator-based registration with unique operation codes
- Full Serialization: JSON save/load with IO-free snapshot format and undo/redo history
- Edge Validators: Customizable connection rules
- Interactive Tools: Edge dragging, rerouting, snapping, cut-line
For detailed technical documentation see docs/architecture.md, which covers:
- Layer architecture (Core, Graphics, Widgets)
- Module dependencies and import hierarchy
- Data flow and evaluation system
- Theme system integration
- Serialization format
- Extension points for custom nodes, graphics, and validators
The framework follows these conventions:
- Graphics attributes: All graphics item references use snake_case naming (e.g.,
graphics_view,graphics_socket,graphics_node). - Class factories: Graphics classes are injected via
node_editor.core._init_graphics_classes()to avoid circular imports. - API methods: All public methods follow snake_case convention (e.g.,
mark_dirty(),get_input(),save_to_file()). - Scene state: Use the
has_been_modifiedproperty to check unsaved changes. - Serialization: Socket objects must explicitly include the
multi_edgesboolean field.
- Python 3.8+
- PyQt5 >= 5.15.0
# Clone the repo
git clone https://github.com/mecondev/node_editor.git
# Copy the portable package into your project
cp -r node_editor/node_editor /path/to/your/project/pip install -r requirements.txt
pip install -e ".[dev]" # For development dependenciesimport sys
from PyQt5.QtWidgets import QApplication
from node_editor import NodeEditorWindow
if __name__ == "__main__":
app = QApplication(sys.argv)
window = NodeEditorWindow()
window.show()
sys.exit(app.exec_())from PyQt5.QtWidgets import QMainWindow, QVBoxLayout, QWidget
from node_editor import NodeEditorWidget
class MyWindow(QMainWindow):
def __init__(self):
super().__init__()
container = QWidget()
layout = QVBoxLayout(container)
self.editor = NodeEditorWidget()
layout.addWidget(self.editor)
self.setCentralWidget(container)from node_editor.core.node import Node
from node_editor.nodes import NodeRegistry
@NodeRegistry.register(200) # Use op_codes >= 200 for custom nodes
class MyCustomNode(Node):
"""Custom node with one input and one output."""
def __init__(self, scene):
super().__init__(scene, "My Node", inputs=[1], outputs=[1])
self.mark_dirty()
def eval(self):
"""Evaluate the node."""
input_val = self.get_input(0)
if input_val is None:
self.mark_invalid()
return None
# Your custom logic here
result = input_val.eval() * 2
self.value = result
self.mark_dirty(False)
self.mark_invalid(False)
return resultfrom node_editor.themes import ThemeEngine, DarkTheme, LightTheme
# Register themes (done automatically on first use)
ThemeEngine.register_theme(DarkTheme)
ThemeEngine.register_theme(LightTheme)
# List available themes
print(ThemeEngine.available_themes()) # ['dark', 'light']
# Switch theme
ThemeEngine.set_theme("light")
# Refresh existing graphics items after theme change
ThemeEngine.refresh_graphics_items(editor.scene)
# Get current theme for accessing colors
theme = ThemeEngine.current_theme()
print(theme.node_background) # QColor objectCreate a new theme by subclassing BaseTheme:
from node_editor.themes import BaseTheme, ThemeEngine
from PyQt5.QtGui import QColor
class MyTheme(BaseTheme):
name = "mytheme"
display_name = "My Custom Theme"
scene_background = QColor("#1a1a2e")
node_background = QColor("#16213e")
node_title_background = QColor("#0f3460")
# ... override other colors as needed
# Register and use
ThemeEngine.register_theme(MyTheme)
ThemeEngine.set_theme("mytheme")| Category | Nodes | Op Codes | Module |
|---|---|---|---|
| Input | NumberInput, TextInput | 1-2 | input_node.py |
| Output | Output | 3 | output_node.py |
| Math | Add, Subtract, Multiply, Divide | 10-13 | math_nodes.py |
| Math Extended | Power, Sqrt, Abs, Min, Max, Round, Modulo | 50-56 | math_nodes.py |
| Comparison | Equal, NotEqual, LessThan, LessEqual, GreaterThan, GreaterEqual | 20-25 | logic_nodes.py |
| Logic | If, And, Or, Not, Xor | 30, 60-63 | logic_nodes.py |
| String | Concatenate, Format, Length, Substring, Split | 40-44 | string_nodes.py |
| Conversion | ToString, ToNumber, ToBool, ToInt | 70-73 | conversion_nodes.py |
| Utility | Constant, Print, Comment, Clamp, Random | 80-84 | utility_nodes.py |
| List | CreateList, GetItem, ListLength, Append, Join | 90-94 | list_nodes.py |
| Time | CurrentTime, FormatDate, ParseDate, TimeDelta, CompareTime | 100-104 | time_nodes.py |
| Advanced | RegexMatch, FileRead, FileWrite, HttpRequest | 110-113 | advanced_nodes.py |
- 1-113: Used by built-in framework nodes
- 200+: Recommended for custom application nodes
@NodeRegistry.register(200) # Your custom op_code
class MyApplicationNode(Node):
pass# Save to file
editor.scene.save_to_file("my_graph.json")
# Load from file
editor.scene.load_from_file("my_graph.json")
# Manual serialization
data = editor.scene.serialize() # Returns OrderedDict with version field
editor.scene.deserialize(data) # Restores state, handles version migrations{
"version": "2.0.0",
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"scene_width": 64000,
"scene_height": 64000,
"nodes": [
{
"sid": "01BX5ZZKBK6S0URNNZZ0BCZ7X0",
"title": "Add",
"pos_x": 100,
"pos_y": 200,
"inputs": [...],
"outputs": [...],
"content": {}
}
],
"edges": [
{
"sid": "01BX5ZZKBK6S0URNNZZ0C5Y9K1",
"edge_type": 2,
"start_sid": "01BX5ZZKBK6S0URNNZZ0BCZ7X0",
"end_sid": "01BX5ZZKBK6S0URNNZZ0BCZZZ0"
}
]
}Format v2 Features:
sid(stable ID): ULID for reliable cross-session identification- IO-free snapshot: Serialization independent of file I/O operations
versionfield enables format migrations when needed
A full-featured calculator application demonstrating MDI, custom nodes, and drag-and-drop:
cd examples/calculator
python main.pyBasic NodeEditorWindow showcase:
cd examples/minimal
python main.py# Run all tests
pytest
# Run with coverage
pytest --cov=node_editor
# Run specific test file
pytest tests/test_nodes_math.pyTest Status: 368 tests passing
node_editor/ # Root repository
├── main.py # Demo entry point
├── config.py # App configuration
├── requirements.txt
├── pyproject.toml
│
├── node_editor/ # THE PORTABLE PACKAGE
│ ├── __init__.py # Public API exports
│ │
│ ├── core/ # Framework core
│ │ ├── node.py # Node class
│ │ ├── edge.py # Edge class
│ │ ├── socket.py # Socket class
│ │ ├── scene.py # Scene manager
│ │ ├── history.py # Undo/redo
│ │ └── clipboard.py # Copy/paste
│ │
│ ├── graphics/ # Qt graphics items
│ │ ├── view.py # QGraphicsView
│ │ ├── scene.py # QGraphicsScene
│ │ ├── node.py # Node graphics
│ │ ├── edge.py # Edge graphics
│ │ └── socket.py # Socket graphics
│ │
│ ├── widgets/ # Embeddable widgets
│ │ ├── editor_widget.py # NodeEditorWidget
│ │ ├── editor_window.py # NodeEditorWindow
│ │ └── content_widget.py # Node content base
│ │
│ ├── nodes/ # Built-in nodes
│ │ ├── registry.py # Node registration
│ │ ├── math_nodes.py # Arithmetic
│ │ ├── logic_nodes.py # Comparisons, boolean
│ │ ├── string_nodes.py # String operations
│ │ └── ... # More node types
│ │
│ ├── themes/ # Theme engine
│ │ ├── theme_engine.py # Theme manager
│ │ ├── base_theme.py # Base theme class
│ │ ├── dark/ # Dark theme
│ │ └── light/ # Light theme
│ │
│ ├── tools/ # Interactive tools
│ │ ├── edge_dragging.py
│ │ ├── edge_validators.py
│ │ └── ...
│ │
│ └── utils/ # Helpers
│ ├── qt_helpers.py
│ └── helpers.py
│
├── examples/ # Example applications
│ ├── calculator/ # Full calculator app
│ └── minimal/ # Basic example
│
├── tests/ # Unit tests
│
└── docs/ # Documentation
from node_editor import (
Node, # Base node class
Edge, # Connection class
Socket, # Connection point class
)from node_editor import (
NodeEditorWidget, # Embeddable canvas
NodeEditorWindow, # Full application window
)from node_editor.nodes import (
NodeRegistry, # Registration system
# Built-in nodes
AddNode, SubtractNode, MultiplyNode, DivideNode,
NumberInputNode, TextInputNode, OutputNode,
# ... and 40+ more
)from node_editor.themes import (
ThemeEngine, # Theme manager
BaseTheme, # For custom themes
DarkTheme, # Built-in dark
LightTheme, # Built-in light
)- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing) - Write tests for your changes
- Ensure
pytestandruff check .pass - Submit a pull request
MIT License - see LICENSE file for details.
Author: Michael Economou
Version: 1.0.0
Date: 2025-12-14