Qt图形视图框架入门:坐标系统与鼠标事件处理详解

最近我在学习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)让我不用按住鼠标也能获得移动事件,这在需要实时显示坐标时非常有用。

八、总结

通过这个练习,我深刻理解了:

  1. 三个坐标系要分清:视图、场景、图形项各有各的坐标系
  2. 坐标转换要熟练:mapToScene()、mapFromScene()等方法是桥梁
  3. 事件处理要完整:重写事件方法时记得调用父类实现
  4. 图形项层次要明确:父子关系会影响坐标计算

这个框架虽然概念较多,但一旦理解了坐标系统的原理,使用起来就会得心应手。希望我的学习笔记对大家有所帮助!

相关推荐
卷无止境1 天前
C++ 的Eigen 库全解析
c++
卷无止境1 天前
现代 C++特性大盘点:一门脱胎换骨的老语言
c++·后端
郝学胜_神的一滴1 天前
CMake 27:缓存变量的特性、语法、类型与实操全解
c++·cmake
Quz2 天前
QML Hello World 入门示例
qt
博客18003 天前
酷宝的使用方法,超好用的免费界面库,C++、MFC可用
c++·mfc·界面库·库来帮·酷宝
郝学胜_神的一滴3 天前
CMake 026:属性体系精讲、四大作用域全解 & 实战代码落地
c++·cmake
众少成多积小致巨4 天前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
xcyxiner5 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner6 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner6 天前
DicomViewer (添加模型类)3
qt