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. 图形项层次要明确:父子关系会影响坐标计算

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

相关推荐
众少成多积小致巨2 小时前
libbinder_ndk 入门指南
前端·c++·架构
纤纡.2 小时前
基于 PyQt5 的桌面应用开发实战:登录、预测、计算器、摄像头多功能系统
开发语言·人工智能·qt·计算机视觉
历程里程碑2 小时前
二叉树---翻转二叉树
开发语言·c++·elasticsearch·链表·搜索引擎·tornado·dash
闻缺陷则喜何志丹2 小时前
【排序】P6149 [USACO20FEB] Triangles S|普及+
c++·算法·排序·洛谷
tankeven2 小时前
HJ178 【模板】双指针
c++·算法
charlie1145141913 小时前
嵌入式C++工程实践——第13篇:第一次重构 —— enum class取代宏,类型安全的开始
开发语言·c++·vscode·stm32·安全·重构·现代c++
CHANG_THE_WORLD3 小时前
C++ 文件读取函数完全指南
开发语言·c++
6Hzlia3 小时前
【Hot 100 刷题计划】 LeetCode 300. 最长递增子序列 | C++ 动态规划 & 贪心二分
c++·leetcode·动态规划
阿正的梦工坊3 小时前
JavaScript 闭包 × C++ 类比:彻底搞懂闭包
开发语言·javascript·c++