Qt编程:QNodeEditor中的NodeGeometry

NodeGeometry 是 QtNodes 框架中负责管理节点几何信息的核心类,它处理所有与节点尺寸、位置和布局相关的计算。

主要职责

  1. 尺寸管理:计算和维护节点的宽度、高度及各部分尺寸

  2. 端口定位:计算端口在场景中的精确位置

  3. 碰撞检测:检测点是否命中端口

  4. 布局计算:处理节点内部各元素的布局(标题、端口、嵌入部件等)

  5. 动态调整:根据内容变化重新计算几何尺寸

#pragma once
#include "Export.hpp"
#include "PortType.hpp"
#include "memory.hpp"

#include <QtCore/QPointF>
#include <QtCore/QRectF>
#include <QtGui/QFontMetrics>
#include <QtGui/QTransform>
namespace QtNodes
{
    class NodeState;
    class NodeDataModel;
    class Node;
    class NODE_EDITOR_PUBLIC NodeGeometry
    {
      public:
        NodeGeometry(std::unique_ptr<NodeDataModel> const &dataModel);

      public:
        unsigned int height() const
        {
            return _height;
        }
        void setHeight(unsigned int h)
        {
            _height = h;
        }
        unsigned int width() const
        {
            return _width;
        }
        void setWidth(unsigned int w)
        {
            _width = w;
        }
        void setEntryHeight(unsigned int h)
        {
            _entryHeight = h;
        }
        unsigned int entryWidth() const
        {
            return _entryWidth;
        }
        void setEntryWidth(unsigned int w)
        {
            _entryWidth = w;
        }
        unsigned int entryHeight() const
        {
            return _entryHeight;
        }
        unsigned int spacing() const
        {
            return _spacing;
        }
        void setSpacing(unsigned int s)
        {
            _spacing = s;
        }
        bool hovered() const
        {
            return _hovered;
        }
        void setHovered(unsigned int h)
        {
            _hovered = h;
        }
        unsigned int nSources() const;
        unsigned int nSinks() const;
        QPointF const &draggingPos() const
        {
            return _draggingPos;
        }
        void setDraggingPosition(QPointF const &pos)
        {
            _draggingPos = pos;
        }

      public:
        QRectF entryBoundingRect() const;
        QRectF boundingRect() const;
        /// Updates size unconditionally
        void recalculateSize() const;
        /// Updates size if the QFontMetrics is changed
        void recalculateSize(QFont const &font) const;
        QSize minimumEmbeddedSize() const;
        QSize maximumEmbeddedSize() const;
        // TODO removed default QTransform()
        QPointF portScenePosition(PortIndex index, PortType portType, QTransform const &t = QTransform()) const;
        PortIndex checkHitScenePoint(PortType portType, QPointF point, QTransform const &t = QTransform()) const;
        QRect resizeRect() const;
        /// Returns the position of a widget on the Node surface
        QPointF widgetPosition() const;
        /// Returns the maximum height a widget can be without causing the node to
        /// grow.
        int equivalentWidgetHeight() const;
        unsigned int validationHeight() const;
        unsigned int validationWidth() const;
        static QPointF calculateNodePositionBetweenNodePorts(PortIndex targetPortIndex, PortType targetPort, Node *targetNode,
                                                             PortIndex sourcePortIndex, PortType sourcePort, Node *sourceNode, Node &newNode);

      private:
        unsigned int captionHeight() const;
        unsigned int captionWidth() const;
        unsigned int portWidth(PortType portType) const;

      private:
        // some variables are mutable because
        // we need to change drawing metrics
        // corresponding to fontMetrics
        // but this doesn't change constness of Node
        mutable unsigned int _width;
        mutable unsigned int _height;
        unsigned int _entryWidth;
        mutable unsigned int _inputPortWidth;
        mutable unsigned int _outputPortWidth;
        mutable unsigned int _entryHeight;
        unsigned int _spacing;
        bool _hovered;
        unsigned int _nSources;
        unsigned int _nSinks;
        QPointF _draggingPos;
        std::unique_ptr<NodeDataModel> const &_dataModel;
        mutable QFontMetrics _fontMetrics;
        mutable QFontMetrics _boldFontMetrics;
    };
} // namespace QtNodes
#include "NodeGeometry.hpp"

#include "Node.hpp"
#include "NodeDataModel.hpp"
#include "NodeGraphicsObject.hpp"
#include "NodeState.hpp"
#include "PortType.hpp"
#include "StyleCollection.hpp"

