NodeGraphicsObject
是 QtNodes 节点编辑器中的核心图形类,负责节点的可视化表现和用户交互。NodeGraphicsObject
继承自 QGraphicsObject
,主要功能包括:
-
管理节点的可视化渲染
-
处理节点相关的鼠标事件和交互
-
维护节点与连接的图形关系
-
提供节点嵌入小部件的能力
#pragma once
#include "Connection.hpp"
#include "Export.hpp"
#include "NodeGeometry.hpp"
#include "NodeState.hpp"
#include <QtCore/QUuid>
#include <QtWidgets/QGraphicsObject>
class QGraphicsProxyWidget;
namespace QtNodes
{
class FlowScene;
class FlowItemEntry;
/// Class reacts on GUI events, mouse clicks and
/// forwards painting operation.
class NODE_EDITOR_PUBLIC NodeGraphicsObject : public QGraphicsObject
{
Q_OBJECT
public:
NodeGraphicsObject(FlowScene &scene, Node &node);
virtual ~NodeGraphicsObject();
Node &node();
Node const &node() const;
QRectF boundingRect() const override;
void setGeometryChanged();
/// Visits all attached connections and corrects
/// their corresponding end points.
void moveConnections() const;
enum
{
Type = UserType + 1
};
int type() const override
{
return Type;
}
void lock(bool locked);
void embedQWidget(bool embed = true);
protected:
void paint(QPainter *painter, QStyleOptionGraphicsItem const *option, QWidget *widget = 0) override;
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
void hoverMoveEvent(QGraphicsSceneHoverEvent *) override;
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
private:
FlowScene &_scene;
Node &_node;
bool _locked;
// either nullptr or owned by parent QGraphicsItem
QGraphicsProxyWidget *_proxyWidget;
};
} // namespace QtNodes
#include "NodeGraphicsObject.hpp"
#include "ConnectionGraphicsObject.hpp"
#include "ConnectionState.hpp"
#include "FlowScene.hpp"
#include "Node.hpp"
#include "NodeConnectionInteraction.hpp"
#include "NodeDataModel.hpp"
#include "NodePainter.hpp"
#include "StyleCollection.hpp"
#include <QtWidgets/QGraphicsEffect>
#include <QtWidgets/QtWidgets>
#include <cstdlib>
#include <iostream>
using QtNodes::FlowScene;
using QtNodes::Node;
using QtNodes::NodeGraphicsObject;
NodeGraphicsObject::NodeGraphicsObject(FlowScene &scene, Node &node) : _scene(scene), _node(node), _locked(false), _proxyWidget(nullptr)
{
_scene.addItem(this);
setFlag(QGraphicsItem::ItemDoesntPropagateOpacityToChildren, true);
setFlag(QGraphicsItem::ItemIsMovable, true);
setFlag(QGraphicsItem::ItemIsFocusable, true);
setFlag(QGraphicsItem::ItemIsSelectable, true);
setFlag(QGraphicsItem::ItemSendsScenePositionChanges, true);
setCacheMode(QGraphicsItem::DeviceCoordinateCache);
auto const &nodeStyle = node.nodeDataModel()->nodeStyle();
{
auto effect = new QGraphicsDropShadowEffect;
effect->setOffset(4, 4);
effect->setBlurRadius(20);
effect->setColor(nodeStyle.ShadowColor);
setGraphicsEffect(effect);
}
setOpacity(nodeStyle.Opacity);
setAcceptHoverEvents(true);
setZValue(0);
embedQWidget(true);
// connect to the move signals to emit the move signals in FlowScene
auto onMoveSlot = [this] {
_scene.nodeMoved(_node, pos());
};
connect(this, &QGraphicsObject::xChanged, this, onMoveSlot);
connect(this, &QGraphicsObject::yChanged, this, onMoveSlot);
}
NodeGraphicsObject::~NodeGraphicsObject()
{
_scene.removeItem(this);
}
Node &NodeGraphicsObject::node()
{
return _node;
}
Node const &NodeGraphicsObject::node() const
{
return _node;
}
void NodeGraphicsObject::embedQWidget(bool embed)
{
NodeGeometry &geom = _node.nodeGeometry();
_node.nodeDataModel()->setWembed(embed);
if (auto w = _node.nodeDataModel()->embeddedWidget())
{
if (embed)
{
if (nullptr == _proxyWidget)
{
_proxyWidget = new QGraphicsProxyWidget(this);
w->setParent(nullptr);
_proxyWidget->setWidget(w);
_proxyWidget->setPreferredWidth(5);
geom.recalculateSize();
if (w->sizePolicy().verticalPolicy() & QSizePolicy::ExpandFlag)
{
// If the widget wants to use as much vertical space as possible, set
// it to have the geom's equivalentWidgetHeight.
_proxyWidget->setMinimumHeight(geom.equivalentWidgetHeight());
}
_proxyWidget->setPos(geom.widgetPosition());
update();
_proxyWidget->setOpacity(1.0);
_proxyWidget->setFlag(QGraphicsItem::ItemIgnoresParentOpacity);
}
}
else
{
if (nullptr != _proxyWidget)
{
_proxyWidget->setWidget(nullptr);
QPoint pos = QCursor::pos();
_proxyWidget->deleteLater();
_proxyWidget = nullptr;
connect(this, SIGNAL(destroyed()), w, SLOT(deleteLater()));
geom.recalculateSize();
update();
w->setWindowTitle(_node.nodeDataModel()->caption());
w->setWindowFlags(Qt::Widget);
w->move(pos.x(), pos.y());
w->show();
w->raise();
}
}
}
moveConnections();
scene()->update();
}
QRectF NodeGraphicsObject::boundingRect() const
{
return _node.nodeGeometry().boundingRect();
}
void NodeGraphicsObject::setGeometryChanged()
{
prepareGeometryChange();
}
void NodeGraphicsObject::moveConnections() const
{
NodeState const &nodeState = _node.nodeState();
for (PortType portType : { PortType::In, PortType::Out })
{
auto const &connectionEntries = nodeState.getEntries(portType);
for (auto const &connections : connectionEntries)
{
for (auto &con : connections) con.second->getConnectionGraphicsObject().move();
}
}
}
void NodeGraphicsObject::lock(bool locked)
{
_locked = locked;
setFlag(QGraphicsItem::ItemIsMovable, !locked);
setFlag(QGraphicsItem::ItemIsFocusable, !locked);
setFlag(QGraphicsItem::ItemIsSelectable, !locked);
}
void NodeGraphicsObject::paint(QPainter *painter, QStyleOptionGraphicsItem const *option, QWidget *)
{
painter->setClipRect(option->exposedRect);
NodePainter::paint(painter, _node, _scene);
}
QVariant NodeGraphicsObject::itemChange(GraphicsItemChange change, const QVariant &value)
{
if (change == ItemPositionChange && scene())
{
moveConnections();
}
return QGraphicsItem::itemChange(change, value);
}
void NodeGraphicsObject::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if (_locked)
return;
// deselect all other items after this one is selected
if (!isSelected() && !(event->modifiers() & Qt::ControlModifier))
{
_scene.clearSelection();
}
for (PortType portToCheck : { PortType::In, PortType::Out })
{
NodeGeometry const &nodeGeometry = _node.nodeGeometry();
// TODO do not pass sceneTransform
int const portIndex = nodeGeometry.checkHitScenePoint(portToCheck, event->scenePos(), sceneTransform());
if (portIndex != INVALID)
{
NodeState const &nodeState = _node.nodeState();
std::unordered_map<QUuid, Connection *> connections = nodeState.connections(portToCheck, portIndex);
// start dragging existing connection
if (!connections.empty() && portToCheck == PortType::In)
{
auto con = connections.begin()->second;
NodeConnectionInteraction interaction(_node, *con, _scene);
interaction.disconnect(portToCheck);
}
else // initialize new Connection
{
if (portToCheck == PortType::Out)
{
auto const outPolicy = _node.nodeDataModel()->portOutConnectionPolicy(portIndex);
if (!connections.empty() && outPolicy == NodeDataModel::ConnectionPolicy::One)
{
_scene.deleteConnection(*connections.begin()->second);
}
}
// todo add to FlowScene
auto connection = _scene.createConnection(portToCheck, _node, portIndex);
_node.nodeState().setConnection(portToCheck, portIndex, *connection);
connection->getConnectionGraphicsObject().grabMouse();
}
}
}
auto pos = event->pos();
auto &geom = _node.nodeGeometry();
auto &state = _node.nodeState();
if (_node.nodeDataModel()->resizable() && geom.resizeRect().contains(QPoint(pos.x(), pos.y())))
{
state.setResizing(true);
}
}
void NodeGraphicsObject::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
auto &geom = _node.nodeGeometry();
auto &state = _node.nodeState();
if (state.resizing())
{
auto diff = event->pos() - event->lastPos();
if (qAbs(diff.x()) < 1 && qAbs(diff.y()) < 1)
{
event->ignore();
return;
}
if (auto w = _node.nodeDataModel()->embeddedWidget())
{
prepareGeometryChange();
const QSize size = w->size();
QSize newSize = size + QSize(diff.x(), diff.y());
const QSize minSize = geom.minimumEmbeddedSize();
const QSize maxSize = geom.maximumEmbeddedSize();
if ((newSize.width() < minSize.width() && newSize.height() < minSize.height()) ||
(newSize.width() > maxSize.width() && newSize.height() > maxSize.height()))
{
event->ignore();
return;
}
newSize = newSize.expandedTo(minSize);
newSize = newSize.boundedTo(maxSize);
w->resize(newSize);
geom.recalculateSize();
_proxyWidget->setMinimumSize(newSize);
_proxyWidget->setMaximumSize(newSize);
_proxyWidget->setPos(geom.widgetPosition());
update();
moveConnections();
event->accept();
}
}
else
{
QGraphicsObject::mouseMoveEvent(event);
if (event->lastPos() != event->pos())
moveConnections();
event->ignore();
}
QRectF r = scene()->sceneRect();
r = r.united(mapToScene(boundingRect()).boundingRect());
scene()->setSceneRect(r);
}
void NodeGraphicsObject::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
auto &state = _node.nodeState();
state.setResizing(false);
QGraphicsObject::mouseReleaseEvent(event);
// position connections precisely after fast node move
moveConnections();
_scene.nodeClicked(node());
}
void NodeGraphicsObject::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
// bring all the colliding nodes to background
QList<QGraphicsItem *> overlapItems = collidingItems();
for (QGraphicsItem *item : overlapItems)
{
if (item->zValue() > 0.0)
{
item->setZValue(0.0);
}
}
// bring this node forward
setZValue(1.0);
if (auto w = _node.nodeDataModel()->embeddedWidget())
{
w->raise();
}
_node.nodeGeometry().setHovered(true);
_node.nodeDataModel()->onNodeHoverEnter();
update();
_scene.nodeHovered(node(), event->screenPos());
event->accept();
}
void NodeGraphicsObject::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
_node.nodeGeometry().setHovered(false);
_node.nodeDataModel()->onNodeHoverLeave();
update();
_scene.nodeHoverLeft(node());
event->accept();
}
void NodeGraphicsObject::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
{
auto pos = event->pos();
auto &geom = _node.nodeGeometry();
if (_node.nodeDataModel()->resizable() && geom.resizeRect().contains(QPoint(pos.x(), pos.y())))
{
setCursor(QCursor(Qt::SizeFDiagCursor));
}
else
{
setCursor(QCursor());
}
event->accept();
}
void NodeGraphicsObject::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsItem::mouseDoubleClickEvent(event);
_scene.nodeDoubleClicked(node());
}
void NodeGraphicsObject::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
_scene.nodeContextMenu(node(), mapToScene(event->pos()));
}
NodeGraphicsObject
是 QtNodes 节点编辑器中的核心图形交互类,负责节点的可视化渲染、用户输入处理以及与 FlowScene
的交互。
主要成员变量
-
_scene
: 节点所属的 FlowScene 引用 -
_node
: 关联的 Node 对象引用 -
_locked
: 节点是否被锁定(不可交互) -
_proxyWidget
: 用于嵌入 QWidget 的代理对象
核心功能
1 构造函数与初始化
构造函数初始化节点图形对象:
-
将自身添加到场景中
-
设置各种图形项标志(可移动、可选择、可聚焦等)
-
添加阴影效果
-
设置初始透明度
-
启用悬停事件
-
初始化嵌入小部件
2 节点连接管理
moveConnections()
方法:
-
遍历节点的所有输入输出端口
-
更新与这些端口相连的所有连接的位置
-
确保连接始终跟随节点移动
3 嵌入小部件管理
embedQWidget()
方法:
-
控制是否将数据模型中的小部件嵌入到节点中
-
创建/销毁 QGraphicsProxyWidget
-
处理小部件的显示/隐藏和位置调整
-
更新节点几何尺寸
4 事件处理
鼠标事件:
-
mousePressEvent
: 处理节点选择和连接创建 -
mouseMoveEvent
: 处理节点移动和大小调整 -
mouseReleaseEvent
: 完成交互操作
悬停事件:
-
hoverEnterEvent
: 节点悬停时提升Z值并触发相关信号 -
hoverLeaveEvent
: 节点离开时恢复状态 -
hoverMoveEvent
: 检测是否在调整大小区域
其他事件:
-
contextMenuEvent
: 触发节点上下文菜单 -
mouseDoubleClickEvent
: 处理双击事件
5 绘制功能
paint()
方法委托给 NodePainter
类完成实际绘制工作,包括:
-
节点背景
-
标题栏
-
端口
-
连接点
-
嵌入小部件区域
重要交互逻辑
1 连接创建
当用户点击端口时:
-
对于输入端口:如果已有连接,则断开它
-
对于输出端口:根据连接策略(单/多连接)处理
-
创建新连接并让连接对象捕获鼠标
2 节点移动
-
通过重写
itemChange()
监听位置变化 -
移动时更新所有连接的路径
-
触发场景的节点移动信号
3 大小调整
当数据模型支持调整大小时:
-
检测鼠标是否在调整区域
-
计算新尺寸并限制在最小/最大范围内
-
调整嵌入小部件大小
-
更新节点几何信息
核心功能详解
1. 节点渲染与样式管理
NodeGraphicsObject
负责节点的绘制,但实际绘制逻辑委托给 NodePainter
类完成。
1.1 绘制流程
-
paint()
方法:void NodeGraphicsObject::paint(QPainter *painter, QStyleOptionGraphicsItem const *option, QWidget *) { painter->setClipRect(option->exposedRect); NodePainter::paint(painter, _node, _scene); // 委托给 NodePainter }
-
调用
NodePainter::paint()
完成实际绘制:-
背景(圆角矩形)
-
标题栏(带颜色)
-
输入/输出端口(圆形标记)
-
嵌入的
QWidget
(如果有)
-
-
受
NodeDataModel
的nodeStyle()
控制样式(颜色、阴影等)。
-
1.2 阴影效果
在构造函数中设置阴影:
auto effect = new QGraphicsDropShadowEffect; effect->setOffset(4, 4); // 阴影偏移 effect->setBlurRadius(20); // 模糊半径 effect->setColor(nodeStyle.ShadowColor); // 阴影颜色 setGraphicsEffect(effect);
-
提升节点的视觉层次感。
2. 节点交互
2.1 鼠标事件
mousePressEvent
(鼠标按下)
-
检测点击位置:
-
如果点在 端口 上:
-
输入端口(PortType::In):
-
如果已有连接,则断开(
NodeConnectionInteraction::disconnect()
)。
-
-
输出端口(PortType::Out):
-
如果连接策略是
ConnectionPolicy::One
,则删除旧连接。 -
创建新连接(
FlowScene::createConnection()
)。
-
-
-
如果点在 调整大小区域(右下角):
-
标记
_node.nodeState().setResizing(true)
,准备调整大小。
-
-
mouseMoveEvent
(鼠标移动)
-
调整节点大小(如果
resizing
):if (state.resizing()) { QSize newSize = w->size() + QSize(diff.x(), diff.y()); newSize = newSize.expandedTo(minSize); // 不小于最小值 newSize = newSize.boundedTo(maxSize); // 不大于最大值 w->resize(newSize); // 调整嵌入的 QWidget geom.recalculateSize(); // 重新计算节点尺寸 moveConnections(); // 更新连接位置 }
-
移动节点:
-
调用
QGraphicsObject::mouseMoveEvent
移动节点。 -
触发
moveConnections()
更新所有连接位置。
-
mouseReleaseEvent
(鼠标释放)
-
结束调整大小或移动操作。
-
触发
FlowScene::nodeClicked()
通知场景。
2.2 悬停事件
hoverEnterEvent
(鼠标进入节点)
-
提升节点 Z 值(
setZValue(1.0)
),使其显示在其他节点之上。 -
调用
NodeDataModel::onNodeHoverEnter()
通知模型。 -
触发
FlowScene::nodeHovered()
。
hoverLeaveEvent
(鼠标离开节点)
-
恢复 Z 值(
setZValue(0.0)
)。 -
调用
NodeDataModel::onNodeHoverLeave()
。 -
触发
FlowScene::nodeHoverLeft()
。
hoverMoveEvent
(鼠标在节点上移动)
-
检测是否在 调整大小区域:
-
是:显示
Qt::SizeFDiagCursor
(对角线调整光标)。 -
否:恢复默认光标。
-
2.3 其他交互
contextMenuEvent
(右键菜单)
-
触发
FlowScene::nodeContextMenu()
,允许自定义节点右键菜单。
mouseDoubleClickEvent
(双击节点)
-
触发
FlowScene::nodeDoubleClicked()
,可用于打开节点属性面板等。
3. 连接管理
moveConnections()
方法
-
遍历节点的所有输入/输出端口:
for (PortType portType : { PortType::In, PortType::Out }) { auto entries = nodeState.getEntries(portType); for (auto &connections : entries) { for (auto &con : connections) { con.second->getConnectionGraphicsObject().move(); // 更新连接路径 } } }
-
确保连接线始终正确连接到端口位置。
4. 嵌入 QWidget
embedQWidget(bool embed)
-
嵌入模式(
embed=true
):-
创建
QGraphicsProxyWidget
,将NodeDataModel::embeddedWidget()
嵌入到节点中。 -
调整代理控件位置和大小:
_proxyWidget->setPos(geom.widgetPosition()); _proxyWidget->setMinimumHeight(geom.equivalentWidgetHeight());
-
-
浮动窗口模式(
embed=false
):-
移除
QGraphicsProxyWidget
,将QWidget
显示为独立窗口:w->setWindowFlags(Qt::Widget); w->move(QCursor::pos()); // 移动到鼠标位置 w->show();
-
5. 节点锁定
lock(bool locked)
-
锁定节点后禁止交互:
void NodeGraphicsObject::lock(bool locked) { _locked = locked; setFlag(QGraphicsItem::ItemIsMovable, !locked); // 禁止移动 setFlag(QGraphicsItem::ItemIsFocusable, !locked); // 禁止聚焦 setFlag(QGraphicsItem::ItemIsSelectable, !locked); // 禁止选择 }
6. 边界计算
boundingRect()
-
返回
NodeGeometry::boundingRect()
,决定节点的碰撞检测和绘制区域:QRectF NodeGraphicsObject::boundingRect() const { return _node.nodeGeometry().boundingRect(); }
总结
功能 | 关键方法 | 说明 |
---|---|---|
节点渲染 | paint() | 委托 NodePainter 绘制 |
鼠标交互 | mousePress/Move/ReleaseEvent | 处理连接创建、移动、调整大小 |
悬停效果 | hoverEnter/Leave/MoveEvent | 提升 Z 值、更新光标样式 |
连接管理 | moveConnections() | 确保连接跟随节点移动 |
嵌入 QWidget | embedQWidget() | 支持内嵌或浮动窗口模式 |
节点锁定 | lock() | 禁止交互 |
右键菜单 | contextMenuEvent() | 触发自定义菜单 |
设计特点
-
职责分离:将绘制逻辑委托给
NodePainter
,保持类职责单一 -
事件转发:通过信号将重要事件转发给 FlowScene
-
灵活嵌入:支持将任意 QWidget 嵌入到节点中
-
连接管理:自动维护节点与连接的图形关系
-
状态感知:响应节点的各种状态变化(悬停、选中等)
这个类是节点编辑器可视化部分的核心,桥接了数据模型(Node/NodeDataModel)和用户界面,提供了丰富的交互能力。