8.3.1 场景、视图与图形项
采用 QPainter 绘图时需要在绘图设备的 paintEvent() 事件里编写绘图程序,实现整个绘图过程。这种方法如同使用 Windows 的画图软件在绘图,绘制的图形是位图,这种方法适合于绘制复杂性不高的固定图形,不能实现图件的选择、编辑、拖放、修改等功能。
Qt为绘制复杂的可交互图形提供了 Graphics view 绘图架构,是一种基于图形项(Graphics Item)的模型/视图模式,与第5章的数据编辑与显示的 Model/View 模式类似。使用 Graphics View 架构可以绘制复杂的有几百万个基本图形元件的图形,并且每个图形元件是可选择、可拖放和修改的,类似于矢量绘图软件的绘图功能。
Graphics View 架构主要由 3 个部分组成,即场景、视图和图形项,其构成的Graphics View绘图系统如图。
场景
QGraphicsScene类提供绘图场景(Scene)。场景是不可见的,是一个抽象的管理图形项的容器,可以向场景添加图形项,获取场景中的某个图形项等。场景主要具有如下一些功能:
除了图形项之外,场景还有背景层和前景层,通常由QBrush指定,也可以通过重新实现drawBackground() 和 drawForeground() 函数来实现自定义的背景和前景,实现一些特殊效果。
视图
QGraphicsView 提供绘图的视图(View)组件,用于显示场景中的内容。可以为一个场景设置几个视图,用于对同一个数据集提供不同的视口。
在图8-16中,虚线框围住的是一个场景,视图 1 比场景大,显示场景的全部内容。缺省情况下,当视图大于场景时,场景在视图的中间部分显示,也可以 设置视图的Alignment属性控制场景在视图中的显示位置;当视图小于场景时(见图 8-16 中的视图2),视图只能显示场景的一部分内容,但是会自动提供卷滚条在整个场景内移动。
视图接收键盘和鼠标输入并转换为场景事件,并进行坐标转换后传送给可视场景。
图形项
图形项(Graphics Item)就是一些基本的图形元件,图形项的基类是 QGraphicsItem。Qt 提供了一些基本的图形项,如绘制椭圆的 QGraphicsEllipseItem、绘制矩形的 QGraphicsRectItem、绘制文字的QGraphicsTextItem等。
QGraphicsItem支持如下的一些操作:
所以,图形项可以被选择、拖放、组合,若编写信号槽函数代码,还可以实现各种编辑功能。一个图形项还可以包含子图形项,图形项还支持碰撞检测,即是否与其他图形项碰撞。
在图8-16所示的视图、场景和图形项之间的关系示意图中,场景是图形项的容器,可以再场景上绘制很多图形项,每个图形项就是一个对象,这些图形项可以被选择、拖动等。视图是显示场景的一部分区域的视口,一个场景可以有多个视图,一个视图现实场景的部分区域或全部区域,或从不同角度观察场景。
8.3.2 Graphics View 的坐标系统
图形项坐标(局部逻辑坐标)、场景坐标(QPainter的逻辑坐标)、视图坐标(设备坐标、物理坐标):
图形项坐标
图形项使用自己的局部坐标(Item Coordinates),通常以其中心为(0,0),也是各种坐标变换的中心。图形项的鼠标事件的坐标使用局部坐标表示的,创建自定义图形项,绘制图形项时只需考虑其局部坐标,QGraphicsScene 和 QGraphicsView 会自动进行坐标转换。
一个图形项的位置是其中心点在父坐标系统中的坐标,对于没有父图形项的图形项,其父对象就是场景,图形项的位置就是在场景中的坐标。
如果一个图形项还是其他图形项的父项,父项进行坐标变换时,子项也做同样的坐标变换。
QGraphicsItem 的大多数函数都是在其局部坐标系上操作的,例如一个图形项的边界矩形 QGraphicsItem::boundingRect() 是用局部坐标给出的,但是 QGraphicsItem::pos() 是仅有的几个例外,它返回的是图形项在父坐标系中的坐标,如果是顶层图形项,就是在场景中的坐标。
视图坐标
视图坐标(View Coordinates)就是窗口界面(widget)的物理坐标,单位是像素。视图坐标只与 widget 或视口有关,而与观察的场景无关。QGraphicsView 视口的左上角坐标总是(0,0)。
所有的鼠标事件、拖放事件的坐标首先是由视图坐标定义的,然后用户需要将这些坐标映射为场景坐标,以便和图形项交互。
场景坐标
场景是所有图形项的基础坐标,场景坐标(Scene Coordinates)描述了每个顶层图形项的位置。创建场景时可以定义场景矩形区的坐标范围,例如
scene = new QGraphicsScene(-400, -300, 800, 600);
这样定义的scene是左上角坐标为(-400,-300),宽度为800,高度为600的矩形区域,单位是像素。
每个图形项在场景里都有一个位置坐标,由函数 QGraphicsItem::scenePos() 给出;还有一个图形项边界矩形,由 QGraphicsItem::sceneBoundingRect() 函数给出。边界矩形可以使 QGraphicsScene 知道场景的哪个区域发生了变化。场景发生变化时会发射 QGraphicsScene::changed() 信号,参数是一个场景的矩形列表,表示发生变化的矩形区。
坐标映射
在场景中操作图形项时,进行场景到图形项、图形项到图形项,或视图到场景之间的坐标变换是比较有用的,即坐标映射(Coordinate Mapping)。例如,在 QGraphicsView 的视口上单击鼠标时,通过函数 QGraphicsView::mapToScene() 可以将视图坐标映射为场景坐标,然后用 QGraphicsScene::itemAt() 函数可以获取场景中鼠标光标处的图形项。
8.3.3 Graphics View 相关的类
8.3.4 Graphics View 程序基本结构和功能实现
实例
实例主要功能包括以下几点:
自定义图形视图组件
QGraphicsView是Qt的图形视图组件,在UI设计器的Display Widgets分组里可以拖放一个QGraphicsView组件到窗口上。但是本实例中需要实现鼠标在QGraphicsView上移动时就显示当前光标的坐标,这涉及 mouseMoveEvent() 事件的处理。QGraphicsView 没有与 mouseMoveEvent 相关的信号,因而无法定义槽函数与此事件相关联。
为此,从 QGraphicsView 继承定义一个类 QWGraphicsView,实现 mouseMoveEvent() 事件和 mousePressEvent() 事件,并把鼠标事件转换为信号,这样就可以在主程序里设计槽函数响应这些鼠标事件。下面是QWGraphicsView类的定义:
// qwgraphicsview.h
#ifndef QWGRAPHICSVIEW_H
#define QWGRAPHICSVIEW_H
#include
class QWGraphicsView : public QGraphicsView
{
Q_OBJECT
protected:
void mouseMoveEvent(QMouseEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
public:
QWGraphicsView();
signals:
void mouseMovePoint(QPoint point);
void mouseClicked(QPoint point);
};
#endif // QWGRAPHICSVIEW_H
QWGraphicsView 的实现:
QWGraphicsVIew.cpp
#include "qwgraphicsview.h"
#include
QWGraphicsView::QWGraphicsView()
{
}
void QWGraphicsView::mouseMoveEvent(QMouseEvent *event)
{
// 鼠标移动事件
QPoint point = event->pos(); // 获取鼠标光标在视图中的坐标 point
emit mouseMovePoint(point); // 发射信号
QGraphicsView::mouseMoveEvent(event);
}
void QWGraphicsView::mousePressEvent(QMouseEvent *event)
{
// 鼠标左键按下事件
if(event->button() == Qt::LeftButton)
{
QPoint point = event->pos(); // 获取鼠标光标在视图中的坐标 point
emit mouseClicked(point); // 发射信号
}
QGraphicsView::mousePressEvent(event);
}
MainWindow 类的完整定义如下:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include
#include
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
QGraphicsScene *scene;
QLabel *labViewCord;
QLabel *labSceneCord;
QLabel *labItemCord;
void iniGraphicsSystem(); // 创建 Graphics View 的各项
protected:
void resizeEvent(QResizeEvent *event);
private slots:
void on_mouseMovePoint(QPoint point);
void on_mouseClicked(QPoint point);
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
#include
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
labViewCord = new QLabel("View coordinates: ");
labViewCord->setMinimumWidth(150);
ui->statusBar->addWidget(labViewCord);
labSceneCord = new QLabel("Scene coordinates: ");
labSceneCord->setMinimumWidth(150);
ui->statusBar->addWidget(labSceneCord);
labItemCord = new QLabel("Item coordinates: ");
labItemCord->setMinimumWidth(150);
ui->statusBar->addWidget(labItemCord);
ui->graphicsView->setCursor(Qt::CrossCursor);
ui->graphicsView->setMouseTracking(true);
ui->graphicsView->setDragMode(QGraphicsView::RubberBandDrag);
QObject::connect(ui->graphicsView, SIGNAL(mouseMovePoint(QPoint)), this, SLOT(on_mouseMovePoint(QPoint)));
QObject::connect(ui->graphicsView, SIGNAL(mouseClicked(QPoint)), this, SLOT(on_mouseClicked(QPoint)));
iniGraphicsSystem();
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_mouseMovePoint(QPoint point)
{
labViewCord->setText(QString::asprintf("View coordiantes: %d, %d", point.x(), point.y()));
QPointF pointScene = ui->graphicsView->mapToScene(point);
labSceneCord->setText(QString::asprintf("Scene coordinates: %.0f, %.0f", pointScene.x(), pointScene.y()));
}
void MainWindow::on_mouseClicked(QPoint point)
{
QPointF pointScene = ui->graphicsView->mapToScene(point);
QGraphicsItem *item = NULL;
item = scene->itemAt(pointScene, ui->graphicsView->transform());
if(item != NULL)
{
QPointF pointItem = item->mapFromScene(pointScene);
labItemCord->setText(QString::asprintf("Item coordinates: %.0f, %.0f", pointItem.x(), pointItem.y()));
}
}
void MainWindow::iniGraphicsSystem()
{
// 构造Graphics View的各项
QRectF rect(-200, -100, 400, 200); // 矩形自身坐标系的左上角坐标,宽度,高度
scene = new QGraphicsScene(rect); // scene逻辑坐标系定义
ui->graphicsView->setScene(scene);
// 画一个矩形,大小等于scene
QGraphicsRectItem *item = new QGraphicsRectItem(rect);
item->setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemIsFocusable); // 设置flags
QPen pen;
pen.setWidth(2);
item->setPen(pen);
scene->addItem(item);
// 画一个位于scene中心的椭圆,测试局部坐标
QGraphicsEllipseItem *item2 = new QGraphicsEllipseItem(-100, -50, 200, 100); // 椭圆自身坐标系的左上角坐标、宽度、高度
item2->setPos(0, 0); // 椭圆在场景中的位置
item2->setBrush(QBrush(Qt::blue));
item2->setFlags(QGraphicsItem::ItemIsMovable
| QGraphicsItem::ItemIsSelectable
| QGraphicsItem::ItemIsFocusable);
scene->addItem(item2);
// 画一个圆,中心位于scene的边缘
QGraphicsEllipseItem *item3 = new QGraphicsEllipseItem(-50, -50, 100, 100);
item3->setPos(rect.right(), rect.bottom());
item3->setFlags(QGraphicsItem::ItemIsMovable
| QGraphicsItem::ItemIsSelectable
| QGraphicsItem::ItemIsFocusable);
scene->addItem(item3);
scene->clearSelection();
}