Qt编程:QNodeEditor中NodeGraphicsObject 类

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 连接创建

当用户点击端口时:

  1. 对于输入端口:如果已有连接,则断开它

  2. 对于输出端口:根据连接策略(单/多连接)处理

  3. 创建新连接并让连接对象捕获鼠标

2 节点移动

  • 通过重写 itemChange() 监听位置变化

  • 移动时更新所有连接的路径

  • 触发场景的节点移动信号

3 大小调整

当数据模型支持调整大小时:

  1. 检测鼠标是否在调整区域

  2. 计算新尺寸并限制在最小/最大范围内

  3. 调整嵌入小部件大小

  4. 更新节点几何信息

核心功能详解

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()确保连接跟随节点移动
嵌入 QWidgetembedQWidget()支持内嵌或浮动窗口模式
节点锁定lock()禁止交互
右键菜单contextMenuEvent()触发自定义菜单

设计特点

  1. 职责分离:将绘制逻辑委托给 NodePainter,保持类职责单一

  2. 事件转发:通过信号将重要事件转发给 FlowScene

  3. 灵活嵌入:支持将任意 QWidget 嵌入到节点中

  4. 连接管理:自动维护节点与连接的图形关系

  5. 状态感知:响应节点的各种状态变化(悬停、选中等)

这个类是节点编辑器可视化部分的核心,桥接了数据模型(Node/NodeDataModel)和用户界面,提供了丰富的交互能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值