Feature Overview
Graph object IDs
Nodes and Connections do not have any dedicated data instances. Instead, all the
relevant data is stored inside a user-defined GraphModel
class inherited from
AbstractGraphModel
.
// Definitions.hpp
using NodeId = unsigned int;
Each node is associated with a unique integer NodeId
which is returned
from the function NodeId AbstractGraphModel::addNode(QString)
.
- Important
This is the responsibility of the model to generate unique
NodeId
s.
The ConnectionId
is nothing else but a combination of input and output
NodeId
values with the corresponding PortIndex
:
// Definitinos.hpp
struct ConnectionId
{
NodeId outNodeId;
PortIndex outPortIndex;
NodeId inNodeId;
PortIndex inPortIndex;
};
Serialization
The serialization is supported at the moment by DataFlowGraphModel
. Should
you implement your own derivative of the class AbstractGraphModel
, it’s up to
you to support the serialization using the existing helper functions. An example
of such a code could be found in src/DataFlowGraphModel.cpp
.
In order so save the whole scene we normally make a loop over all the nodes and save their data in a Json arrray with a key “nodes” and then over all the connections which are saved in an array “connections”.
A typical node Json saved for the data flow scenario looks as follows:
{
"id" : 0,
"internal-data" : {
"model-name" : "Subtraction"
},
"position" : {
"x" : -383,
"y" : -95
}
}
The section internal-data
is filled by the specific data of the node itself
and is created in the function DataFlowGraphModel::saveNode(NodeId)
. In the
data-propagation workflow we store a name of the model there. In your application
it could be any internal additional data, i.e. an internal node state.
The Json for a serialized Connection in this case looks very simple:
{
"inPortIndex" : 0,
"intNodeId" : 1,
"outNodeId" : 0,
"outPortIndex" : 0
}
The data above is produced by a function DataFlowGraphModel::saveConnection
.
- Code Example
See the function
DataFlowGraphModel::save()
in the filesrc/DataFlowGraphModel.cpp
.
Undo/Redo
In order to support the undo/redo capabilities we employ the standar Qt’s class
QUndoStack . We keep a stack instance inside your BasicGraphicsScene
(or its derivatives).
Some default QUndoCommand
s are already implemented in the file
src/QUndoCommands.cpp
The command DeleteCommand
uses serialization to store the information of the
removed objects, namely AbstractGraphModel::saveNode(NodeId)
and
AbstractGraphModel::saveConnection(ConnectionId)
. Make sure you override
these functions in your derived graph models.
Wrapping your Graph Structure
If your task is to visualize a graph specific to your application and you do not
need the basic “data propagation” semantic implemented in this library
(DataFlowGraphModel
, DataFlowGraphicsScene
), you might need to derive
from AbstractGraphModel
and implement several abstract functions.
The models follows pretty much the ideas underlying the Qt’s
QAbstractItemModel
. The class delivers the IDs for all the existing scene
objects, the model is responsible for generating such unique IDs. You should
deliver information about nodes via AbstractGraphModel::nodeData
and assign
the data to nodes using AbstractGraphModel::setNodeData
. The passed and
returned data is wrapped into the data type QVariant
The pivotal enum
that defines type of the information we need to obtain is
called NodeRole
. See the file include/QtNodes/internal/Definitions.hpp
.
NodeRole |
Description |
---|---|
Type |
It corresponds to the type of the node and is described
by a |
Position |
|
Size |
|
CaptionVisible |
|
Caption |
|
Style |
Node editor’s internal json structure returned as a
|
InternalData |
|
InPortCount |
|
OutPortCount |
|
Widget |
|
- Code Example
For the usage see
examples/simple_graph_model
.
Node and Scene Styling
Default Node, Connection, and GraphicsView styles are stored in a centrall class
StyleCollection
.
Each default style is parsed from an internal Json string and stored in a corresponding data-class. Below you’ll find the contents of the Json strings at the moment of writing this documentation.
GraphicsViewStyle
{
"GraphicsViewStyle": {
"BackgroundColor": [53, 53, 53],
"FineGridColor": [60, 60, 60],
"CoarseGridColor": [25, 25, 25]
}
}
NodeStyle
{
"NodeStyle": {
"NormalBoundaryColor": [255, 255, 255],
"SelectedBoundaryColor": [255, 165, 0],
"GradientColor0": "gray",
"GradientColor1": [80, 80, 80],
"GradientColor2": [64, 64, 64],
"GradientColor3": [58, 58, 58],
"ShadowColor": [20, 20, 20],
"FontColor" : "white",
"FontColorFaded" : "gray",
"ConnectionPointColor": [169, 169, 169],
"FilledConnectionPointColor": "cyan",
"ErrorColor": "red",
"WarningColor": [128, 128, 0],
"PenWidth": 1.0,
"HoveredPenWidth": 1.5,
"ConnectionPointDiameter": 8.0,
"Opacity": 0.8
}
}
ConnectionStyle
{
"ConnectionStyle": {
"ConstructionColor": "gray",
"NormalColor": "darkcyan",
"SelectedColor": [100, 100, 100],
"SelectedHaloColor": "orange",
"HoveredColor": "lightcyan",
"LineWidth": 3.0,
"ConstructionLineWidth": 2.0,
"PointDiameter": 10.0,
"UseDataDefinedColors": false
}
}
- Code Example
For the usage see
examples/styles
andexamples/connection_colors
.
Vertical Layout
This feature might seem to be a bit “raw”. I haven’t had good use cases from real life projects to polish the code and the resulting node layout and rendering.
The current node layout in a vertical mode looks as follows:
-------o-------------o-------
| PortCaption PortCaption |
| |
| Node Caption |
| |
| |
| PortCaption |
--------------o--------------
- Code Example
For the usage see
examples/vertical_layout
.
Dynamic Ports
Dynamic Ports operations are driven by functions of the class
AbstractGraphModel
:
AbstractGraphModel::portsAboutToBeDeleted
AbstractGraphModel::portsDeleted
AbstractGraphModel::portsAboutToBeInserted
AbstractGraphModel::portsInserted
The function with the name “AboutTo” prepares the changes:
It computes the new connection IDs that are to be applied after the change is done.
It removes the existing connections that would have invalid addresses after modifications.
The functions porstDeleted
and portsInserted
create the new precomputed
connections with the correct IDs.
If you want to modify the number of ports in your code, it should approximately as follows:
void YourGraphModel::addPort()
{
portsAboutToBeInserted(nodeId, PortType::Out, 1, 2);
// DO YOUR UNDERLYING DATA MODIFICATIONS HERE
// The function call above has prepared the insertion of new output ports
// with the indexes 1 and 2.
// All the existed connectes below the new port 2 would be deleted and
// re-inserted with the new IDs (shifted by 2).
porstInserted();
}
- Code Example
For the usage see
examples/dynamic_ports
.
Locked Nodes and Connections
It is possible to completely disable or “freeze” the nodes. This would make them insensitive to moving and selecting events with the mouse.
In order to achieve such a behavior set specific flags and return from your graph model:
NodeFlags
YourGraphModel::
nodeFlags(NodeId nodeId) const override
{
auto basicFlags = DataFlowGraphModel::nodeFlags(nodeId);
if (_nodesLocked)
basicFlags |= NodeFlag::Locked;
return basicFlags;
}
Disabled Connection Detaching
For disabling detaching the connections from certain nodes override the function
virtual bool AbstractGraphModel::detachPossible(ConnectionId const) const
.
The default implementaion always returns true
.
- Code Example
For the usage see
examples/lock_nodes_and_connections
.
Data Propagation
Data-propagating classes add extra funtionality to the basic
AbstractGraphModel
which allows them to push the data from node to node upon
creating a connection.
The chain starts from the instance of a NodeDelegateModel
. It emits a Qt
signal dataUpdated(PortIndex)
. We always assume that the data is emitted from
one of the right hand side ports (PortType::Out
).
Then the function DataFlowGraphModel::onOutPortDataUpdated(NodeId, PortIndex)
comes into play. It reads the data from the output port, collects all the
attached connections for the given PortIndex
and sets the data to the
connected nodes using DataFlowGraphModel::setPortData
. After setting the data
to the input delegate model via NodeDelegateModel::setInData(...)
we emit the
signal inPortDataWasSet(nodeId, portType, portIndex)
. The signal is used to
redraw the receiver node and could be hooked up for other user’s purposes.
NodeDelegateModel:::dataUpdated(PortIndex)
// Source Delegate Model -> source NodeId
DataFlowGraphModel::onOutPortDataUpdated(NodeId, PortIndex)
// soure NodeId -> target NodeId
DataFlowGraphModel::setPortData()
// target NodeId -> target Delegate Model
NodeDelegateModel::setInData(NodeData, portIndex)
DataFlowGraphModel::setPortData()
Headless Mode
The class AbstractGraphModel
is independent of any scenes or visualization
windows. It is possible to instantiate a descendant of this abstract class and
populate the graph model.
Any instantiated BasicGraphicsScene
could be also used without attaching it
to a dedicated GraphicsView
.
- Code Example
See
examples/calculator/headless_main.cpp
. In this file we instantiate just aDataFlowGraphModel
and load a pre-saved calculator graph structure into it. The model is able to compute the results if the user modifies the inputs in the code.