场景、视图与图形项
图形/视图架构主要由 3 个部分组成,即场景、视图和图形项,三者的关系如图所示:
场景(QGraphicsScene 类)
场景不是界面组件,它是不可见的。场景是一个抽象的管理图形项的容器,我们可以向场景添加图形项,可以获取场景中的某个图形项。场景主要具有如下一些功能。
• 提供管理大量图形项的快速接口。
• 将事件传播给每个图形项。
• 管理每个图形项的状态,例如选择状态、焦点状态等。
• 管理未经变换的渲染功能,主要用于打印。
视图(QGraphicsView类)
QGraphicsView 是图形/视图架构中的视图组件。所以它是一个界面组件,用于显示场景中的内容。可以为一个场景设置多个视图,用于对同一个场景提供不同的显示界面。
在上图中,虚线框部分是一个场景,视图 1(场景外部的实线矩形框)比场景大,显示场景的全部内容。默认情况下,当视图大于场景时,场景在视图的中央位置显示,也可以通过设置视图的 Alignment 属性来控制场景在视图中的显示位置。视图 2(场景内部的实线矩形框)比场景小,视图 2 只能显示场景的部分内容,但是会自动提供卷滚条实现在整个场景内移动。
图形项(QGraphicsItem类)
图形项相当于模型中的数据,一个图形项存储了绘制这个图形项的各种参数,场景管理所
有图形项,而视图组件则负责绘制这些图形项。
QGraphicsItem 支持如下一些操作:
• 鼠标事件响应。
• 键盘输入,以及按键事件。
• 拖放操作。
• 支持组合,可以是父子图形项关系组合,也可以通过 QGraphicsItemGroup 类进行组合。
总结:场景是图形项的容器,可以在场景中添加很多图形项,每个图形项就是一 个对象。视图是显示场景的全部或部分区域的视口,也就是显示场景中的图形项,这些图形项可以被选择和拖动。
图形/视图架构的坐标系
图形/视图架构有 3 个有效的坐标系:场景坐标系、视图坐标系、图形项坐标系。3 个坐标系的
示意如图所示。
绘图的时候,场景坐标系等价于 QPainter的逻辑坐标系,一般以场景的中心为原点;视图坐标系与绘图设备坐标系相同,是物理坐标系,默认以左上角为原点;图形项坐标系是局部逻辑坐标系,一般以图形项的中心为原点。
图形项坐标系
图形项使用自己的局部坐标系,通常以其中心为原点(0, 0),这也是各种坐标变换的中心。绘制图形项时只需考虑其局部坐标系,QGraphicsScene 和 QGraphicsView 会自动进行坐标变换。
图形项的位置是指其中心在父对象坐标系中的坐标。 其父对象可以是场景或者其他父图形项。
QGraphicsItem 的大多数函数都在其局部坐标系上操作,QGraphicsItem::pos()是仅有的几个例外之一,它返回的是图形项在父图形项坐标系中的坐标,如果是顶层图形项,则返回的是在场景中的坐标。
视图坐标系
视图坐标系就是视图组件的物理坐标系,单位是像素。视图坐标系只与视图组件或视口有关,而与观察的场景无关。QGraphicsView 视口的左上角坐标总是(0,0)。
所有的鼠标事件、拖放事件的坐标首先是由视图坐标系定义的,然后用户需要将这些坐标映射为场景坐标,以便和图形项交互。
场景坐标系
场景坐标系定义了所有图形项的基础坐标,场景坐标系描述了每个顶层图形项的位置。创建场景时可以定义场景矩形区域的坐标范围。
scene= new QGraphicsScene(-400,-300,800,600);
这样定义的 scene 是左上角坐标为(−400,−300)、宽度为 800 像素、高度为 600 像素的矩形区域,单位是像素。
每个图形项在场景里有一个位置坐标,由函数 QGraphicsItem::scenePos()给出,还有一个图形项边界矩形,由函数 QGraphicsItem::sceneBoundingRect()给出。边界矩形可以使场景知道场景的哪个区域发生了变化。场景发生变化时会发射 QGraphicsScene::changed()信号,参数是场景内的矩形区域列表,表示发生变化的矩形区域。
坐标映射
在场景中操作图形项时,会进行场景到图形项、图形项到图形项或视图到场景的坐标变换。例如,在 QGraphicsView 的视口上点击鼠标时,通过函数 QGraphicsView::mapToScene() 可以将视图坐标映射为场景坐标,然后用 QGraphicsScene::itemAt()函数可以获取场景中鼠标光标 处的图形项。
相关类的介绍
图形/视图架构主要的类包括视图类 QGraphicsView、场景类 QGraphicsScene,以及各种图形
项类,图形项类的基类是 QGraphicsItem。
QGraphicsView 类
QGraphicsView 是用于观察场景的物理窗口,其视口坐标系等同于显示设备的物理坐标系,也可以对 QGraphicsView 的坐标系进行平移、旋转、缩放等变换。
QGraphicsView 的主要接口函数如下表所示,一般设置函数有一个对应的读取函数,set_XXX对应get_XXX,这里只列出设置函数。
QGraphicsScene 类
QGraphicsScene 是用于管理图形项的场景,是图形项的容器。主要接口函数如下表所示,表中仅列出函数的返回值类型,省略了输入参数。
QGraphicsItem类
QGraphicsItem 是所有图形项类的基类,我们可以从 QGraphicsItem 继承定义自己的图形项。Qt 定义了一些常见的图形项,这些常见的图形项类的继承关系如下所示。
例如要设置图形项是一张图像,可以使用QGraphicsPixmapItem类和QImage、QPixmap配合设置设置显示的图像。
有关图形项操作的函数,主要的接口函数如下表所示,表中仅列出函数的返回值类型,省略了输入参数。
函数 setFlags()用于设置图形项的操作标志,包括可选择、可移动、可获取焦点等。函数 setPos()用于设置图形项在父图形项中的坐标,如果没有父图形项,则是在场景中的坐标。函数 setZValue()用于控制图形项的叠放次序,当多个图形项重叠时,zValue 值越大的越显示在前面,相当于优先级,值越大,优先级越高。
示例程序解读
该程序的主要功能:
• 工作区是一个 TGraphicsView 组件,其作为绘图的视图组件。
• 程序中创建了一个 QGraphicsScene 场景,场景的大小就是下图中的实线矩形框的大小。
• 若改变窗口大小,当视图大于场景时,矩形框总是居于视图的中央;当视图小于场景时,在视图窗口中会自动出现卷滚条。
• 椭圆正好处于场景的中央,圆位于场景的右下角。当图形项位置不在场景的矩形框中时,图形项也是可以显示的。
• 当鼠标光标在窗口上移动时,状态栏上会显示当前光标位置在视图中的坐标和在场景中的坐标。在某个图形项上点击鼠标时,状态栏上还会显示图形项中的局部坐标。
自定义QGraphicsView视图组件
为了实现一些定制化功能,从 QGraphicsView 继承定义了一个类 TGraphicsView ,重新实现事件处理函数mouseMoveEvent()和 mousePressEvent(),把鼠标事件转换为自定义信号,这样就可以在使用TGraphicsView 组件的主窗口里设计槽函数响应这些鼠标事件。
类声明与定义如下:
//.h文件
#ifndef TGRAPHICSVIEW_H
#define TGRAPHICSVIEW_H
#include <QObject>
#include <QGraphicsView>
//自定义TGraphicsView类继承QGraphicsView类
class TGraphicsView : public QGraphicsView
{
Q_OBJECT
protected: //重新实现下面两个函数
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
public:
TGraphicsView(QWidget *parent = nullptr);
signals://信号会附带发送当前鼠标坐标
void mouseMovePoint(QPoint point);
void mouseClicked(QPoint point);
};
#endif // TGRAPHICSVIEW_H
///.cpp文件
#include "tgraphicsview.h"
#include <QMouseEvent>
#include <QPoint>
void TGraphicsView::mouseMoveEvent(QMouseEvent *event)
{//鼠标移动事件
QPoint point=event->pos(); //QGraphicsView的坐标
emit mouseMovePoint(point); //释放信号
QGraphicsView::mouseMoveEvent(event); //调用父类的事件函数
}
void TGraphicsView::mousePressEvent(QMouseEvent *event)
{ //鼠标左键按下事件
if (event->button()==Qt::LeftButton)
{
QPoint point=event->pos(); //QGraphicsView的坐标
emit mouseClicked(point); //释放信号
}
QGraphicsView::mousePressEvent(event); //父类事件
}
//构造函数
TGraphicsView::TGraphicsView(QWidget *parent):QGraphicsView(parent)
{
}
这两个事件处理函数 mouseMoveEvent()和 mousePressEvent(),并定义了两个信号 mouseMovePoint()和 mouseClicked()。这两个函数的参数 event 都是 QMouseEvent 类型,如果鼠标发生相应的事件会将将发生的event传入事件处理函数。
通过 event->pos()可以获取鼠标光标在视图中的坐标 point,在发射自定义信号 mouseMovePoint()和 mouseClicked()时将 point 作为信号的参数。在其他地方可以使用connect()将该信号与相应的槽函数连接后就可以实现相应的功能。
主窗口
在主窗口函数中,设置视图的属性,连接视图的鼠标事件与槽函数,然后在视图中创建场景并将其与界面上的视图组件 view 关联起来,在场景中添加图形项,这里需要注意图形项的一些属性设置。若不调用函数 setPos()设置图形项在场景中的位置,默认位置为(0, 0)。
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//状态栏设计
labViewCord=new QLabel("View 坐标:", this);
labViewCord->setMinimumWidth(150);
ui->statusBar->addWidget(labViewCord);
labSceneCord=new QLabel("Scene 坐标:", this);
labSceneCord->setMinimumWidth(150);
ui->statusBar->addWidget(labSceneCord);
labItemCord=new QLabel("Item 坐标:", this);
labItemCord->setMinimumWidth(150);
ui->statusBar->addWidget(labItemCord);
ui->view->setCursor(Qt::CrossCursor); //十字光标
ui->view->setMouseTracking(true); //鼠标跟踪
ui->view->setDragMode(QGraphicsView::RubberBandDrag); //矩形选择框
// ui->view->setDragMode(QGraphicsView::ScrollHandDrag);
connect(ui->view,SIGNAL(mouseMovePoint(QPoint)),this, SLOT(do_mouseMovePoint(QPoint)));
connect(ui->view,SIGNAL(mouseClicked(QPoint)),this, SLOT(do_mouseClicked(QPoint)));
iniGraphicsSystem(); //Graphics View结构初始化
}
//创建Graphics View结构中的各项
void MainWindow::iniGraphicsSystem()
{
QRectF rect(-200,-100,400,200); //该坐标是在view中的坐标
scene=new QGraphicsScene(rect,this); //scene逻辑坐标系定义
ui->view->setScene(scene); //为视图设置场景
//画一个矩形框,大小等于scene
QGraphicsRectItem *item=new QGraphicsRectItem(rect); //矩形框正好等于scene的大小
// item->setFlag(QGraphicsItem::ItemIsSelectable); //可选中
// item->setFlag(QGraphicsItem::ItemIsFocusable); //可以获得焦点
// item->setFlag(QGraphicsItem::ItemIsMovable,false); //不能移动
//可选,可以有焦点,但是不能移动
item->setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsFocusable);
QPen pen;
pen.setWidth(2);
item->setPen(pen);
// item->setPos(500,0);//缺省位置在scene的(0,0)
scene->addItem(item);
//一个位于scene中心的椭圆,测试局部坐标
QGraphicsEllipseItem *item2=new QGraphicsEllipseItem(-100,-50,200,100); //矩形框内创建椭圆,绘图项的局部坐标,左上角(-100,-50),宽200,高100
item2->setPos(0,0); //图形项在场景中的坐标
item2->setBrush(QBrush(Qt::blue));
item2->setFlag(QGraphicsItem::ItemIsMovable); //能移动
item2->setFlag(QGraphicsItem::ItemIsSelectable); //可选中
item2->setFlag(QGraphicsItem::ItemIsFocusable); //可以获得焦点
// item2->setFlags(QGraphicsItem::ItemIsMovable
// | QGraphicsItem::ItemIsSelectable
// | QGraphicsItem::ItemIsFocusable);
scene->addItem(item2);
//一个圆,中心位于scene的边缘
QGraphicsEllipseItem *item3=new QGraphicsEllipseItem(-50,-50,100,100); //矩形框内创建椭圆,绘图项的局部坐标,左上角(-100,-50),宽200,高100
item3->setPos(rect.right(),rect.bottom()); //图形项在场景中的坐标
item3->setBrush(QBrush(Qt::red));
item3->setFlag(QGraphicsItem::ItemIsMovable); //能移动
item3->setFlag(QGraphicsItem::ItemIsSelectable); //可选中
item3->setFlag(QGraphicsItem::ItemIsFocusable); //可以获得焦点
// item3->setFlags(QGraphicsItem::ItemIsMovable
// | QGraphicsItem::ItemIsSelectable
// | QGraphicsItem::ItemIsFocusable);
scene->addItem(item3);
scene->clearSelection();
}
对于视图属性的设置:
• setCursor(Qt::CrossCursor):将鼠标光标设置为十字形。
• setMouseTracking(true):开启鼠标跟踪,在鼠标移动时就会产生 mouseMove 事件。默认是不开启鼠标跟踪的,那么只有在按下鼠标某个键时才会产生 mouseMove 事件。
• setDragMode(QGraphicsView::RubberBandDrag):在场景的背景上点击并拖动鼠标时,显示 一个矩形框。默认是不响应(QGraphicsView::NoDrag),其值还可以设置为 QGraphicsView:: ScrollHandDrag,拖动鼠标时鼠标光标变成手形,拖动鼠标就会使卷滚条移动。
关联信号与槽函数
连接的两个自定义槽函数用于与界面上的 TGraphicsView 组件的两个信号关联,在鼠标移动时显示视图中的坐标和场景中的坐标,在点击鼠标时显示图形项的局部坐标。
槽函数定义如下所示:
//鼠标移动,point是 GraphicsView的坐标,物理坐标
void MainWindow::do_mouseMovePoint(QPoint point)
{
labViewCord->setText(QString::asprintf("View 坐标:%d,%d",point.x(),point.y()));
QPointF pointScene=ui->view->mapToScene(point); //转换到Scene坐标
labSceneCord->setText(QString::asprintf("Scene 坐标:%.0f,%.0f", pointScene.x(),pointScene.y()));
}
//鼠标单击
void MainWindow::do_mouseClicked(QPoint point)
{
QPointF pointScene=ui->view->mapToScene(point); //转换到Scene坐标
QGraphicsItem *item=NULL;
item=scene->itemAt(pointScene,ui->view->transform()); //获取光标下的绘图项
if (item != NULL) //有绘图项
{
QPointF pointItem=item->mapFromScene(pointScene); //转换为绘图项的局部坐标
labItemCord->setText(QString::asprintf("Item 坐标:%.0f,%.0f", pointItem.x(),pointItem.y()));
}
}
鼠标事件中,参数 point 是鼠标在视图中的坐标,使用 QGraphicsView 的 mapToScene()函数可以将此坐标变换为场景中的坐标。先用 QGraphicsView 的mapToScene()函数将此坐标变换为场景中的坐标 pointScene,然后用 QGraphicsScene 的 itemAt()函数获得光标处的图形项。如果鼠标光标处有图形项,就用图形项的 mapFromScene()函数将pointScene 变换为图形项的局部坐标 pointItem。
自定义TGraphicsView的UI提升
我们使用时需要的是TGraphicsView,但是QT可选组件中只有GraphicsView,因此需要将视图组件升级为 TGraphicsView 类。升级方法如下所示,之前在QT界面设计中添加的是QGraphicsView,右键该组件点击提升为----->TGraphicView即可。这是使用自定义界面组件的一种方法。
为了测试视图与场景的尺寸变化,主窗口还重定义了事件处理函数 resizeEvent(),以便在窗口大小变化时,显示视图区域和场景的大小信息。其代码如下:
//窗口变化大小时的事件
void MainWindow::resizeEvent(QResizeEvent *event)
{
// Q_UNUSED(event);
//Graphics View坐标,左上角总是(0,0),宽度=,长度=
QString str=QString::asprintf("Graphics View坐标,左上角总是(0,0),宽度=%d,高度=%d",
ui->view->width(),ui->view->height());
ui->labViewSize->setText(str);
// ui->labViewSize->setText(QString::asprintf("Graphics View坐标,左上角总是(0,0),宽度=%d,高度=%d",
// ui->view->width(),ui->view->height()));
QRectF rectF=ui->view->sceneRect(); //场景的矩形区
QString str2=QString::asprintf("QGraphicsView::sceneRect=(Left,Top,Width,Height)"
"=%.0f,%.0f,%.0f,%.0f", rectF.left(),rectF.top(),rectF.width(),rectF.height());
ui->labSceneRect->setText(str2);
// ui->labSceneRect->setText(QString::asprintf("QGraphicsView::sceneRect=(Left,Top,Width,Height)=%.0f,%.0f,%.0f,%.0f",
// rectF.left(),rectF.top(),rectF.width(),rectF.height()));
event->accept();
}
通过测试,程序在窗口大小改变时,labViewSize 显示的视图宽度和高度是随窗口大小变化而变化的,而 labSceneRect 显示的场景的矩形区大小是固定的,总是(−200, −100, 400, 200)。
如下图,将程序窗口缩小,视图也会变小,显示的内容也会被隐藏一部分,但是场景是尺寸是不变的,所以视图是用来“看”的,而场景是用“存”的,视图中虽然有些组件看不见了,但是这些组件确确实实还在场景中,这也是为什么在视图和图形项中要套一层场景,这样更方便管理。