#include <cmath>
#include <iostream>
using QtNodes::Node;
using QtNodes::NodeDataModel;
using QtNodes::NodeGeometry;
using QtNodes::PortIndex;
using QtNodes::PortType;
NodeGeometry::NodeGeometry(std::unique_ptr<NodeDataModel> const &dataModel)
    : _width(100), _height(150), _inputPortWidth(70), _outputPortWidth(70), _entryHeight(20), _spacing(20), _hovered(false),
      _nSources(dataModel->nPorts(PortType::Out)), _nSinks(dataModel->nPorts(PortType::In)), _draggingPos(-1000, -1000), _dataModel(dataModel),
      _fontMetrics(QFont()), _boldFontMetrics(QFont())
{
    QFont f;
    f.setBold(true);
    _boldFontMetrics = QFontMetrics(f);
}
unsigned int NodeGeometry::nSources() const
{
    return _dataModel->nPorts(PortType::Out);
}
unsigned int NodeGeometry::nSinks() const
{
    return _dataModel->nPorts(PortType::In);
}
QRectF NodeGeometry::entryBoundingRect() const
{
    double const addon = 0.0;
    return QRectF(0 - addon, 0 - addon, _entryWidth + 2 * addon, _entryHeight + 2 * addon);
}
QRectF NodeGeometry::boundingRect() const
{
    auto const &nodeStyle = StyleCollection::nodeStyle();
    double hAddon = 1 * nodeStyle.ConnectionPointDiameter;
    double vAddon = 2 * nodeStyle.ConnectionPointDiameter;
    return QRectF(0 - vAddon, 0 - hAddon, _width + 2 * vAddon, _height + 2 * hAddon);
}
void NodeGeometry::recalculateSize() const
{
    _entryHeight = _fontMetrics.height();
    {
        unsigned int maxNumOfEntries = std::max(_nSinks, _nSources);
        unsigned int step = _entryHeight + _spacing;
        _height = step * maxNumOfEntries;
    }
    if (_dataModel->wembed())
        if (auto w = _dataModel->embeddedWidget())
        {
            _height = std::max(_height, static_cast<unsigned>(w->height()));
        }
    _height += captionHeight();
    _inputPortWidth = portWidth(PortType::In);
    _outputPortWidth = portWidth(PortType::Out);
    _width = _inputPortWidth + _outputPortWidth + 2 * _spacing;
    if (_dataModel->wembed())
        if (auto w = _dataModel->embeddedWidget())
        {
            _width += w->width();
        }
    _width = std::max(_width, captionWidth());
    if (_dataModel->validationState() != NodeValidationState::Valid)
    {
        _width = std::max(_width, validationWidth());
        _height += validationHeight() + _spacing;
    }
}
void NodeGeometry::recalculateSize(QFont const &font) const
{
    QFontMetrics fontMetrics(font);
    QFont boldFont = font;
    boldFont.setBold(true);
    QFontMetrics boldFontMetrics(boldFont);
    if (_boldFontMetrics != boldFontMetrics)
    {
        _fontMetrics = fontMetrics;
        _boldFontMetrics = boldFontMetrics;
        recalculateSize();
    }
}
QSize NodeGeometry::minimumEmbeddedSize() const
{
    const unsigned int maxNumOfEntries = std::max(_nSinks, _nSources);
    const unsigned int step = _fontMetrics.height() + _spacing;
    unsigned int height = step * maxNumOfEntries;
    unsigned int width = 0;
    if (auto w = _dataModel->embeddedWidget())
    {
        height = std::max(height, static_cast<unsigned>(w->minimumHeight()));
        width = std::max(width, static_cast<unsigned>(w->minimumHeight()));
    }
    width = std::max(width, captionWidth());
    if (_dataModel->validationState() != NodeValidationState::Valid)
        width = std::max(width, validationWidth());
    return QSize(width, height);
}
QSize NodeGeometry::maximumEmbeddedSize() const
{
    if (auto w = _dataModel->embeddedWidget())
        return w->maximumSize();
    return QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
}
QPointF NodeGeometry::portScenePosition(PortIndex index, PortType portType, QTransform const &t) const
{
    auto const &nodeStyle = StyleCollection::nodeStyle();
    unsigned int step = _entryHeight + _spacing;
    QPointF result;
    double totalHeight = 0.0;
    totalHeight += captionHeight();
    totalHeight += step * index;
    // TODO: why?
    totalHeight += step / 2.0;
    switch (portType)
    {
        case PortType::Out:
        {
            double x = _width + nodeStyle.ConnectionPointDiameter;
            result = QPointF(x, totalHeight);
            break;
        }
        case PortType::In:
        {
            double x = 0.0 - nodeStyle.ConnectionPointDiameter;
            result = QPointF(x, totalHeight);
            break;
        }
        default: break;
    }
    return t.map(result);
}
PortIndex NodeGeometry::checkHitScenePoint(PortType portType, QPointF const scenePoint, QTransform const &sceneTransform) const
{
    auto const &nodeStyle = StyleCollection::nodeStyle();
    PortIndex result = INVALID;
    if (portType == PortType::None)
        return result;
    double const tolerance = 2.0 * nodeStyle.ConnectionPointDiameter;
    unsigned int const nItems = _dataModel->nPorts(portType);
    for (unsigned int i = 0; i < nItems; ++i)
    {
        auto pp = portScenePosition(i, portType, sceneTransform);
        QPointF p = pp - scenePoint;
        auto distance = std::sqrt(QPointF::dotProduct(p, p));
        if (distance < tolerance)
        {
            result = PortIndex(i);
            break;
        }
    }
    return result;
}
QRect NodeGeometry::resizeRect() const
{
    unsigned int rectSize = 7;
    return QRect(_width - rectSize, _height - rectSize, rectSize, rectSize);
}
QPointF NodeGeometry::widgetPosition() const
{
    if (auto w = _dataModel->embeddedWidget())
    {
        if (w->sizePolicy().verticalPolicy() & QSizePolicy::ExpandFlag)
        {
            // If the widget wants to use as much vertical space as possible, place it
            // immediately after the caption.
            return QPointF(_spacing + portWidth(PortType::In), captionHeight());
        }
        else
        {
            if (_dataModel->validationState() != NodeValidationState::Valid)
            {
                return QPointF(_spacing + portWidth(PortType::In),
                               (captionHeight() + _height - validationHeight() - _spacing - w->height()) / 2.0);
            }
            return QPointF(_spacing + portWidth(PortType::In), (captionHeight() + _height - w->height()) / 2.0);
        }
    }
    return QPointF();
}
int NodeGeometry::equivalentWidgetHeight() const
{
    if (_dataModel->validationState() != NodeValidationState::Valid)
    {
        return height() - captionHeight() + validationHeight();
    }
    return height() - captionHeight();
}
unsigned int NodeGeometry::captionHeight() const
{
    if (!_dataModel->captionVisible())
        return 0;
    QString name = _dataModel->caption();
    return _boldFontMetrics.boundingRect(name).height();
}
unsigned int NodeGeometry::captionWidth() const
{
    if (!_dataModel->captionVisible())
        return 0;
    QString name = _dataModel->caption();
    return _boldFontMetrics.boundingRect(name).width();
}
unsigned int NodeGeometry::validationHeight() const
{
    QString msg = _dataModel->validationMessage();
    return _boldFontMetrics.boundingRect(msg).height();
}
unsigned int NodeGeometry::validationWidth() const
{
    QString msg = _dataModel->validationMessage();
    return _boldFontMetrics.boundingRect(msg).width();
}
QPointF NodeGeometry::calculateNodePositionBetweenNodePorts(PortIndex targetPortIndex, PortType targetPort, Node *targetNode,
                                                            PortIndex sourcePortIndex, PortType sourcePort, Node *sourceNode, Node &newNode)
{
    // Calculating the nodes position in the scene. It'll be positioned half way
    // between the two ports that it "connects". The first line calculates the
    // halfway point between the ports (node position + port position on the node
    // for both nodes averaged). The second line offsets this coordinate with the
    // size of the new node, so that the new nodes center falls on the originally
    // calculated coordinate, instead of it's upper left corner.
    auto converterNodePos =
        (sourceNode->nodeGraphicsObject().pos() + sourceNode->nodeGeometry().portScenePosition(sourcePortIndex, sourcePort) +
         targetNode->nodeGraphicsObject().pos() + targetNode->nodeGeometry().portScenePosition(targetPortIndex, targetPort)) /
        2.0f;
    converterNodePos.setX(converterNodePos.x() - newNode.nodeGeometry().width() / 2.0f);
    converterNodePos.setY(converterNodePos.y() - newNode.nodeGeometry().height() / 2.0f);
    return converterNodePos;
}
unsigned int NodeGeometry::portWidth(PortType portType) const
{
    unsigned width = 0;
    for (auto i = 0ul; i < _dataModel->nPorts(portType); ++i)
    {
        QString name;
        if (_dataModel->portCaptionVisible(portType, i))
        {
            name = _dataModel->portCaption(portType, i);
        }
        else
        {
            name = _dataModel->dataType(portType, i)->name();
        }
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
        width = std::max(unsigned(_fontMetrics.horizontalAdvance(name)), width);
#else
        width = std::max(unsigned(_fontMetrics.width(name)), width);
#endif
    }
    return width;
}

