1. Qt Graphics View 坐标系基础解析
第一次接触Qt Graphics View框架时,最让我困惑的就是那套"三重坐标系"系统。记得当时在实现一个简单的流程图工具时,鼠标点击位置和实际图元位置总是对不上,调试了整整两天才发现是坐标转换出了问题。今天我们就从最基础的坐标系定义开始,用实际案例帮你避开这些坑。
Graphics View框架包含三个核心坐标系,它们像俄罗斯套娃一样层层嵌套:
- 场景坐标系(Scene Coordinates) :相当于整个世界地图,所有图元都存在于这个空间。原点默认在场景中心,X轴向右为正,Y轴向下为正(注意不是数学坐标系)。创建场景时可以这样设置范围:
QGraphicsScene *scene = new QGraphicsScene(-400, -300, 800, 600);
// 相当于定义了一张左上角在(-400,-300),宽800高600的"画布"
-
视图坐标系(View Coordinates) :就是显示在屏幕上的窗口坐标系。左上角永远是(0,0),单位是像素。所有鼠标事件最初获取的都是视图坐标。
-
图元坐标系(Item Coordinates) :每个图元自己的局部坐标系,通常以图元中心为原点。这是最容易被忽视但最重要的坐标系,比如当你要重写paint()函数时:
void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
painter->drawRect(0, 0, 100, 50); // 这里的坐标是相对于图元自身的
}
提示:新手最容易犯的错误就是混淆这三个坐标系。记住一个原则:图元创建时用场景坐标,绘制时用图元坐标,交互时需要进行坐标转换。
2. 关键坐标转换函数实战指南
在实际项目中,我整理了一套坐标转换的"生存法则"。这些函数用好了能事半功倍,用错了就是灾难现场。
2.1 基础转换函数
- mapToScene() / mapFromScene() :视图与场景之间的转换
// 把视图中的鼠标位置转换为场景坐标
QPointF scenePos = view->mapToScene(event->pos());
// 反过来将场景坐标转换为视图坐标
QPoint viewPos = view->mapFromScene(QPointF(100.5, 200.3));
- mapToItem() / mapFromItem() :图元之间的转换
// 将当前图元的(0,0)点转换为目标图元的坐标系
QPointF targetPos = this->mapToItem(targetItem, QPointF(0, 0));
// 检测两个图元是否碰撞
if (item1->mapToScene(item1->boundingRect())
.intersects(item2->mapToScene(item2->boundingRect()))) {
// 碰撞处理逻辑
}
2.2 高级技巧:链式转换
处理复杂交互时,经常需要多级转换。比如要实现"在视图上框选多个图元"的功能:
// 将视图中的选择矩形转换为场景坐标
QRectF sceneRect = view->mapToScene(selectionRect).boundingRect();
// 再转换为某个图元的局部坐标
QRectF itemRect = someItem->mapFromScene(sceneRect).boundingRect();
我在实际项目中总结出一个转换口诀:"视图转场景用mapToScene,场景转图元用mapFromScene,图元互转用mapToItem"。
3. 复杂交互中的坐标同步方案
当涉及到图元的拖拽、旋转、缩放时,坐标转换就变得更加复杂。下面分享几个实战中总结的解决方案。
3.1 拖拽实现方案
正确的拖拽处理需要处理好三个坐标系的同步:
void DragItem::mousePressEvent(QGraphicsSceneMouseEvent *event) {
// 记录按下时的图元局部坐标
m_dragStartPos = event->pos();
}
void DragItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
// 计算移动距离(场景坐标)
QPointF sceneMove = event->scenePos() - event->lastScenePos();
// 移动图元(会自动处理坐标转换)
moveBy(sceneMove.x(), sceneMove.y());
}
3.2 缩放与旋转处理
当图元发生变换时,其子图元的坐标会如何变化?看这个例子:
// 父图元旋转45度
parentItem->setRotation(45);
// 子图元位置在父图元坐标系中是(10,0)
childItem->setPos(10, 0);
// 此时子图元的场景坐标会是?
QPointF scenePos = childItem->mapToScene(0, 0);
// 不是简单的(10,0),而是经过旋转变换后的位置!
注意:所有变换操作都是基于图元的局部坐标系。旋转和缩放的中心点默认是图元原点,可以通过setTransformOriginPoint()修改。
4. 性能优化与常见陷阱
在开发图形编辑器时,我踩过不少坐标转换的性能坑。这里分享几个关键优化点:
- 避免频繁转换 :在循环中批量转换坐标,而不是每次使用都转换
// 错误做法(每次循环都转换)
for (auto item : items) {
QRectF sceneRect = item->mapToScene(item->boundingRect()).boundingRect();
// ...
}
// 正确做法(先收集再批量转换)
QList<QGraphicsItem*> itemsToProcess;
// ...收集需要处理的图元...
prepareGeometryChange(); // 通知系统准备批量更新
for (auto item : itemsToProcess) {
// 处理逻辑
}
- 边界矩形缓存 :对于复杂图元,重写boundingRect()时注意:
QRectF ComplexItem::boundingRect() const {
if (m_boundingRect.isNull()) {
// 计算并缓存边界矩形
m_boundingRect = ...;
}
return m_boundingRect;
}
- 常见错误排查 :
- 图元显示位置不对:检查是否混淆了setPos()和moveBy()
- 鼠标点击不准确:确认是否正确处理了视图到场景的坐标转换
- 变换后子图元位置异常:检查transformOriginPoint设置
5. 实战案例:迷你CAD系统开发
让我们通过一个简化版的CAD系统,把前面知识串起来。这个案例支持图元创建、移动和缩放。
5.1 场景初始化
// 创建场景和视图
QGraphicsScene *scene = new QGraphicsScene(-1000, -1000, 2000, 2000);
QGraphicsView *view = new QGraphicsView(scene);
view->setRenderHint(QPainter::Antialiasing);
// 添加网格背景(方便观察坐标)
for (int i = -1000; i <= 1000; i += 50) {
scene->addLine(i, -1000, i, 1000, QPen(Qt::lightGray));
scene->addLine(-1000, i, 1000, i, QPen(Qt::lightGray));
}
5.2 自定义图元类
class CADItem : public QGraphicsRectItem {
public:
CADItem(qreal x, qreal y, qreal w, qreal h)
: QGraphicsRectItem(x, y, w, h) {
setFlag(QGraphicsItem::ItemIsMovable);
setFlag(QGraphicsItem::ItemIsSelectable);
setFlag(QGraphicsItem::ItemSendsGeometryChanges);
}
protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override {
if (event->modifiers() & Qt::ShiftModifier) {
// 按住Shift时限制只能水平移动
QPointF newPos = event->scenePos();
setPos(newPos.x(), pos().y());
} else {
QGraphicsRectItem::mouseMoveEvent(event);
}
}
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override {
if (change == ItemPositionChange && scene()) {
// 位置变化时进行坐标对齐(示例:对齐到网格)
QPointF newPos = value.toPointF();
qreal gridSize = 50;
newPos.setX(round(newPos.x()/gridSize)*gridSize);
newPos.setY(round(newPos.y()/gridSize)*gridSize);
return newPos;
}
return QGraphicsRectItem::itemChange(change, value);
}
};
5.3 坐标调试技巧
开发过程中,可以添加一个实时显示坐标的调试工具:
class CoordinateHUD : public QGraphicsTextItem {
public:
CoordinateHUD(QGraphicsView *view) : m_view(view) {
setZValue(1000);
setDefaultTextColor(Qt::red);
}
void updatePosition() {
QPointF scenePos = m_view->mapToScene(m_view->mapFromGlobal(QCursor::pos()));
setPlainText(QString("View: %1,%2\nScene: %3,%4")
.arg(m_view->mapFromGlobal(QCursor::pos()).x())
.arg(m_view->mapFromGlobal(QCursor::pos()).y())
.arg(scenePos.x())
.arg(scenePos.y()));
setPos(scenePos + QPointF(20, 20));
}
private:
QGraphicsView *m_view;
};
在项目后期,我们遇到了一个棘手的问题:当视图缩放后,图元的选择框会出现偏移。最终发现是因为没有考虑视图的变换矩阵。解决方案是:
QPointF correctSelectionPos(const QGraphicsView *view, const QPointF &pos) {
QTransform viewTransform = view->viewportTransform();
return viewTransform.inverted().map(pos);
}
6. 进阶技巧:自定义坐标系统
对于有特殊需求的场景(比如CAD中的极坐标),可以扩展默认坐标系:
class PolarItem : public QGraphicsItem {
public:
QRectF boundingRect() const override {
return QRectF(-100, -100, 200, 200);
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override {
// 将直角坐标转换为极坐标绘制
for (qreal r = 10; r <= 100; r += 10) {
painter->drawEllipse(QPointF(0, 0), r, r);
for (qreal angle = 0; angle < 360; angle += 30) {
qreal rad = qDegreesToRadians(angle);
QPointF p(r * cos(rad), r * sin(rad));
painter->drawLine(QPointF(0, 0), p);
}
}
}
// 重写shape()实现精确碰撞检测
QPainterPath shape() const override {
QPainterPath path;
for (qreal r = 10; r <= 100; r += 10) {
path.addEllipse(QPointF(0, 0), r, r);
}
return path;
}
};
在处理地图应用时,可能需要处理超大坐标系的性能问题。这时可以采用"区块加载"策略:
void MapView::drawBackground(QPainter *painter, const QRectF &rect) {
// 只加载可见区域的区块
int tileSize = 256;
int xStart = floor(rect.left() / tileSize);
int xEnd = ceil(rect.right() / tileSize);
int yStart = floor(rect.top() / tileSize);
int yEnd = ceil(rect.bottom() / tileSize);
for (int x = xStart; x <= xEnd; ++x) {
for (int y = yStart; y <= yEnd; ++y) {
QRectF tileRect(x * tileSize, y * tileSize, tileSize, tileSize);
if (rect.intersects(tileRect)) {
// 绘制或加载对应区块
}
}
}
}
最后分享一个真实案例:在开发电路图编辑器时,我们需要实现元件的自动对齐功能。通过重写itemChange()和结合场景的items()函数,我们实现了智能吸附效果:
QVariant ComponentItem::itemChange(GraphicsItemChange change, const QVariant &value) {
if (change == ItemPositionChange && scene()) {
QPointF newPos = value.toPointF();
// 查找附近可能吸附的图元
QList<QGraphicsItem*> nearbyItems = scene()->items(
mapToScene(boundingRect()).boundingRect().adjusted(-10, -10, 10, 10));
foreach (QGraphicsItem *item, nearbyItems) {
if (item != this && item->type() == ComponentItem::Type) {
QPointF otherPos = item->pos();
// X轴吸附
if (qAbs(newPos.x() - otherPos.x()) < 5) {
newPos.setX(otherPos.x());
}
// Y轴吸附
if (qAbs(newPos.y() - otherPos.y()) < 5) {
newPos.setY(otherPos.y());
}
}
}
return newPos;
}
return QGraphicsItem::itemChange(change, value);
}



510

被折叠的 条评论
为什么被折叠?



