NodeGeometry
是 QtNodes 框架中负责管理节点几何信息的核心类,它处理所有与节点尺寸、位置和布局相关的计算。
主要职责
-
尺寸管理:计算和维护节点的宽度、高度及各部分尺寸
-
端口定位:计算端口在场景中的精确位置
-
碰撞检测:检测点是否命中端口
-
布局计算:处理节点内部各元素的布局(标题、端口、嵌入部件等)
-
动态调整:根据内容变化重新计算几何尺寸
#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; }
设计特点
-
mutable 成员:允许在 const 方法中修改绘制相关度量值,保持逻辑 const 性
-
字体度量缓存:缓存普通和粗体字体的度量信息,提高性能
-
动态布局:根据内容动态调整尺寸(端口数量、嵌入部件、验证信息等)
-
精确碰撞检测:支持基于场景坐标的精确命中测试
-
坐标转换支持:所有位置计算方法都支持 QTransform 参数
与其他类的协作
-
NodeDataModel:
-
获取端口数量和配置
-
获取验证状态和信息
-
获取嵌入部件信息
-
获取标题和端口标签内容
-
-
NodeGraphicsObject:
-
提供场景位置信息
-
处理拖拽位置
-
-
StyleCollection:
-
获取连接点直径等样式信息
-
-
NodePainter:
-
提供绘制所需的几何信息
-
确定各元素的位置和尺寸
-
使用场景示例
-
节点创建时:初始化几何尺寸
-
内容变化时:重新计算尺寸(如端口数量变化、验证状态变化)
-
绘制时:获取各元素的位置信息
-
交互时:进行命中测试确定点击的端口
-
节点连接时:计算新节点的最佳位置
这个类的设计使得节点的几何管理集中化,同时保持足够的灵活性以适应各种节点类型和内容变化。