核心数据结构

mutable unsigned int _width;        // 节点宽度
mutable unsigned int _height;       // 节点高度
unsigned int _entryWidth;           // 条目宽度
mutable unsigned int _inputPortWidth;  // 输入端口区域宽度
mutable unsigned int _outputPortWidth; // 输出端口区域宽度
mutable unsigned int _entryHeight;   // 单个条目高度
unsigned int _spacing;              // 间距
bool _hovered;                     // 悬停状态
unsigned int _nSources;            // 输出端口数量
unsigned int _nSinks;              // 输入端口数量
QPointF _draggingPos;              // 拖拽位置
std::unique_ptr<NodeDataModel> const &_dataModel; // 关联的数据模型
mutable QFontMetrics _fontMetrics;  // 字体度量
mutable QFontMetrics _boldFontMetrics; // 粗体字体度量

关键方法分析

1. 尺寸计算 (recalculateSize)

void NodeGeometry::recalculateSize() const
{
    // 基本高度计算(基于端口数量)
    _entryHeight = _fontMetrics.height();
    unsigned int maxNumOfEntries = std::max(_nSinks, _nSources);
    unsigned int step = _entryHeight + _spacing;
    _height = step * maxNumOfEntries;
    
    // 考虑嵌入部件
    if (auto w = _dataModel->embeddedWidget()) {
        _height = std::max(_height, static_cast<unsigned>(w->height()));
    }
    
    // 添加标题高度
    _height += captionHeight();
    
    // 计算端口区域宽度
    _inputPortWidth = portWidth(PortType::In);
    _outputPortWidth = portWidth(PortType::Out);
    
    // 总宽度计算
    _width = _inputPortWidth + _outputPortWidth + 2 * _spacing;
    
    // 考虑嵌入部件宽度
    if (auto w = _dataModel->embeddedWidget()) {
        _width += w->width();
    }
    
    // 确保宽度足够显示标题
    _width = std::max(_width, captionWidth());
    
    // 考虑验证信息区域
    if (_dataModel->validationState() != NodeValidationState::Valid) {
        _width = std::max(_width, validationWidth());
        _height += validationHeight() + _spacing;
    }
}

