最近我在学习Qt的图形视图框架时,发现坐标系统和鼠标事件处理是初学者最容易混淆的部分。通过实践,我总结了一些经验,希望能帮助到同样在学习这个框架的朋友们。
一、图形视图框架的三个核心概念
在Qt的图形视图框架中,主要有三个核心类需要我们理解:
- QGraphicsView(视图):显示场景的窗口,用户可以与之交互
- QGraphicsScene(场景):管理所有图形项的容器
- QGraphicsItem(图形项):场景中的具体图形元素
想象一下,这就像一个舞台剧:
- Scene(场景) 是整个舞台
- Item(图形项) 是舞台上的演员和道具
- View(视图) 是观众看到的镜头
二、理解三个坐标系系统
这是最让我困惑的部分,但理解后就会发现很清晰:
1. 视图坐标系(View Coordinates)
以视图窗口的左上角为原点(0,0),向右为x正方向,向下为y正方向,单位是像素。
cpp
// 在鼠标移动事件中,我们获取的就是视图坐标
void MyGraphicsView::mouseMoveEvent(QMouseEvent *event)
{
QPoint point = event->pos(); // 这是视图坐标
emit mouseMove(point);
}
2. 场景坐标系(Scene Coordinates)
场景有自己的坐标系,我们可以自由定义原点和范围。
cpp
// 创建场景时指定坐标系范围
scene = new QGraphicsScene(
-200, -100, // 场景左上角在场景坐标系中的位置
400, 200); // 场景的宽度和高度
3. 图形项坐标系(Item Coordinates)
每个图形项都有自己的局部坐标系,以图形项自身的左上角为原点。
cpp
// 创建矩形图形项,参数是在图形项自身坐标系中的位置和大小
QGraphicsRectItem *item2 = new QGraphicsRectItem(0,0,200,100);
三、坐标转换的实用方法
在实际开发中,我经常需要在不同坐标系间转换:
cpp
// 视图坐标 → 场景坐标
QPointF pointScene = ui->graphicsView->mapToScene(point);
// 场景坐标 → 图形项坐标
QPointF pointItem = item->mapFromScene(pointScene);
// 场景坐标 → 视图坐标
QPoint pointView = ui->graphicsView->mapFromScene(pointScene);
四、自定义视图类处理鼠标事件
我创建了一个自定义视图类来更好地处理鼠标事件:
头文件:mygraphicsview.h
cpp
#ifndef MYGRAPHICSVIEW_H
#define MYGRAPHICSVIEW_H
#include <QGraphicsView>
#include <QMouseEvent>
class MyGraphicsView : public QGraphicsView
{
Q_OBJECT
public:
explicit MyGraphicsView(QWidget *parent = nullptr);
signals:
// 自定义信号,传递鼠标位置(视图坐标)
void mousePress(QPoint point);
void mouseMove(QPoint point);
protected:
// 重写鼠标事件处理函数
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
};
#endif // MYGRAPHICSVIEW_H
实现文件:mygraphicsview.cpp
cpp
#include "mygraphicsview.h"
#include <QMouseEvent>
MyGraphicsView::MyGraphicsView(QWidget *parent) : QGraphicsView(parent)
{
// 启用鼠标跟踪,这样不按鼠标也能触发移动事件
setMouseTracking(true);
}
void MyGraphicsView::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton) {
QPoint point = event->pos(); // 获取鼠标在视图中的位置
emit mousePress(point); // 发射自定义信号
}
// 调用基类处理,确保不影响原有功能
QGraphicsView::mousePressEvent(event);
}
void MyGraphicsView::mouseMoveEvent(QMouseEvent *event)
{
QPoint point = event->pos(); // 获取当前鼠标位置
emit mouseMove(point); // 发射移动信号
QGraphicsView::mouseMoveEvent(event);
}
五、主窗口中的场景搭建和事件处理
在主窗口中,我创建了场景并添加了图形项:
cpp
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 创建场景
scene = new QGraphicsScene(-200, -100, 400, 200);
ui->graphicsView->setScene(scene);
// 创建底层蓝色边框矩形(作为背景参考)
QGraphicsRectItem *item1 = new QGraphicsRectItem(-200, -100, 400, 200);
item1->setPos(0,0); // 设置图形项在场景中的位置
QPen pen;
pen.setWidth(2);
pen.setColor(Qt::blue);
item1->setPen(pen);
scene->addItem(item1);
// 创建可移动的红色矩形
QGraphicsRectItem *item2 = new QGraphicsRectItem(0,0,200,100);
item2->setPos(-200,-100); // 在场景中的位置
item2->setBrush(QBrush(Qt::red));
// 设置图形项属性:可选择、可聚焦、可移动
item2->setFlags(QGraphicsItem::ItemIsSelectable |
QGraphicsItem::ItemIsFocusable |
QGraphicsItem::ItemIsMovable);
scene->addItem(item2);
// 创建绿色矩形作为红色矩形的子项
QGraphicsRectItem *item3 = new QGraphicsRectItem(0,0,100,50);
item3->setBrush(QBrush(Qt::green));
item3->setPos(100,0); // 相对于父项的位置
item3->setParentItem(item2); // 设置为item2的子项
// 连接信号槽
connect(ui->graphicsView, &MyGraphicsView::mousePress,
this, &MainWindow::on_graphicsView_mousePress);
connect(ui->graphicsView, &MyGraphicsView::mouseMove,
this, &MainWindow::on_graphicsView_mouseMove);
}
六、鼠标事件的具体处理
当鼠标在视图上移动或点击时,我需要实时显示不同坐标系下的位置:
cpp
void MainWindow::on_graphicsView_mouseMove(QPoint point)
{
// 显示视图坐标
ui->labelView->setText(QString::asprintf("视图坐标: %d, %d",
point.x(), point.y()));
// 转换为场景坐标并显示
QPointF pointScene = ui->graphicsView->mapToScene(point);
ui->labelScene->setText(QString::asprintf("场景坐标: %.0f, %.0f",
pointScene.x(), pointScene.y()));
}
void MainWindow::on_graphicsView_mousePress(QPoint point)
{
// 视图坐标转场景坐标
QPointF pointScene = ui->graphicsView->mapToScene(point);
// 在场景坐标位置查找图形项
QGraphicsItem *item = scene->itemAt(pointScene,
ui->graphicsView->transform());
if (item) {
// 场景坐标转图形项局部坐标
QPointF pointItem = item->mapFromScene(pointScene);
ui->labelItem->setText(QString::asprintf("Item坐标: %.0f, %.0f",
pointItem.x(), pointItem.y()));
}
}
运行结果:
七、我遇到的重点难点解析
1. setPos() 与图形项构造函数参数的区别
- 构造函数中的坐标是图形项自身坐标系中的位置和大小
- setPos()设置的是图形项原点 在场景坐标系中的位置
2. 图形项的父子关系
子项会跟随父项移动,其坐标是相对于父项的。这在我创建item3作为item2的子项时体现得很明显。
3. 鼠标跟踪的重要性
setMouseTracking(true)让我不用按住鼠标也能获得移动事件,这在需要实时显示坐标时非常有用。
八、总结
通过这个练习,我深刻理解了:
- 三个坐标系要分清:视图、场景、图形项各有各的坐标系
- 坐标转换要熟练:mapToScene()、mapFromScene()等方法是桥梁
- 事件处理要完整:重写事件方法时记得调用父类实现
- 图形项层次要明确:父子关系会影响坐标计算
这个框架虽然概念较多,但一旦理解了坐标系统的原理,使用起来就会得心应手。希望我的学习笔记对大家有所帮助!