2. 端口位置计算 (portScenePosition)

QPointF NodeGeometry::portScenePosition(PortIndex index, PortType portType, QTransform const &t) const
{
    unsigned int step = _entryHeight + _spacing;
    double totalHeight = captionHeight() + step * index + step / 2.0;
    
    double x = (portType == PortType::Out) 
        ? _width + nodeStyle.ConnectionPointDiameter 
        : 0.0 - nodeStyle.ConnectionPointDiameter;
    
    return t.map(QPointF(x, totalHeight));
}

3. 碰撞检测 (checkHitScenePoint)

PortIndex NodeGeometry::checkHitScenePoint(PortType portType, QPointF const scenePoint, QTransform const &sceneTransform) const
{
    double const tolerance = 2.0 * nodeStyle.ConnectionPointDiameter;
    for (unsigned int i = 0; i < _dataModel->nPorts(portType); ++i) {
        auto pp = portScenePosition(i, portType, sceneTransform);
        auto distance = std::sqrt(QPointF::dotProduct(pp - scenePoint, pp - scenePoint));
        if (distance < tolerance) return PortIndex(i);
    }
    return INVALID;
}

4. 节点间位置计算 (calculateNodePositionBetweenNodePorts)

QPointF NodeGeometry::calculateNodePositionBetweenNodePorts(...)
{
    // 计算两个端口中间点
    auto converterNodePos = 
        (sourceNodePos + sourcePortPos + targetNodePos + targetPortPos) / 2.0f;
    
    // 调整使新节点中心对准计算点
    converterNodePos -= QPointF(newNode.width()/2.0f, newNode.height()/2.0f);
    
    return converterNodePos;
}

设计特点

  1. mutable 成员:允许在 const 方法中修改绘制相关度量值,保持逻辑 const 性

  2. 字体度量缓存:缓存普通和粗体字体的度量信息,提高性能

  3. 动态布局:根据内容动态调整尺寸(端口数量、嵌入部件、验证信息等)

  4. 精确碰撞检测:支持基于场景坐标的精确命中测试

  5. 坐标转换支持:所有位置计算方法都支持 QTransform 参数

与其他类的协作

  1. NodeDataModel

    • 获取端口数量和配置

    • 获取验证状态和信息

    • 获取嵌入部件信息

    • 获取标题和端口标签内容

  2. NodeGraphicsObject

    • 提供场景位置信息

    • 处理拖拽位置

  3. StyleCollection

    • 获取连接点直径等样式信息

  4. NodePainter

    • 提供绘制所需的几何信息

    • 确定各元素的位置和尺寸

使用场景示例

  1. 节点创建时:初始化几何尺寸

  2. 内容变化时:重新计算尺寸(如端口数量变化、验证状态变化)

  3. 绘制时:获取各元素的位置信息

  4. 交互时:进行命中测试确定点击的端口

  5. 节点连接时:计算新节点的最佳位置

这个类的设计使得节点的几何管理集中化,同时保持足够的灵活性以适应各种节点类型和内容变化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值