【Qt】《Qt 5.9 C++开发指南》笔记-Chapter8-绘图

第8章 绘图

GUI用户界面的优势是通过可视化的界面元素为用户提供便利的操作,界面上的按钮、编辑框等各种界面组件其实都是通过绘图而得到的。Qt的二维绘图基本功能是使用QPainter在绘图设备上绘图,绘图设备包括QWidget、QPixmap等,通过绘制一些基本的点、线、圆等基本形状组成自己需要的图形,得到的图形是不可交互操作的图形。

Qt还提供了Graphics View架构,使用QGraphicsView、QGraphicsScene和各种QGraphicsItem类绘图,在一个场景中可以绘制大量图件,且每个图件是可选择、可交互的,如同矢量图编辑软件那样可以操作每个图件。Graphics View架构为用户绘制复杂的组件化图形提供了便利。

本章首先介绍使用QPainter绘图的原理,这是Qt绘图的基础,再介绍Graphics View架构的原理和使用。

8.1 QPainter基本绘图

8.1.1 QPainter绘图系统

1.QPainter与QPaintDevice

Qt的绘图系统使用户可以在屏幕或打印设备上用相同的API绘图,绘图系统基于QPainter、QPaintDevice和QPaintEngine类。QPainter是用来进行绘图操作的类,QPaintDevice是一个可以使用QPainter进行绘图的抽象的二维界面,QPaintEngine给QPainter提供在不同设备上绘图的接口。QPaintEngine类由QPainter和QPaintDevice内部使用,应用程序一般无需和QPaintEngine打交道,除非要创建自己的设备类型。

一般的绘图设备包括QWidget、QPixmap、QImage等,这些绘图设备为QPainter提供一个"画布"。

2.paintEvent事件和绘图区

QWidget类及其子类是最常用的绘图设备,从QWidget类继承的类都有paintEvent()事件,要在设备上绘图,只需重定义此事件并编写响应代码。创建一个QPainter对象获取绘图设备的接口,然后就可以在绘图设备的"画布"上绘图了。

在paintEvent()事件里绘图的基本程序结构是:

复制代码
void Widget::paintEvent(QPaintEvent *event)
{
   QPainter   painter(this);  //创建与绘图设备关联的QPainter对象
...//painter在设备的窗口上画图
}

首先创建一个属于本绘图设备的QPainter对象painter,然后使用这个painter在绘图设备的窗口上画图。

QWidget的绘图区就是其窗口内部区域。如原书的图8-1所示是在一个QWidget窗口上绘制了一个填充矩形(这个实心矩形及其边框是程序绘制的图形,其他直线和文字是为说明而加的),整个窗口内部的矩形区就是QPainter可以绘图的区域。

QWidget的内部绘图区的坐标系统如图8-1所示,坐标系统的单位是像素。左上角坐标为(0,0),向右是X轴正方向,向下是Y轴正方向,绘图区的宽度由QWidget::width()函数获取,高度由QWidget::height()函数获取,所以,绘图区右下角的的点的坐标是(width(), height())。这个坐标系统是QWidget绘图区的局部物理坐标,称为视口(viewport)坐标。相应的还有逻辑坐标,称为窗口(window)坐标,后面再详细介绍。

使用QPainter在QWidget上绘图就是在这样的一个矩形区域里绘图。

3.QPainter绘图的主要属性

用QPainter在绘图设备上绘图,主要是绘制一些基本的图形元素,包括点、直线、圆形、矩形、曲线、文字等,控制这些绘图元素特性的主要是QPainter的3个属性,分别如下。

  • pen属性:是一个QPen对象,用于控制线条的颜色、宽度、线型等,如图8-1所示矩形边框的线条的特性就是由pen属性决定的。

  • brush属性:是一个QBrush对象,用于设置一个区域的填充特性,可以设置填充颜色、填充方式、渐变特性等,还可以采用图片做材质填充。图8-1中的矩形用黄色填充就是由brush属性设置决定的。

  • font属性:是一个QFont对象,用于绘制文字时,设置文字的字体样式、大小等属性。

使用这3个属性基本就控制了绘图的基本特点,当然还有一些其他的功能结合使用,比如叠加模式、旋转和缩放等功能。

4.创建实例

为演示QPainter绘图的基本功能,创建一个Qt Widget Application项目samp8_1,并选择窗口基类为QWidget,自动创建窗体。创建后的项目有一个Widget类,为了简化代码功能,Widget窗口里不再放置任何其他组件,只用来绘图。

下面是Widget类的完整定义。只是重新定义了paintEvent()事件,在此事件里编写绘图的代码。Q_DECL_OVERRIDE宏表示这个函数是对父类虚函数的重载。

复制代码
class Widget : public QWidget
{
   Q_OBJECT
protected:
   void   paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
public:
   explicit Widget(QWidget *parent = 0);
   ~Widget();
private:
   Ui::Widget *ui;
};

下面是Widget类构造函数和paintEvent()函数的代码,在界面上绘制如图8-1所示的一个填充矩形,演示了QPainter绘图的基本过程。

复制代码
Widget::Widget(QWidget *parent) :  QWidget(parent),  ui(new Ui::Widget)
{
   ui->setupUi(this);
   setPalette(QPalette(Qt::white)); //设置窗口为白色背景
   setAutoFillBackground(true);
}
void Widget::paintEvent(QPaintEvent *event)
{
   QPainter   painter (this);//创建QPainter对象
   painter.setRenderHint(QPainter::Antialiasing);
   painter.setRenderHint(QPainter::TextAntialiasing);
   int W=this->width(); //绘图区宽度
   int H=this->height(); //绘图区高度
   QRect   rect(W/4,H/4,W/2,H/2); //中间区域矩形框
//设置画笔
   QPen   pen;
   pen.setWidth(3); //线宽
   pen.setColor(Qt::red); //划线颜色
   pen.setStyle(Qt::SolidLine);//线的样式,实线、虚线等
   pen.setCapStyle(Qt::FlatCap);//线端点样式
   pen.setJoinStyle(Qt::BevelJoin);//线的连接点样式
   painter.setPen(pen);
//设置画刷
   QBrush  brush;
   brush.setColor(Qt::yellow); //画刷颜色
   brush.setStyle(Qt::SolidPattern); //画刷填充样式
   painter.setBrush(brush);
//绘图
   painter.drawRect(rect);
}

在paintEvent()函数中,首先创建与Widget关联的QPainter对象painter,这样就可以用这个painter在Widget上绘图了。

下面的代码获取Widget窗口的宽度和高度,并定义了位于中间区域的矩形rect,这个矩形的大小随Widget的大小变化而变化,因为它的大小定义中利用了Widget的宽度和高度,而不是固定大小。

复制代码
int W=this->width(); //绘图区宽度
int H=this->height(); //绘图区高度
QRect   rect(W/4,H/4,W/2,H/2); //中间区域矩形框

然后定义了一个QPen类的对象pen,设置其线宽、颜色、线型等,然后设置为painter的pen。

再定义了一个QBrush类的对象brush,设置其颜色为黄色,填充方式为实体填充,然后设置为painter的brush。

这样设置好painter的pen和brush属性后,调用QPainter类的drawRect()函数,就可以绘制前面定义的矩形了,矩形框的线条特性由pen决定,填充特性由brush决定。运行程序就可以得到如图8-1所示的居于界面中间的填充矩形框。

为了不使程序结构太过于复杂,在paintEvent()函数里直接设置pen和brush的各种属性,而不是设计复杂的界面程序来修改这些设置。要实现什么绘图功能,只需在paintEvent()函数里直接修改代码即可。

8.1.2 QPen的主要功能

QPen用于绘图时对线条进行设置,主要包括线宽、颜色、线型等,表8-1是QPen类的主要接口函数。通常一个设置函数都有一个对应的读取函数,例如setColor()用于设置画笔颜色,对应的读取画笔颜色的函数为color(),表8-1仅列出设置函数(省略了函数参数中的const关键字)。

表8-1 QPen的主要函数

函数原型 功能
void setColor(QColor &color) 设置画笔颜色,即线条颜色
void setWidth(int width) 设置线条宽度
void setStyle (Qt::PenStyle style) 设置线条样式,参数为Qt::PenStyle枚举类型
void setCapStyle (Qt::PenCapStyle style) 设置线条端点样式,参数为Qt::PenCapStyle枚举类型
void setJoinStyle (Qt::PenJoinStyle style) 设置连接样式,参数为Qt::PenJoinStyle枚举类型

线条颜色和宽度的设置无需多说,QPen影响线条特性的另外3个主要属性是线条样式(style)、端点样式(capStyle)和连接样式(joinStyle)。

1.线条样式

setStyle(Qt::PenStyle style)函数用于设置线条样式,参数是一个枚举类型Qt::PenStyle的常量,几种典型的线条样式的绘图效果如原书的图8-2所示。Qt::PenStyle类型还有一个常量Qt::NoPen表示不绘制线条。

除了几种基本的线条样式外,用户还可以自定义线条样式,自定义线条样式时需要用到setDashOffset()和setDashPattern()函数。

2.线条端点样式

setCapStyle (Qt::PenCapStyle style)函数用于设置线条端点样式,参数是一个枚举类型Qt::PenCapStyle的常量,该枚举类型的3种取值及其绘图效果如原书的图8-3所示。

3.线条连接样式

setJoinStyle (Qt::PenJoinStyle style)函数用于设置线条连接样式,参数是一个枚举类型Qt::PenJoinStyle 的常量,该枚举类型的取值及其绘图效果如原书的图8-4所示。

8.1.3 QBrush的主要功能

QBrush定义了QPainter绘图时的填充特性,包括填充颜色、填充样式、材质填充时的材质图片等,其主要函数见表8-2(省略了函数参数中的const关键字)。

表8-2 QBrush的主要函数

函数原型 功能
void setColorr(QColor &color) 设置画刷颜色,实体填充时即为填充颜色
void setStyle(Qt::BrushStyle style) 设置画刷样式,参数为Qt::BrushStyle枚举类型
void setTexture(QPixmap &pixmap) 设置一个QPixmap类型的图片作为画刷的图片,画刷样式自动设置为Qt::TexturePattern
void setTextureImage(QImage &image) 设置一个QImage类型的图片作为画刷的图片,画刷样式自动设置为Qt::TexturePattern

setStyle(Qt::BrushStyle style)函数设置画刷的样式,参数是Qt::BrushStyle style枚举类型,该枚举类型典型的几种取值见表8-3,详细的取值请参考Qt的帮助文件。几种典型取值的填充效果如原书的图8-5所示。

表8-3 枚举类型Qt:: BrushStyle几个主要常量及其意义

枚举常量 描述
Qt:: NoBrush 不填充
Qt:: SolidPattern 单一颜色填充
Qt:: HorPattern 水平线填充
Qt:: VerPattern 垂直线填充
Qt:: LinearGradientPattern 线性渐变,需要使用QLinearGradient类对象作为Brush
Qt:: RadialGradientPattern 辐射渐变,需要使用QRadialGradient类对象作为Brush
Qt:: ConicalGradientPattern 圆锥型渐变,需要使用QConicalGradient类对象作为Brush
Qt::TexturePattern 材质填充,需要指定texture或textureImage图片

渐变填充需要使用专门的类作为Brush赋值给QPainter,这部分在后面详细介绍。其他各种线型填充只需设置类型参数即可,使用材质需要设置材质图片。

下面是使用资源文件里的一个图片进行材质填充的示例程序,用材质图片填充一个矩形,程序运行结果如原书的图8-6所示。

复制代码
void Widget::paintEvent(QPaintEvent *event)
{
   QPainter   painter(this); 
   int W=this->width(); //绘图区宽度
   int H=this->height(); //绘图区高度
   QRect   rect(W/4,H/4,W/2,H/2); //中间区域矩形框
//设置画笔
   QPen   pen;
   pen.setWidth(3); //线宽
   pen.setColor(Qt::red); //划线颜色
   pen.setStyle(Qt::SolidLine);//线的类型,实线、虚线等
   painter.setPen(pen);
//设置画刷
   QPixmap texturePixmap(":images/images/texture.jpg");
   QBrush  brush;
   brush.setStyle(Qt::TexturePattern); //画刷填充样式
   brush.setTexture(texturePixmap); //设置材质图片
   painter.setBrush(brush);
//绘图
   painter.drawRect(rect);
}

8.1.4 渐变填充

使用渐变填充需要用渐变类的对象作为Painter的brush,有3个实现渐变填充的类。

  • QLinearGradient:线性渐变。指定一个起点及其颜色,终点及其颜色,还可以指定中间的某个点的颜色,起点至终点之间的颜色会线性插值计算,得到线性渐变的填充颜色。

  • QRadialGradient:有简单辐射渐变和扩展辐射渐变两种方式。简单辐射渐变是在一个圆内的一个焦点和一个端点之间生成渐变颜色,扩展辐射渐变是在一个焦点圆和一个中心圆之间生成渐变色。

  • QConicalGradient:圆锥形渐变,围绕一个中心点逆时针生成渐变颜色。

这3种渐变的示例效果如原书的图8-7所示。

这3个渐变类都继承自QGradient类,除了生成渐变颜色的方式不同之外,在设定的渐变颜色坐标范围之外,还需要用QGradient类的setSpread(QGradient::Spread method)函数设置延展方式。枚举类型QGradient::Spread有3种取值,分别表示3种延展效果,原书的图8-8是使用辐射渐变时3种延展方式的效果。setSpread()对圆锥形渐变不起作用。

  • PadSpread模式是用结束点的颜色填充外部区域,这是缺省的方式。

  • RepeatSpread模式是重复使用渐变方式填充外部区域。

  • ReflectSpread是反射式重复使用渐变方式填充外部区域。

下面的代码演示使用渐变效果绘图,程序中使用了辐射渐变。

复制代码
void Widget::paintEvent(QPaintEvent *event)
{
   QPainter   painter(this); 
   int W=this->width();
   int H=this->height();
//径向渐变
   QRadialGradient  radialGrad(W/2,H/2,qMax(W/8,H/8),W/2,H/2);
   radialGrad.setColorAt(0,Qt::green);
   radialGrad.setColorAt(1,Qt::blue);
   radialGrad.setSpread(QGradient::ReflectSpread);
   painter.setBrush(radialGrad);
//绘图
   painter.drawRect(this->rect()); //填充更大区域,会有延展效果
}

上面的代码中定义QRadialGradient对象时使用的构造函数原型是:

复制代码
QRadialGradient(qreal cx, qreal cy, qreal radius, qreal fx, qreal fy)

其中,(cx, cy)是辐射填充的中心点,程序中设置为(W/2, H/2),也就是Widget窗口的中心;radius是辐射填充区的半径,程序中设置为qMax(W/8, H/8);(fx, fy)是焦点坐标,程序中设置为(W/2, H/2),与中心点相同。

设置辐射渐变的起点颜色和终点颜色的语句是:

复制代码
radialGrad.setColorAt(0,Qt::green);
radialGrad.setColorAt(1,Qt::blue);

这里的"点"使用了逻辑坐标,0表示起点,即辐射中心点;1表示终点,即填充区圆的圆周。

再用setSpread()函数设置延展方式为QGradient::ReflectSpread。

最后的绘图语句是:

复制代码
painter.drawRect(this->rect());

这里绘制一个矩形,但是使用的矩形是this->rect(),即Widget窗口的整个矩形区域,它大于定义的辐射填充区域,所以会有延展效果。程序运行效果如原书的图8-9所示。

若使用线性渐变填充,示例代码如下(只列出QLinearGradient定义的部分)。定义QLinearGradient对象时指定了线性渐变的起点和终点,设置颜色时可以在起点和终点之间设置多个点的颜色值。

复制代码
//   QLinearGradient  linearGrad(rect.left(),rect.top(),
//                 rect.right(),rect.bottom()); //对角线
   QLinearGradient  linearGrad(rect.left(), rect.top(), rect.right(), rect.top());//从左到右
   linearGrad.setColorAt(0,Qt::blue);//起点颜色
   linearGrad.setColorAt(0.5,Qt::green);//中间点颜色
   linearGrad.setColorAt(1,Qt::red);//终点颜色
   linearGrad.setSpread(QGradient::ReflectSpread);  //展布模式
   painter.setBrush(linearGrad);

创建QLinearGradient对象时传递了两个坐标点,分别表示填充区的起点和终点,起点和终点的定义方式不同,可以实现水平渐变、垂直渐变、或对角渐变等不同效果。

使用圆锥形渐变的示例代码如下。创建QConicalGradient对象时指定了中心点坐标和起始角度,然后设置多个点的颜色。但是注意,圆锥形填充没有延展效果。

复制代码
QConicalGradient  coniGrad(W/2,H/2,45);
coniGrad.setColorAt(0,Qt::yellow);
coniGrad.setColorAt(0.5,Qt::blue);
coniGrad.setColorAt(1,Qt::green);
painter.setBrush(coniGrad);

8.1.5 QPainter绘制基本图形元件

1.基本图形元件

QPainter提供了很多绘制基本图形的功能,包括点、直线、椭圆、矩形、曲线等,由这些基本的图形可以构成复杂的图形。QPainter中提供的绘制基本图元的函数见表8-4。每个函数基本上都有多种参数形式,这里只列出函数名,给出了其中一种参数形式的示例代码,并且假设已经通过以下的代码获得了绘图窗口的painter、窗口宽度W和高度H。

复制代码
QPainter painter(this);
int W=this->width(); //绘图区宽度
int H=this->height(); //绘图区高度

同一个函数名的其他参数形式的函数原型可查阅Qt的帮助文件。

表8-4 QPainter绘制基本图形元件的函数

函数名 功能和示例代码 示例图形
drawArc 画弧线,例如QRect rect(W/4,H/4,W/2,H/2); int startAngle = 90 * 16; //起始90° int spanAngle = 90 * 16; //旋转90° painter.drawArc(rect, startAngle, spanAngle);
drawChord 画一段弦,例如QRect rect(W/4,H/4,W/2,H/2);int startAngle = 90 * 16; //起始90° int spanAngle = 90 * 16; //旋转90° painter. drawChord (rect, startAngle, spanAngle);
drawConvexPolygon 根据给定的点画凸多边形 QPoint points4={QPoint(5W/12,H/4),QPoint(3 W/4,5H/12),QPoint(5 W/12,3H/4),QPoint(W/4,5H/12), }; painter.drawConvexPolygon(points, 4);
drawEllipse 画椭圆 QRect rect(W/4,H/4,W/2,H/2); painter.drawEllipse(rect);
drawImage 在指定的矩形区域内绘制图片 QRect rect(W/4,H/4,W/2,H/2);QImage image(":images/images/qt.jpg");painter.drawImage(rect, image);
drawLine 画直线 QLine Line(W/4,H/4,W/2,H/2); painter.drawLine(Line);
drawLines 画一批直线 QRect rect(W/4,H/4,W/2,H/2); QVector Lines;Lines.append(QLine(rect.topLeft(),rect.bottomRight()));Lines.append(QLine(rect.topRight(),rect.bottomLeft()));Lines.append(QLine(rect.topLeft(),rect.bottomLeft()));Lines.append(QLine(rect.topRight(),rect.bottomRight()));painter.drawLines(Lines);
drawPath 绘制由QPainterPath对象定义的路线 QRect rect(W/4,H/4,W/2,H/2);QPainterPath path;path.addEllipse(rect);path.addRect(rect);painter.drawPath(path);
drawPie 绘制扇形 QRect rect(W/4,H/4,W/2,H/2);int startAngle = 40 * 16;//起始40° int spanAngle = 120 * 16;//旋转120° painter.drawPie(rect, startAngle, spanAngle);
drawPixmap 绘制Pixmap图片 QRect rect(W/4,H/4,W/2,H/2);QPixmap pixmap(":images/images/qt.jpg");painter.drawPixmap(rect, pixmap);
drawPoint 画一个点 painter.drawPoint(QPoint(W/2,H/2));
drawPoints 画一批点 QPoint points\[\]={QPoint(5W/12,H/4),QPoint(3 W/4,5H/12),QPoint(2W/4,5*H/12) };painter.drawPoints(points, 3);
drawPolygon 画多边形,最后一个点会和第一个点闭合 QPoint points\[\]={QPoint(5W/12,H/4),QPoint(3 W/4,5H/12),QPoint(5 W/12,3H/4),QPoint(2W/4,5*H/12) };painter.drawPolygon(points, 4);
drawPolyline 画多点连接的线,最后一个点不会和第一个点连接 QPoint points\[\]={QPoint(5W/12,H/4),QPoint(3 W/4,5H/12),QPoint(5 W/12,3H/4),QPoint(2W/4,5*H/12), };painter.drawPolyline(points, 4);
drawRect 画矩形 QRect rect(W/4,H/4,W/2,H/2);painter.drawRect(rect);
drawRoundedRect 画圆角矩形 QRect rect(W/4,H/4,W/2,H/2);painter.drawRoundedRect(rect,20,20);
drawText 绘制文本,只能绘制单行文字,字体的大小等属性由QPainter::font()决定。QRect rect(W/4,H/4,W/2,H/2);QFont font;font.setPointSize(30);font.setBold(true);painter.setFont(font);painter.drawText (rect,"Hello,Qt");
eraseRect 擦除某个矩形区域,等效于用背景色填充该区域 QRect rect(W/4,H/4,W/2,H/2); painter.eraseRect(rect);
fillPath 填充某个QPainterPath定义的绘图路径,但是轮廓线不显示 QRect rect(W/4,H/4,W/2,H/2);QPainterPath path;path.addEllipse(rect);path.addRect(rect);painter.fillPath(path,Qt::red);
fillRect 填充一个矩形,无边框线 QRect rect(W/4,H/4,W/2,H/2); painter.fillRect (rect,Qt::green);

这些基本图形元件的绘制用户可以通过修改samp8_1的paintEvent()里的代码进行测试,这里就不再详细举例和说明了。

2.QPainterPath的使用

在表8-4列举的QPainter绘制基本图形元件的函数中,一般的图形元件的绘制都比较简单和直观,只有drawPath()函数是绘制一个复合的图形对象,它使用一个QPainterPath类型的参数作为绘图对象。drawPath()函数的原型是:

复制代码
void QPainter::drawPath(const QPainterPath &path)

QPainterPath是一系列绘图操作的顺序集合,便于重复使用。一个PainterPath由许多基本的绘图操作组成,如绘图点移动、划线、画圆、画矩形等,一个闭合的PainterPath是终点和起点连接起来的绘图路径。使用QPainterPath的优点是绘制某些复杂形状时只需创建一个PainterPath,然后调用QPainter::drawPath()就可以重复使用。例如绘制一个复杂的星星图案需要多次调用lineto()函数,定义一个QPainterPath类型的变量path记录这些绘制过程,再调用drawPath(path)就可以完成星型图案的绘制。

QPainterPath提供了很多函数可以添加各种基本图形元件的绘制,其功能与QPainter提供的绘制基本图件的功能类似,也有一些用于PainterPath的专用函数,如closeSubpath()、connectPath()等,对于QPainterPath的函数功能不做详细说明,可以参考Qt帮助文件查看QPainterPath类的详细描述。在下一节的实例samp8_2中将结合QPainter的坐标变换功能演示QPainterPath 绘制多个星星的实现方法。

8.2 坐标系统和坐标变换

8.2.1 坐标变换函数

QPainter在窗口上绘图的默认坐标系统如原书的图8-1所示,这是绘图设备的物理坐标。为了绘图的方便,QPainter提供了一些坐标变换的功能,通过平移、旋转等坐标变换,得到一个逻辑坐标系统,使用逻辑坐标系统在某些时候绘图更方便。坐标变换函数见表8-5。

表8-5 QPainter有关坐标变换操作的函数

分组 函数原型 功能
坐标变换 void translate(qreal dx, qreal dy) 坐标系统平移一定的偏移量,坐标原点平移到新的点
void rotate(qreal angle) 坐标系统顺时针旋转一个角度
void scale(qreal sx, qreal sy) 坐标系统缩放
void shear(qreal sh, qreal sv) 坐标系统做扭转变换
状态保存与恢复 void save() 保存painter当前的状态,就是将当前状态压入堆栈
void restore() 恢复上一次状态,就是从堆栈中弹出上次的状态
void resetTransform() 复位所有的坐标变换

常用的坐标变换是平移、旋转和缩放,使用世界坐标变换矩阵也可以实现这些变换功能,但是需要单独定义一个QTransform类的变量,对于QPainter来说,简单的坐标变换使用QPainter自有的坐标变换函数就足够了。

1.坐标平移

坐标平移函数是translate(),其中一种参数形式的函数原型是:

复制代码
void translate(qreal dx, qreal dy)

表示将坐标系统水平方向平移dx个单位,垂直方向平移dy个单位,在缺省的坐标系统中,单位就是像素。如果是从原始状态平移(dx, dy),那么平移后的坐标原点就移到了(dx, dy)。

假设一个绘图窗口宽度为300像素,高度为200像素,则其原始坐标系统如原书的图8-10左所示;若执行平移函数translate(150, 100),则坐标系统水平向右平移150像素,向下平移100像素,平移后的坐标系统如图 8-10 右所示,坐标原点在窗口的中心,而左上角的坐标变为(−150, −100),右下角的坐标变为(150, 100)。如此将坐标原点变换到窗口中心在绘制某些图形时是非常方便的。

2.坐标旋转

坐标旋转的函数是rotate(),其函数原型为:

复制代码
void rotate(qreal angle)

它是将坐标系统绕坐标原点顺时针旋转angle角度,单位是度。当angle为正数时是顺时针旋转,为负数时是逆时针旋转。

在原书的图8-10右的基础上,若执行rotate(90),则得到原书的图8-11所示的坐标系统。

注意:旋转之后并不改变窗口矩形的实际大小,只是改变了坐标轴的方向。

在图8-11的新坐标系下,窗口左上角的坐标变成了(-100, 150),而右下角的坐标变成了(100, -150)。

3.缩放

缩放函数是scale(),其函数原型为:

复制代码
void scale(qreal sx, qreal sy)

其中,sx, sy分别为横向和纵向缩放比例,比例大于1是放大,小于1是缩小。

4.状态保存与恢复

进行坐标变换时,QPainter内部实际上有一个坐标变换矩阵,用save()保存当前坐标状态,用restore()恢复上次保存的坐标状态,这两个函数必须配对使用,操作的是一个堆栈对象。

resetTransform()函数则是复位所有坐标变换操作,恢复原始的坐标系统。

8.2.2 坐标变换绘图实例

1.绘制3个五角星的程序

创建一个基于QWidget的窗口的应用程序samp8_2,窗体上不放置任何组件。在Widget类的构造函数和paintEvent()事件中编写代码,代码内容如下。

复制代码
Widget::Widget(QWidget *parent) :   QWidget(parent),   ui(new Ui::Widget)
{
   ui->setupUi(this);
   setPalette(QPalette(Qt::white)); //设置窗口背景色
   setAutoFillBackground(true);
   resize(600,300); //固定初始化窗口大小
}
void Widget::paintEvent(QPaintEvent *event)
{
   QPainter   painter(this); 
   painter.setRenderHint(QPainter::Antialiasing);
   painter.setRenderHint(QPainter::TextAntialiasing);
//生成五角星的5个顶点的坐标,假设原点在五角星中心
   qreal   R=100; //半径
   const   qreal Pi=3.14159;
   qreal   deg=Pi*72/180;
   QPoint points[5]={
      QPoint(R,0),
      QPoint(R*std::cos(deg),-R*std::sin(deg)),
      QPoint(R*std::cos(2*deg),-R*std::sin(2*deg)),
      QPoint(R*std::cos(3*deg),-R*std::sin(3*deg)),
      QPoint(R*std::cos(4*deg),-R*std::sin(4*deg)),
   };
//设置字体
   QFont   font;
   font.setPointSize(12);
   font.setBold(true);
   painter.setFont(font);
//设置画笔
   QPen   penLine;
   penLine.setWidth(2); //线宽
   penLine.setColor(Qt::blue); //划线颜色
   penLine.setStyle(Qt::SolidLine);//线的类型 
   penLine.setCapStyle(Qt::FlatCap);//线端点样式
   penLine.setJoinStyle(Qt::BevelJoin);//线的连接点样式
   painter.setPen(penLine);
//设置画刷
   QBrush  brush;
   brush.setColor(Qt::yellow); //画刷颜色
   brush.setStyle(Qt::SolidPattern); //画刷填充样式
   painter.setBrush(brush);
//设计绘制五角星的PainterPath,以便重复使用
   QPainterPath starPath;
   starPath.moveTo(points[0]);
   starPath.lineTo(points[2]);
   starPath.lineTo(points[4]);
   starPath.lineTo(points[1]);
   starPath.lineTo(points[3]);
   starPath.closeSubpath(); //闭合路径,最后一个点与第一个点相连
   starPath.addText(points[0],font,"0"); //显示端点编号
   starPath.addText(points[1],font,"1");
   starPath.addText(points[2],font,"2");
   starPath.addText(points[3],font,"3");
   starPath.addText(points[4],font,"4");
//绘图
   painter.save(); //保存坐标状态
   painter.translate(100,120);//平移
   painter.drawPath(starPath); //画星星
   painter.drawText(0,0,"S1");
   painter.restore(); //恢复坐标状态

   painter.translate(300,120); //平移
   painter.scale(0.8,0.8); //缩放
   painter.rotate(90); //顺时针旋转90度
   painter.drawPath(starPath);//画星星
   painter.drawText(0,0,"S2");

   painter.resetTransform(); //复位所有坐标变换
   painter.translate(500,120); //平移
   painter.rotate(-145); //逆时针旋转145度
   painter.drawPath(starPath);//画星星
   painter.drawText(0,0,"S3");
}

运行该实例程序,得到如原书的图8-12所示的结果,在窗口上绘制了3个五角星。

第1个是原始的五角星;第2个是缩小为0.8倍,顺时针旋转90度的五角星;第3个是逆时针旋转145度的五角星。这个程序中使用到了QPainterPath和QPainter的坐标变换功能。

2.绘制五角星的PainterPath的定义

首先假设一个五角星的中心点是原点,第0个点在X轴上,五角星外接圆半径为100,计算出5个点的坐标,保存在points数组中。

然后定义了一个QPainterPath类的变量starPath,用于记录画五角星的过程,就是几个点的连线过程,并且标注点的编号。使用QPainterPath的优点就是定义一个QPainterPath类型的变量记录一个复杂图形的绘制过程后,可以重复使用。虽然points数组中的点的坐标是假设五角星的中心点是原点,在绘制不同的五角星时只需将坐标平移到新的原点位置,就可以绘制不同的五角星。

绘制第1个五角星的程序是:

复制代码
painter.save(); //保存坐标状态
painter.translate(100,120);
painter.drawPath(starPath); //画星星
painter.drawText(0,0,"S1");
painter.restore(); //恢复坐标状态

这里,save()函数保存当前坐标状态(也就是坐标的原始状态),然后将坐标原点平移到(100,120),调用绘制路径的函数drawPath(starPath)绘制五角星,在五角星的中心标注"S1"表示第1个五角星,最后调用restore()函数恢复上次的坐标状态。这样就以(100,120)为中心点绘制了第1个五角星。

绘制第2个五角星的程序是:

复制代码
painter.translate(300,120); //平移
painter.scale(0.8,0.8); //缩放
painter.rotate(90); //顺时针旋转90度
painter.drawPath(starPath);//画星星
painter.drawText(0,0,"S2");

这里首先调用坐标平移函数translate(300, 120)。由于上次restore()之后回到坐标初始状态,所以这次平移后,坐标原点到了物理坐标(300, 120)。而如果没有上一个restore(),会在上一次的坐标基础上平移。

绘图之前调用了缩放函数scale(0.8, 0.8),使得缩小到原来的0.8,再顺时针旋转90°,然后调用绘制路径函数drawPath(starPath)绘制五角星,就得到了第2个五角星。

绘制第3个五角星时首先使坐标复位,即:

复制代码
painter.resetTransform(); //复位所有坐标变换

这样会复位所有坐标变换,又回到了原始坐标。

8.2.3 视口和窗口

1.视口和窗口的定义与原理

绘图设备的物理坐标是基本的坐标系,通过QPainter的平移、旋转等变换可以得到更容易操作的逻辑坐标。

为了实现更方便的坐标,QPainter还提供了视口(Viewport)和窗口(Window)坐标系,通过QPainter内部的坐标变换矩阵自动转换为绘图设备的物理坐标。

视口表示绘图设备的任意一个矩形区域的物理坐标,可以只选取物理坐标的一个矩形区域用于绘图。默认情况下,视口等于绘图设备的整个矩形区。

窗口与视口是同一个矩形,只不过是用逻辑坐标定义的坐标系。窗口可以直接定义矩形区的逻辑坐标范围。原书的图8-13是对视口和窗口的图示说明。

图8-13左图中的矩形框代表绘图设备的物理大小和坐标范围,假设宽度为300像素,高度为200像素。现在要取其中间的一个正方形区域作为视口,灰色的正方形就是视口,绘图设备的物理坐标中,视口的左上角坐标为(50, 0),右下角坐标为(250, 200)。定义此视口,可以使用QPainter的setViewport()函数,其函数原型为:

复制代码
void QPainter::setViewport(int x, int y, int width, int height)

要定义图8-13左图中的视口,使用下面的语句:

复制代码
painter.setViewport(50,0,200,200);

表示从绘图设备物理坐标系统的起点(50, 0)开始,取宽度为200、高度为200的一个矩形区域作为视口。

对于图8-13左图的视口所表示的正方形区域,定义一个窗口(图8-13右图),窗口坐标的中心在正方形中心,并设置正方形的逻辑边长为100。可使用QPainter的setWindow()函数,其函数原型为:

复制代码
void QPainter::setWindow(int x, int y, int width, int height)

所以,此处定义窗口的语句是:

复制代码
painter.setWindow(-50,-50,100,100);

它表示对应于视口的矩形区域,其窗口左上角的逻辑坐标是(-50, -50),窗口宽度为100,高度为100。这里设置的窗口还是一个正方形,使得从视口到窗口变换时,长和宽的变化比例是相同的。实际可以任意指定窗口的逻辑坐标范围,长和宽的变化比例不相同也是可以的。

2.视口和窗口的使用实例

使用窗口坐标的优点是,只需按照窗口坐标定义来绘图,而不用管实际的物理坐标范围的大小。例如在一个固定边长为100的正方形窗口内绘图,当实际绘图设备大小变化时,绘制的图形会自动变化大小。这样,就可以将绘图功能与绘图设备隔离开来,使得绘图功能适用于不同大小、不同类型的设备。

实例samp8_3演示了使用视口和窗口的方法,项目创建与samp8_1类似,只在Widegt的paintEvent()事件里添加绘图代码。

复制代码
void Widget::paintEvent(QPaintEvent *event)
{
   QPainter   painter(this);
   int W=width();
   int H=height();
   int side=qMin(W,H);//取长和宽的较小值
   QRect rect((W-side)/2, (H-side)/2,side,side); //viewport矩形区
   painter.drawRect(rect); //Viewport的矩形区域
   painter.setViewport(rect);//设置Viewport
   painter.setWindow(-100,-100,200,200); // 设置窗口大小,逻辑坐标
   painter.setRenderHint(QPainter::Antialiasing);
//设置画笔
   QPen   pen;
   pen.setWidth(1); //线宽
   pen.setColor(Qt::red); //划线颜色
   pen.setStyle(Qt::SolidLine);//线的类型 
   painter.setPen(pen);
   for(int i=0; i

运行实例程序samp8_3,可以得到如原书的图8-14所示的图形效果。当窗口的宽度大于高度时,以高度作为正方形的边长;当高度大于宽度时,以宽度作为正方形边长,且图形是自动缩放的。

程序首先定义了一个正方形视口,正方形以绘图设备的长、宽中较小者为边长。

然后定义窗口,定义的窗口是原点在中心,边长为200的正方形。

图8-14的图形效果实际上是画了36个圆得到的,循环部分的代码如下:

复制代码
for(int i=0; i<36;i++)
{
   painter.drawEllipse(QPoint(50,0),50,50);
   painter.rotate(10);
}

每个圆的圆心在X轴上的(50, 0),半径为50。画完一个圆之后坐标系旋转10°,再画相同的圆,这就巧妙应用了坐标轴的旋转。

8.2.4 绘图叠加的效果

对上面的程序稍作修改,增加渐变填充和叠加效果的设置。

复制代码
void Widget::paintEvent(QPaintEvent *event)
{
   QPainter   painter(this);
   int W=width();
   int H=height();
   int side=qMin(W,H);//取长和宽的较小值
   QRect rect((W-side)/2, (H-side)/2,side,side); //viewport矩形区
   painter.drawRect(rect); //Viewport大小
   painter.setViewport(rect);//设置Viewport
   painter.setWindow(-100,-100,200,200); // 设置窗口大小,逻辑坐标
   painter.setRenderHint(QPainter::Antialiasing);
//设置画笔
   QPen   pen;
   pen.setWidth(1); //线宽
   pen.setColor(Qt::red); //划线颜色
   pen.setStyle(Qt::SolidLine);//线的类型
   painter.setPen(pen);
//线性渐变
   QLinearGradient  linearGrad(0,0,100,0);//从左到右,
   linearGrad.setColorAt(0,Qt::yellow);//起点颜色
   linearGrad.setColorAt(1,Qt::green);//终点颜色
   linearGrad.setSpread(QGradient::PadSpread);  //展布模式
   painter.setBrush(linearGrad);
//设置复合模式
  painter.setCompositionMode(QPainter::RasterOp_NotSourceXorDestination);
//   painter.setCompositionMode(QPainter::CompositionMode_Difference);
//   painter.setCompositionMode(QPainter::CompositionMode_Exclusion);
   for(int i=0; i

在上面的程序中,对单个圆使用了线性渐变填充,单个圆从左到右,由黄色渐变为绿色。

使用QPainter::setCompositionMode()函数设置组合模式,即后面绘制的图与前面绘制的图的叠加模式。函数参数是一个QPainter::CompositionMode枚举类型值,可以查看Qt帮助,这个枚举类型有近40种取值,表示了后绘制图形与前面图形的不同叠加运算方式。

原书的图8-15是其中两种叠加模式下的绘图效果,可以发现采用不同的叠加模式,可以得到不同的绘图效果,甚至是意想不到的绚丽效果。用户可以自己修改程序,设置不同渐变颜色、渐变填充模式、不同叠加模式,也许能绘制出更炫的图形呢。

8.3 Graphics View绘图架构

8.3.1 场景、视图与图形项

采用QPainter绘图时需要在绘图设备的paintEvent()事件里编写绘图的程序,实现整个绘图过程。这种方法如同使用Windows的画图软件在绘图,绘制的图形是位图,这种方法适合于绘制复杂性不高的固定图形,不能实现图件的选择、编辑、拖放、修改等功能。

Qt为绘制复杂的可交互图形提供了Graphics View 绘图架构,是一种基于图形项(Graphics Item)的模型/视图模式,与第5章的数据编辑与显示的Model/View模式类似。使用Graphics View架构可以绘制复杂的有几万个基本图形元件的图形,并且每个图形元件是可选择、可拖放和修改的,类似于矢量绘图软件的绘图功能。

Graphics View架构主要由3个部分组成,即场景、视图和图形项,其构成的Graphics View绘图系统结构如原书的图8-16所示。

1.场景

QGraphicsScene类提供绘图场景(Scene)。场景是不可见的,是一个抽象的管理图形项的容器,可以向场景添加图形项,获取场景中的某个图形项等。场景主要具有如下一些功能:

  • 提供管理大量图形项的快速接口;

  • 将事件传播给每个图形项;

  • 管理每个图形项的状态,例如选择状态、焦点状态等;

  • 管理未经变换的渲染功能,主要用于打印。

除了图形项之外,场景还有背景层和前景层,通常由QBrush指定,也可以通过重新实现drawBackground()和drawForeground()函数来实现自定义的背景和前景,实现一些特殊效果。

2.视图

QGraphicsView 提供绘图的视图(View)组件,用于显示场景中的内容。可以为一个场景设置几个视图,用于对同一个数据集提供不同的视口。

在原书的图8-16中,虚线框的部分是一个场景,视图1比场景大,显示场景的全部内容。缺省情况下,当视图大于场景时,场景在视图的中间部分显示,也可以设置视图的Alignment属性控制场景在视图中的显示位置;当视图小于场景时(见图8-16中的视图2),视图只能显示场景的一部分内容,但是会自动提供卷滚条在整个场景内移动。

视图接收键盘和鼠标输入并转换为场景事件,并进行坐标转换后传送给可视场景。

3.图形项

图形项(Graphics Item)就是一些基本的图形元件,图形项的基类是QGraphicsItem。Qt提供了一些基本的图形项,如绘制椭圆的QGraphicsEllipseItem、绘制矩形的QGraphicsRectItem、绘制文字的QGraphicsTextItem等。

QGraphicsItem支持如下的一些操作:

  • 支持鼠标事件响应,包括鼠标按下、移动、释放、双击,还包括鼠标停留、滚轮、快捷菜单等事件;

  • 支持键盘输入,按键事件;

  • 支持拖放操作;

  • 支持组合,可以是父子项关系组合,也可以是通过QGraphicsItemGroup类进行组合。

所以,图形项可以被选择、拖放、组合,若编写信号槽函数代码,还可以实现各种编辑功能。一个图形项还可以包含子图形项,图形项还支持碰撞检测,即是否与其他图形项碰撞。

在图8-16所示的视图、场景和图形项之间的关系示意图中,场景是图形项的容器,可以在场景上绘制很多图形项,每个图形项就是一个对象,这些图形项可以被选择、拖动等。视图是显示场景的一部分区域的视口,一个场景可以有多个视图,一个视图显示场景的部分区域或全部区域,或从不同角度观察场景。

8.3.2 Graphics View的坐标系统

Graphics View系统有3个有效的坐标系,图形项坐标、场景坐标、视图坐标。3个坐标系的示意图如原书的图8-17所示。绘图的时候,场景的坐标等价于QPainter的逻辑坐标,一般以场景的中心为原点;视图坐标与设备坐标相同,是物理坐标,缺省以左上角为原点;图形项坐标是局部逻辑坐标,一般以图件的中心为原点。

1.图形项坐标

图形项使用自己的局部坐标(Item Coordinates),通常以其中心为(0, 0),也是各种坐标变换的中心。图形项的鼠标事件的坐标是用局部坐标表示的,创建自定义图形项,绘制图形项时只需考虑其局部坐标,QGraphicsScene和QGraphicsView会自动进行坐标转换。

一个图形项的位置是其中心点在父坐标系统中的坐标,对于没有父图形项的图形项,其父对象就是场景,图形项的位置就是在场景中的坐标。

如果一个图形项还是其他图形项的父项,父项进行坐标变换时,子项也做同样的坐标变换。

QGraphicsItem的大多数函数都是在其局部坐标系上操作的,例如一个图形项的边界矩形QGraphicsItem::boundingRect()是用局部坐标给出的,但是QGraphicsItem::pos()是仅有的几个例外,它返回的是图形项在父项坐标系中的坐标,如果是顶层图形项,就是在场景中的坐标。

2.视图坐标

视图坐标(View Coordinates)就是窗口界面(widget)的物理坐标,单位是像素。视图坐标只与widget或视口有关,而与观察的场景无关。QGraphicsView视口的左上角坐标总是(0,0)。

所有的鼠标事件、拖放事件的坐标首先是由视图坐标定义的,然后用户需要将这些坐标映射为场景坐标,以便和图形项交互。

3.场景坐标

场景是所有图形项的基础坐标,场景坐标(Scene Coordinates)描述了每个顶层图形项的位置。创建场景时可以定义场景矩形区的坐标范围,例如

复制代码
scene=new QGraphicsScene(-400,-300,800,600);

这样定义的scene是左上角坐标为(-400, -300),宽度为800,高度为600的矩形区域,单位是像素。

每个图形项在场景里都有一个位置坐标,由函数QGraphicsItem::scenePos()给出;还有一个图形项边界矩形,由QGraphicsItem::sceneBoundingRect()函数给出。边界矩形可以使QGraphicsScene知道场景的哪个区域发生了变化。场景发生变化时会发射QGraphicsScene::changed()信号,参数是一个场景的矩形列表,表示发生变化的矩形区。

4.坐标映射

在场景中操作图形项时,进行场景到图形项、图形项到图形项,或视图到场景之间的坐标变换是比较有用的,即坐标映射(Coordinate Mapping)。例如,在QGraphicsView的视口上单击鼠标时,通过函数QGraphicsView::mapToScene()可以将视图坐标映射为场景坐标,然后用QGraphicsScene::itemAt()函数可以获取场景中鼠标光标处的图形项。

8.3.3 Graphics View相关的类

Graphics View结构的主要类包括视图类QGraphicsView、场景类QGraphicsScene,和各种图形项类,图形项类的基类都是QGraphicsItem。

1.QGraphicsView类的主要接口函数

QGraphicsView是用于观察一个场景的物理窗口,当场景小于视图时,整个场景在视图中可见;当场景大于视图时,视图自动提供卷滚条。

QGraphicsView的视口坐标等于显示设备的物理坐标,但是也可以对QGraphicsView的坐标进行平移、旋转、缩放等变换。

表8-6是QGraphicsView的主要接口函数。一般的设置函数还有一个对应的读取函数,如setScene()对应的读取函数是scene(),这里只列出设置函数。并且仅列出函数的返回数据类型,省略了输入参数,函数的详细定义见Qt帮助文件。

表8-6 QGraphicsView主要函数功能说明

分组 函数 功能描述
场景 void setScene() 设置关联显示的场景
void setSceneRect() 设置场景在视图中可视的部分的矩形区域
外观 void setAlignment() 设置场景在视图中的对齐方式,缺省是上下都居中
void setBackgroundBrush() 设置场景的背景画刷
void setForegroundBrush() 设置场景的前景画刷
void setRenderHints() 设置视图的绘图选项
交互 void setInteractive() 是否允许场景可交互,如果禁止交互,则任何键盘或鼠标操作都被忽略
QRect rubberBandRect() 返回选择矩形框
void setRubberBandSelectionMode() 选择模式,参数为枚举类型Qt::ItemSelectionMode
QGraphicsItem* itemAt() 获取视图坐标系中某个位置处的图形项
QList<QGraphicsItem*> items() 获取场景中的所有、或者某个选择区域内图形项的列表
坐标
映射 QPoint mapFromScene() 将场景中的一个坐标转换为视图的坐标
QPointF mapToScene() 将视图中的一个坐标转换为场景的坐标
2.QGraphicsScene类的主要接口函数

QGraphicsScene是用于管理图形项的场景,是图形项的容器,有添加、删除图形项的函数,管理图形项的各种函数。表8-7是QGraphicsScene的主要接口函数(仅列出函数的返回数据类型,省略了输入参数)。

表8-7 QGraphicsScene主要函数功能说明

分组 函数 功能描述
场景 void setSceneRect() 设置场景的矩形区
分组 QGraphicsItemGroup* createItemGroup() 创建图形项组
void destroyItemGroup() 解除一个图形项组
输入焦点 QGraphicsItem * focusItem() 返回当前获得焦点的图形项
void clearFocus() 去除选择焦点
bool hasFocus() 视图是否有焦点
图形项操作 void addItem() 添加一个已经创建的图形项
void removeItem() 删除图形项
void clear() 清除所有图形项
QGraphicsItem* mouseGrabberItem() 返回鼠标抓取的图形项
QList<QGraphicsItem *> selectedItems() 返回选择的图形项列表
void clearSelection() 清除所有选择
QGraphicsItem * itemAt() 获取某个位置处的顶层图形项
QList<QGraphicsItem *> items() 返回某个矩形区域、多边形等选择区域内的图形项列表
添加图形项 QGraphicsEllipseItem * addEllipse() 添加一个椭圆
QGraphicsLineItem * addLine() 添加一条直线
QGraphicsPathItem * addPath() 添加一个绘图路径(QPainterPath)
QGraphicsPixmapItem * addPixmap() 添加一个图片
QGraphicsPolygonItem * addPolygon() 添加一个多边形
QGraphicsRectItem * addRect() 添加一个矩形
QGraphicsSimpleTextItem * addSimpleText() 添加简单文字
QGraphicsTextItem * addText() 添加字符串
QGraphicsProxyWidget * addWidget() 添加界面组件
3.图形项

QGraphicsItem是所有图形项的基类,用户也可以从QGraphicsItem继承定义自己的图形项。Qt定义了一些常见的图形项,这些常见的图形项的类的继承关系如原书的图8-18所示。

QGraphicsItem类提供了图形项操作的函数,常见的函数见表8-8(仅列出函数的返回数据类型,省略了输入参数)。

表8-8 QGraphicsItem主要函数功能说明

分组 函数 功能描述
属性设置 void setFlags() 设置图形项的操作属性,例如,可选择、可移动等
void setOpacity() 设置透明度
void setGraphicsEffect() 设置图形效果
void setSelected() 图形项是否被选中
void setData() 用户自定义数据
坐标 void setX() 图形项的X坐标
void setY() 图形项的Y坐标
void setZValue() 图形项的Z值,Z值控制图形项的叠放次序
void setPos() 图形项在父项中的位置
QPointF scenePos() 返回图形项在场景中的坐标,相当于调用mapToScene(0, 0)
坐标变换 void resetTransform() 复位坐标系,取消所有坐标变换
void setRotation() 旋转一定角度,参数为正数时表示顺时针旋转
void setScale() 按比例缩放,缺省值为1
坐标映射 QPointF mapFromItem() 将另一个图形项的一个点映射到本图形项的坐标系
QPointF mapFromParent() 将父项的一个点映射到本图形项的坐标系
QPointF mapFromScene() 将场景中的一个点映射到本图形项的坐标系
QPointF mapToItem() 将本图形项内的一个点映射到另一个图形项的坐标系
QPointF mapToParent() 将本图形项内的一个点映射到父项坐标系
QPointF mapToScene() 将本图形项内的一个点映射到场景坐标系

setFlags()函数可以设置一个图形项的操作标志,包括可选择、可移动、可获取焦点等,如:

复制代码
item->setFlags(QGraphicsItem::ItemIsMovable
            | QGraphicsItem::ItemIsSelectable
            | QGraphicsItem::ItemIsFocusable);

setPos()函数设置图形项在父项中的坐标,如果没有父项,就是在场景中的坐标。

setZValue()控制图形项的叠放次序,当有多个图形项有重叠时,zValue越大的,越显示在前面。

图形项可以通过setRotation()进行旋转,通过setScale()进行缩放。

图形项还可以与其他图形项、父项和场景之间进行坐标转换,这些将在实例程序中讲解。

8.3.4 Graphics View程序基本结构和功能实现

1.实例程序功能

创建一个实例samp8_4,是一个以QMainWindow为基类的窗口程序,其运行时界面如原书的图8-19所示。

实例程序samp8_4的主要功能包括以下几点。

  • 工作区是一个从QGraphicsView继承的自定义类QWGraphicsView,作为绘图的视图组件。

  • 创建一个QGraphicsScene场景,场景的大小就是图中的实线矩形框的大小。

  • 改变窗口大小,当视图大于场景时,矩形框总是居于图形视图的中央;当视图小于场景时,在视图窗口自动出现卷滚条。

  • 蓝色椭圆正好处于场景的中间,红色圆形位于场景的右下角。当图形项位置不在场景的矩形框中时,图形项也是可以显示的。

  • 当鼠标在窗口上移动时,会在状态栏显示当前光标位置的视图坐标和场景坐标,在某个图形项上单击鼠标时,还会显示在图形项中的局部坐标。

这个实例演示了Graphics View绘图几个类的基本使用方法,演示视图、场景和绘图项3个坐标系的关系,以及它们之间的坐标转换。

2.自定义图形视图组件

QGraphicsView是Qt的图形视图组件,在UI设计器的Display Widgets分组里可以拖放一个QGraphicsView组件到窗口上。但是本实例中需要实现鼠标在QGraphicsView上移动时就显示当前光标的坐标,这涉及mouseMoveEvent()事件的处理。QGraphicsView没有与mouseMoveEvent()相关的信号,因而无法定义槽函数与此事件相关联。

为此,从QGraphicsView继承定义一个类QWGraphicsView,实现mouseMoveEvent()事件和mousePressEvent()事件,并把鼠标事件转换为信号,这样就可以在主程序里设计槽函数响应这些鼠标事件。下面是QWGraphicsView类的定义:

复制代码
class QWGraphicsView : public QGraphicsView
{
   Q_OBJECT
protected:
   void mouseMoveEvent(QMouseEvent *event);
   void mousePressEvent(QMouseEvent *event);
public:
   QWGraphicsView(QWidget *parent = 0);
signals:
   void mouseMovePoint(QPoint point);
   void mouseClicked(QPoint point);
};

mouseMoveEvent()是鼠标移动事件,其实现代码如下:

复制代码
void QWGraphicsView::mouseMoveEvent(QMouseEvent *event)
{//鼠标移动事件
   QPoint  point=event->pos(); //QGraphicsView的坐标
   emit mouseMovePoint(point); //发射信号
   QGraphicsView::mouseMoveEvent(event);
}

在此事件响应代码里,通过事件的pos()函数获取鼠标光标在视图中的坐标point,然后作为参数发射mouseMovePoint(point)信号。这样,若在其他地方编写槽函数与此信号关联,就可以对鼠标移动事件作出响应。

mousePressEvent()是鼠标按键按下的事件,其实现代码如下:

复制代码
void QWGraphicsView::mousePressEvent(QMouseEvent *event)
{ //鼠标左键按下事件
   if (event->button()==Qt::LeftButton)
   {
      QPoint  point=event->pos(); //QGraphicsView的坐标
      emit mouseClicked(point);//发射信号
   }
   QGraphicsView::mousePressEvent(event);
}

在此事件响应代码里,首先判断是否是鼠标左键按下,然后通过事件的pos()函数获取鼠标光标在视图中的坐标point,然后作为参数发射mouseClicked(point)信号。

3.主窗口类定义与QGraphicsView组件升级

主窗口是一个从QMainWindow继承的类,重定义了resizeEvent()事件,对窗口改变大小的事件作出响应。槽函数on_mouseMovePoint()响应鼠标在图形视图上移动的事件信号,显示视图坐标和场景坐标;槽函数on_mouseClicked()响应鼠标单击信号,显示图形项的局部坐标;iniGraphicsSystem()用于创建Graphics View结构的各个对象。

MainWindow类的完整定义如下:

复制代码
class MainWindow : public QMainWindow
{
   Q_OBJECT
private:
   QGraphicsScene  *scene;
   QLabel  *labViewCord;
   QLabel  *labSceneCord;
   QLabel  *labItemCord;
   void iniGraphicsSystem(); //创建Graphics View的各项
protected:
   void resizeEvent(QResizeEvent *event); 
public:
   explicit MainWindow(QWidget *parent = 0);
   ~MainWindow();
private slots:
   void   on_mouseMovePoint(QPoint point);
   void   on_mouseClicked(QPoint point);
private:
   Ui::MainWindow *ui;
};

主窗口是采用UI设计器进行可视化设计的。在设计界面时,先从组件面板里放一个QGraphicsView组件到窗口上。但是我们要用的是从QGraphicsView继承的自定义图形视图组件QWGraphicsView,需要将QGraphicsView升级为QWGraphicsView。

在设计的窗口上选中放置的QGraphicsView组件,单击右键,在快捷菜单中选择"Promote to...",出现如原书的图8-20的对话框。在其中选择基类名称QGraphicsView,升级类名称输入QWGraphicsView,然后单击"Promote"按钮,就可以将窗口上的QGraphicsView组件升级为QWGraphicsView组件。这是使用自定义界面组件的一种方法,在第12章会详细介绍。

4.窗口初始化

下面是主窗口的构造函数的代码:

复制代码
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
   ui->setupUi(this);
   labViewCord=new QLabel("View 坐标:");
   labViewCord->setMinimumWidth(150);
   ui->statusBar->addWidget(labViewCord);
   labSceneCord=new QLabel("Scene 坐标:");
   labSceneCord->setMinimumWidth(150);
   ui->statusBar->addWidget(labSceneCord);
   labItemCord=new QLabel("Item 坐标:");
   labItemCord->setMinimumWidth(150);
   ui->statusBar->addWidget(labItemCord);
   ui->View->setCursor(Qt::CrossCursor);
   ui->View->setMouseTracking(true);
   ui->View->setDragMode(QGraphicsView::RubberBandDrag);
   QObject::connect(ui->View,SIGNAL(mouseMovePoint(QPoint)),
                 this, SLOT(on_mouseMovePoint(QPoint)));
   QObject::connect(ui->View,SIGNAL(mouseClicked(QPoint)),
                 this, SLOT(on_mouseClicked(QPoint)));
   iniGraphicsSystem();
}

槽函数on_mouseMovePoint()响应鼠标在图形视图上移动的mouseMovePoint()信号,实现代码如下:

复制代码
void MainWindow::on_mouseMovePoint(QPoint point)
{//鼠标移动事件,point是 GraphicsView的坐标,物理坐标
   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()));
}

信号传递的参数point就是鼠标在图形视图中的坐标,使用视图组件的mapToScene()函数可以将此坐标转换为场景中的坐标,这两个坐标在状态栏上显示。

槽函数on_mouseClicked()响应鼠标在图形视图上单击的mouseClicked()信号,代码如下:

复制代码
void MainWindow::on_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就是鼠标光标在图形视图中的坐标,先使用视图组件的mapToScene()函数将此坐标转换为场景中的坐标pointScene,然后通过场景对象的itemAt()函数获得光标下的图形项。如果鼠标光标下有图形项,就用图形项的mapFromScene()函数将pointScene转换为图形项的局部坐标pointItem,并在状态栏上显示。

另外还定义了窗口的resizeEvent()事件的响应函数,以便在窗口变化大小时,显示视图区域的大小,以及场景的大小信息。其代码如下:

复制代码
void MainWindow::resizeEvent(QResizeEvent *event)
{ //窗口变化大小时的事件
   ui->labViewSize->setText(QString::asprintf("Graphics View坐标,左上角总是(0,0),宽度=%d,高度=%d",ui->View->width(),ui->View->height()));
   QRectF  rectF=ui->View->sceneRect(); //Scene的矩形区
   ui->LabSceneRect->setText(QString::asprintf("QGraphicsView::sceneRect= (Left,Top,Width,Height)=%.0f,%.0f,%.0f,%.0f",rectF.left(),rectF.top(), rectF.width(),rectF.height()));
}
5.Graphics View系统初始化

构造函数中调用的iniGraphicsSystem()用于创建Graphics View结构中的其他元素,包括场景和多个图形项。其代码如下:

复制代码
void MainWindow::iniGraphicsSystem()
{ //构造Graphics View的各项
   QRectF   rect(-200,-100,400,200); //左上角坐标,宽度,高度
   scene=new QGraphicsScene(rect); //scene逻辑坐标系定义
   ui->View->setScene(scene);
//画一个矩形框,大小等于scene
   QGraphicsRectItem   *item=new QGraphicsRectItem(rect); 
   item->setFlags(QGraphicsItem::ItemIsSelectable  //设置flags
               | QGraphicsItem::ItemIsFocusable);
   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->setBrush(QBrush(Qt::red));
   item3->setFlags(QGraphicsItem::ItemIsMovable
                | QGraphicsItem::ItemIsSelectable
                | QGraphicsItem::ItemIsFocusable);
   scene->addItem(item3);
   scene->clearSelection();
}

可视化设计窗体时,将QWGraphicsView类对象的名称命名为View,并且自动填充主窗口的工作区。创建场景并与View关联的代码如下:

复制代码
QRectF   rect(-200,-100,400,200); //左上角坐标,宽度,高度
scene=new QGraphicsScene(rect); //scene逻辑坐标系定义
ui->View->setScene(scene);

这里用一个矩形定义了创建的场景的坐标系统,表示场景的左上角坐标是(-200, -100),场景宽度为400,高度为200,这样,场景的中心点是(0, 0),这是场景的坐标系。

创建了一个矩形框图形项item,矩形框的大小就等于创建的场景的大小,矩形框不能移动。

创建的第二个图形项item2是一个椭圆,椭圆的左上角坐标是(-100, -50),宽度200,高度100,所以椭圆的中心是(0, 0),这是图形项的局部坐标系。再采用setPos(0, 0)设置椭圆在场景中的位置,若不调用setPos()函数设置图形项在场景中的位置,缺省为位置为(0, 0)。椭圆设置为可移动、可选择、可以获得焦点。

创建的第三个图形项item3是一个圆,圆的左上角坐标是(-50, -50),宽度100,高度100,所以圆的中心是(0, 0),这是图形项的局部坐标系。再采用setPos()设置圆在场景中的位置。

复制代码
item3->setPos(rect.right(),rect.bottom());

其中心位置是场景的右下角,圆的一部分区域是超出了场景的矩形区域的,但是整个圆还是可以正常显示的。

从这个实例程序可以看到Graphics View结构中场景与视图的关系,如何创建图形项组件,场景、视图、图形项各自的坐标系以及坐标系之间的转换关系。

8.3.5 Graphics View绘图程序实例

1.实例功能

实例samp8_4只是演示了Graphics View的基本结构和3个坐标系的概念,为了演示Graphics View结构编程的更多功能,创建了实例程序samp8_5。

实例samp8_5是一个基于Graphics View结构的简单绘图程序,通过这个实例可以发现Graphics View图形编程更多功能的使用方法。程序运行界面如原书的图8-21所示。

这个实例程序具有如下的功能。

  • 可以创建矩形、椭圆、圆形、三角形、梯形、直线、文字等基本图形项。

  • 每个图形项都可以被选择和拖动。

  • 图形项或整个视图可以缩放和旋转。

  • 图形项重叠时,可以调整前置或后置。

  • 多个图形项可以组合,也可以解除组合。

  • 可以删除选择的图形项。

  • 鼠标在视图上移动时,会在状态栏显示视图坐标和场景坐标。

  • 鼠标单击某个图形项时,会显示图形项的局部坐标,也会显示图形项的文字描述和编号。

  • 双击某个图形项时,会根据图形项的类型调用颜色对话框或字体对话框,设置图形项的填充颜色、线条颜色或文字的字体。

  • 选中某个图形项时,可以进行按键操作,Delete键删除图形项,PgUp放大,PgDn缩小,空格键旋转90°,上下左右光标键移动图形项。

2.自定义图形视图类QWGraphicsView

与实例samp8_4类似,从QGraphicsView类继承定义一个图形视图类QWGraphicsView,在自定义类中添加鼠标和按键事件的处理,将鼠标和按键事件转换为信号,以便在主程序中设计槽函数做相应的处理。

下面是QWGraphicsView类的定义,处理了mouseMoveEvent()、mousePressEvent()、mouse DoubleClickEvent()和keyPressEvent()事件,定义了相应的信号,在事件处理里发射信号。

复制代码
class QWGraphicsView : public QGraphicsView
{
   Q_OBJECT
protected:
   void mouseMoveEvent(QMouseEvent *event);
   void mousePressEvent(QMouseEvent *event);
   void mouseDoubleClickEvent(QMouseEvent *event);
   void keyPressEvent(QKeyEvent *event);
public:
   QWGraphicsView(QWidget *parent = 0);
signals:
   void mouseMovePoint(QPoint point); //鼠标移动
   void mouseClicked(QPoint point); //鼠标单击
   void mouseDoubleClick(QPoint point); //双击事件
   void keyPress(QKeyEvent *event); //按键事件
};

下面是QWGraphicsView类的4个事件的实现代码,在每个事件里发射相应的信号。在QWGraphicsView类里,将鼠标和键盘事件转换为信号,就可以利用信号与槽机制,在主程序里设计槽函数对相应的鼠标和键盘操作进行响应。

复制代码
void QWGraphicsView::mouseMoveEvent(QMouseEvent *event)
{//鼠标移动事件
   QPoint  point=event->pos(); //QGraphicsView的坐标
   emit mouseMovePoint(point); //发射信号
   QGraphicsView::mouseMoveEvent(event);
}
void QWGraphicsView::mousePressEvent(QMouseEvent *event)
{ //鼠标按键按下事件
   if (event->button()==Qt::LeftButton)
   {
      QPoint  point=event->pos(); //QGraphicsView的坐标
      emit mouseClicked(point);//发射信号
   }
   QGraphicsView::mousePressEvent(event);
}
void QWGraphicsView::mouseDoubleClickEvent(QMouseEvent *event)
{ //鼠标双击事件
   if (event->button()==Qt::LeftButton)
   {
      QPoint  point=event->pos(); //QGraphicsView的坐标
      emit mouseDoubleClick(point);//发射信号
   }
   QGraphicsView::mouseDoubleClickEvent(event);
}
void QWGraphicsView::keyPressEvent(QKeyEvent *event)
{ //按键事件
   emit keyPress(event); //发射信号
   QGraphicsView::keyPressEvent(event);
}
3.主窗口设计

主窗口是一个从QMainWindow继承的窗口类MainWindow,采用可视化设计。设计的Action如原书的图8-22所示,利用这些Action创建两个工具栏,一个用于图形项的创建,一个用于图形项的各种操作。主窗口工作区的QWGraphicsView组件是从QGraphicsView组件升级而来的。

主窗口类MainWindow的定义的主要部分如下(省略了Action的槽函数的定义):

复制代码
class MainWindow : public QMainWindow
{   Q_OBJECT
private:
   static const int ItemId = 1; //图形项自定义数据的key
   static const int ItemDesciption = 2; //图形项自定义数据的key
   int seqNum=0; //用于图形项的编号,每个图形项有一个编号
   int frontZ=0;   //用于bring to front
   int backZ=0;  //用于bring to back
   QGraphicsScene  *scene;
   QLabel  *labViewCord;
   QLabel  *labSceneCord;
   QLabel  *labItemCord;
   QLabel  *labItemInfo;
public:
   explicit MainWindow(QWidget *parent = 0);
   ~MainWindow();
private slots:
   void   on_mouseMovePoint(QPoint point); //鼠标移动
   void   on_mouseClicked(QPoint point); //鼠标单击
   void   on_mouseDoubleClick(QPoint point); //鼠标双击
   void   on_keyPress(QKeyEvent *event); //按键
private:
   Ui::MainWindow *ui;
};

ItemId和ItemDesciption是用于定义图形项的自定义数据的键。

变量seqNum用于给每个图形项编号,每个图形项有一个唯一编号。

变量frontZ用于设置图形项的叠放顺序,数值越大,越在前面显示。

变量backZ用于设置图形项的叠放顺序,数值越小,越在后面显示。

定义了4个私有槽函数,用于响应QWGraphicsView的4个信号,实现鼠标移动、单击、双击、按键时的处理。

  • on_mouseMovePoint()槽函数响应绘图视图的mouseMovePoint()信号,在鼠标移动时显示鼠标光标处的视图坐标和场景坐标。

  • on_mouseClicked()槽函数响应绘图视图的mouseClicked()信号,在鼠标单击时,显示图形项的局部坐标和图形项存储的自定义信息,如图形项编号和描述信息。

  • on_mouseDoubleClick ()槽函数响应绘图视图的mouseDoubleClick()信号,在某个图形项上双击时,根据图形项的类型调用颜色对话框或字体对话框,设置填充颜色、线条颜色或字体。

  • on_keyPress()槽函数响应绘图视图的keyPress()信号,在选中某个图形项时,用按键实现缩放、删除、移动等操作。

MainWindow的构造函数代码如下:

复制代码
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)
{
   ui->setupUi(this);
   labViewCord=new QLabel("View 坐标:"); //创建状态栏标签
   labViewCord->setMinimumWidth(150);
   ui->statusBar->addWidget(labViewCord);
   labSceneCord=new QLabel("Scene 坐标:");
   labSceneCord->setMinimumWidth(150);
   ui->statusBar->addWidget(labSceneCord);
   labItemCord=new QLabel("Item 坐标:");
   labItemCord->setMinimumWidth(150);
   ui->statusBar->addWidget(labItemCord);
   labItemInfo=new QLabel("ItemInfo: ");
   labItemInfo->setMinimumWidth(200);
   ui->statusBar->addWidget(labItemInfo);

   scene=new QGraphicsScene(-300,-200,600,200); //创建QGraphicsScene
   ui->View->setScene(scene); //与view关联
   ui->View->setCursor(Qt::CrossCursor); //设置鼠标光标形状
   ui->View->setMouseTracking(true); 
   ui->View->setDragMode(QGraphicsView::RubberBandDrag);
   this->setCentralWidget(ui->View);

   QObject::connect(ui->View,SIGNAL(mouseMovePoint(QPoint)),
                 this, SLOT(on_mouseMovePoint(QPoint)));
   QObject::connect(ui->View,SIGNAL(mouseClicked(QPoint)),
                 this, SLOT(on_mouseClicked(QPoint)));
   QObject::connect(ui->View,SIGNAL(mouseDoubleClick(QPoint)),
                 this, SLOT(on_mouseDoubleClick(QPoint)));
   QObject::connect(ui->View,SIGNAL(keyPress(QKeyEvent*)),
                 this, SLOT(on_keyPress(QKeyEvent*)));
   qsrand(QTime::currentTime().second()); //随机数初始化
}

在构造函数里,首先是创建状态栏上的标签,然后创建场景并与视图关联,再将视图组件的4个信号与4个槽函数关联。

qsrand()用于随机数初始化,这里用当前时间的秒作为随机数种子。

4.图形项的创建

主窗口左侧工具栏上的按钮用于创建各种标准的图形项。创建矩形使用QGraphicsRectItem,创建椭圆和圆使用QGraphicsEllipseItem,创建三角形和梯形使用QGraphicsPolygonItem,创建直线使用QGraphicsLineItem,创建文字使用QGraphicsTextItem。还有一些其他的标准图形项类,程序里未全部涉及。

创建各图形项的代码基本类似,以创建椭圆的代码为例进行说明。

复制代码
void MainWindow::on_actItem_Ellipse_triggered()
{ //添加一个椭圆
   QGraphicsEllipseItem   *item=new QGraphicsEllipseItem(-50,-30,100,60);
   item->setFlags(QGraphicsItem::ItemIsMovable
               | QGraphicsItem::ItemIsSelectable
               | QGraphicsItem::ItemIsFocusable);
   item->setBrush(QBrush(Qt::blue)); //填充颜色
   item->setZValue(++frontZ); //用于叠放顺序
   item->setPos(-50+(qrand() % 100),-50+(qrand() % 100)); //初始位置
   item->setData(ItemId,++seqNum);  //自定义数据,ItemId键
   item->setData(ItemDesciption,"椭圆"); //自定义数据,ItemDesciption键

   scene->addItem(item);
   scene->clearSelection();
   item->setSelected(true);
}

椭圆图形类是QGraphicsEllipseItem。创建一个椭圆图形实例item,用setFlags()函数设置为可移动、可选择和可获取焦点;setBrush()用于设置填充色;setZValue()用于设置ZValue,这个参数控制叠放顺序,当有多个图形项叠加在一起时,ZValue值最大的显示在最前面;setPos()函数设置图形项在场景中的位置,这里使用了随机数函数qrand()。

setData()函数用于设置图形项的自定义参数,setData()函数原型为:

复制代码
void QGraphicsItem::setData(int key, const QVariant &value)

key是一个整数,value是QVariant类型,key和value是一个键值对。所以,使用setData()一次可以设置一个键值对,可以为一个图形项设置多个自定义键值对。程序里设置了两个自定义数据:

复制代码
item->setData(ItemId,++seqNum);  //自定义数据,ItemId键
item->setData(ItemDesciption,"椭圆"); //自定义数据,ItemDesciption键

ItemId是图形项的编号,ItemDesciption是图形项的描述,两个都是在MainWindow类中定义的静态变量。这样,每个图形项有一个唯一的编号,有一个文字描述。在窗口上单击某个图形项时,会提取这两个自定义数据在状态栏上显示。

工具栏上的按钮还可以创建其他一些图形项,代码与此基本类似,这里不再详细介绍。

5.图形项操作

主窗口的另一个工具栏上的按钮实现图形项的缩放、旋转、组合等操作。

  • 缩放

图形项的缩放使用QGraphicsItem的setScale()函数,参数大于1是放大,小于1是缩小。例如,下面是实现放大的程序。

复制代码
void MainWindow::on_actZoomIn_triggered()
{ //放大
   int cnt=scene->selectedItems().count();
   if (cnt==1)
   {
      QGraphicsItem*  item=scene->selectedItems().at(0);
      item->setScale(0.1+item->scale());
   }
   else
      ui->View->scale(1.1,1.1);
}

QGraphicsScene::selectedItems()返回场景中选中的图形项的列表,如果只有一个图形项被选中,就用QGraphicsItem的setScale()对图形项进行放大,在原来的缩放系数scale()基础上加0.1作为放大系数。如果选中的图形项个数大于1个,或没有图形项被选中,就用QGraphicsView的scale()函数对绘图视图进行放大。

  • 旋转

图形项的旋转使用QGraphicsItem::setRotation ()函数,参数为角度值,正值表示顺时针旋转,负值表示逆时针旋转。例如,下面是逆时针旋转的程序。

复制代码
void MainWindow::on_actRotateLeft_triggered()
{//逆时针旋转
   int cnt=scene->selectedItems().count();
   if (cnt==1)
   {
      QGraphicsItem*   item=scene->selectedItems().at(0);
      item->setRotation(-30+item->rotation());
   }
   else
      ui->View->rotate(-30);
}

如果有一个图形项被选中,就对图形项进行旋转,否则对绘图视图进行旋转。

  • 恢复坐标变换

缩放和旋转都是坐标变换,要取消所有变换恢复初始状态,调用QGraphicsItem或Qgraphics View的resetTransform()函数。下面是"恢复"按钮的响应代码。

复制代码
void MainWindow::on_actRestore_triggered()
{//取消所有变换
   int cnt=scene->selectedItems().count();
   if (cnt==1)
   {
      QGraphicsItem*   item=scene->selectedItems().at(0);
      item->resetTransform();
   }
   else
      ui->View->resetTransform();
}
  • 叠放顺序

QGraphicsItem的zValue()表示图形项在Z轴的值,若有多个图形项叠加在一起,zValue()值最大的显示在最前面,zValue()值最小的显示在最后面。用setZValue()函数可以设置这个属性值。下面是工具栏上的"前置"和"后置"按钮的代码。

复制代码
void MainWindow::on_actEdit_Front_triggered()
{ //bring to front,前置
   int cnt=scene->selectedItems().count();
   if (cnt>0)
   { //只处理选中的第1个图形项
      QGraphicsItem*   item=scene->selectedItems().at(0);
      item->setZValue(++frontZ);
   }
}

void MainWindow::on_actEdit_Back_triggered()
{//bring to back,后置
   int cnt=scene->selectedItems().count();
   if (cnt>0)
   {//只处理选中的第1个图形项
      QGraphicsItem*   item=scene->selectedItems().at(0);
      item->setZValue(--backZ);
   }
}

frontZ和backZ是在MainWindow类中定义的私有变量,专门用于存储叠放次序的编号。frontZ只增加,所以每增加一次都是最大值,设置该值的图形项就可以显示在最前面;backZ只减少,所以每减小一次都是最小值,设置该值的图形项就可以显示在最后面。

  • 图形项的组合

可以将多个图形项组合为一个图形项,当作一个整体进行操作,如同PowerPoint软件里图形组合功能一样。使用QGraphicsItemGroup类实现多个图形项的组合,QGraphicsItemGroup是QGraphicsItem的子类,所以实质上也是一个图形项。下面是工具栏上的"组合"按钮的响应代码:

复制代码
void MainWindow::on_actGroup_triggered()
{ //组合
   int cnt=scene->selectedItems().count();
   if (cnt>1)
   {
      QGraphicsItemGroup*  group =new QGraphicsItemGroup; //创建组合
      scene->addItem(group); //组合添加到场景中
       for (int i=0;i<cnt;i++)
       {
          QGraphicsItem*   item=scene->selectedItems().at(0);
          item->setSelected(false); //清除选择虚线框
          item->clearFocus();
          group->addToGroup(item); //添加到组合
       }
       group->setFlags(QGraphicsItem::ItemIsMovable
                    | QGraphicsItem::ItemIsSelectable
                    | QGraphicsItem::ItemIsFocusable);
       group->setZValue(++frontZ);
       scene->clearSelection();
       group->setSelected(true);
    }
}

当有多个图形项被选择时,创建一个QGraphicsItemGroup类型的实例group,并添加到场景中,然后将选中的图形项逐一添加到group中。这样创建的group就是场景中的一个图形项,可以对其进行缩放、旋转等操作。

一个组合对象也可以被打散,使用QGraphicsScene的destroyItemGroup()函数可以打散一个组合对象,这个函数打散组合,删除组合对象,但是不删除原来组合里的图形项。下面是工具栏上的"打散"按钮的响应代码:

复制代码
void MainWindow::on_actGroupBreak_triggered()
{ //break group,打散组合
   int cnt=scene->selectedItems().count();
   if (cnt==1)
   {
      QGraphicsItemGroup  *group;
      group=(QGraphicsItemGroup*)scene->selectedItems().at(0);
      scene->destroyItemGroup(group);
    }
}

这里假设在单击"打散"按钮时,选中的是一个组合对象,并没有做类型判断。

  • 图形项的删除

使用QGraphicsScene的removeItem()函数删除某个图形项,下面是工具栏上的"删除"按钮的响应代码。

复制代码
void MainWindow::on_actEdit_Delete_triggered()
{ //删除所有选中的图形项
   int cnt=scene->selectedItems().count();
   if (cnt>0)
   for (int i=0;i<cnt;i++)
   {
      QGraphicsItem*   item=scene->selectedItems().at(0);
      scene->removeItem(item); //删除图形项
   }
}
6.鼠标与键盘操作

MainWindow定义了4个槽函数与QWGraphicsView定义的4个信号进行关联,实现对鼠标移动、单击、双击和按键的响应。

  • 鼠标移动

鼠标在绘图视图上移动时,在状态栏显示光标处的视图坐标和场景坐标,on_mouseMovePoint()槽函数的代码如下:

复制代码
void MainWindow::on_mouseMovePoint(QPoint point)
{//鼠标移动事件,point是 GraphicsView的坐标,物理坐标
   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()));
}

参数point是鼠标光标在视图上的坐标,用QGraphicsView::mapToScene()函数可以将此坐标转换为场景中的坐标。

  • 鼠标单击

在视图上单击鼠标时,如果选中一个图形项,就会显示图形项的局部坐标,并提取其自定义信息进行显示,on_mouseClicked()槽函数代码如下:

复制代码
void MainWindow::on_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()));
      labItemInfo->setText(item->data(ItemDesciption).toString()
                 +", ItemId="+ item->data(ItemId).toString());
   }
}

首先将视图的坐标point转换为场景中的坐标pointScene,再利用QGraphicsScene的itemAt()函数获得光标处的图形项。利用QGraphicsItem的mapFromScene()函数将pointScene转换为图形项的局部坐标pointItem。

在创建图形项时,使用setData()函数设置了自定义数据的键值对,这里用data()函数提取图形项的自定义的数据ItemId和ItemDesciption,并进行显示。

  • 鼠标双击

当鼠标双击某个图形项时,希望根据图形项的类型,调用不同的对话框进行图形项的设置。例如,当图形项是矩形、圆形、梯形等有填充色的对象时,打开一个颜色选择对话框,设置其填充颜色;当图形项是直线时,设置其线条颜色;当图形项是文字时,打开一个字体对话框,设置其字体。下面是on_mouseDoubleClick()槽函数的代码。

复制代码
void MainWindow::on_mouseDoubleClick(QPoint point)
{//鼠标双击,调用相应的对话框,设置填充颜色、线条颜色或字体
   QPointF pointScene=ui->View->mapToScene(point); //转换到Scene坐标
   QGraphicsItem  *item=NULL;
   item=scene->itemAt(pointScene,ui->View->transform()); //光标下的图形项
   if (item == NULL) //没有图形项
      return;

   switch (item->type())  //图形项的类型
   { 
     case   QGraphicsRectItem::Type: //矩形框
     { //强制类型转换
      QGraphicsRectItem *theItem; 
      theItem =qgraphicsitem_cast<QGraphicsRectItem*>(item);
      setBrushColor(theItem);
      break;
     }
     case   QGraphicsEllipseItem::Type: //椭圆和圆都是此类型
     {
      QGraphicsEllipseItem *theItem;
      theItem =qgraphicsitem_cast<QGraphicsEllipseItem*>(item);
      setBrushColor(theItem);
      break;
     }
     case   QGraphicsPolygonItem::Type: //梯形和三角形
     {
      QGraphicsPolygonItem *theItem;
      theItem =qgraphicsitem_cast<QGraphicsPolygonItem*>(item);
      setBrushColor(theItem);
      break;
    }
     case   QGraphicsLineItem::Type: //直线,设置线条颜色
     {
      QGraphicsLineItem *theItem;
      theItem =qgraphicsitem_cast<QGraphicsLineItem*>(item);
      QPen   pen=theItem->pen();
      QColor  color=theItem->pen().color();
      color=QColorDialog::getColor(color,this,"选择线条颜色");
      if (color.isValid())
      {
         pen.setColor(color);
         theItem->setPen(pen);
      }
      break;
     }
     case  QGraphicsTextItem::Type: //文字,设置字体
     {
      QGraphicsTextItem *theItem; 
      theItem =qgraphicsitem_cast<QGraphicsTextItem*>(item);
      QFont font=theItem->font();
      bool ok=false;
      font=QFontDialog::getFont(&ok,font,this,"设置字体");
      if (ok)
         theItem->setFont(font);
      break;
     }
   }
}

双击鼠标时,获取光标下的图形项item,由于不知道具体是什么类型的图形项,item定义为QGraphicsItem,即所有图形项的父类。

QGraphicsItem的函数type()返回图形项的类型,每个具体的图形项类都需要重定义此函数,例如自定义一个图形项时,需要定义枚举常量Type和函数type,枚举常量Type的值必须大于UserType,示例代码如下:

复制代码
class CustomItem : public QGraphicsItem
  {
  public:
    enum { Type = UserType + 1 };
    int type() const
    {
       // 使得 qgraphicsitem_cast 可以使用这个图形项
       return Type;
    }
    ...
  };

根据item->type()值判断图形项的类型,若该值等于QGraphicsLineItem::Type,说明这个item是一个QGraphicsLineItem类型实例,可以设置其线条颜色。

设置线条颜色可以调用QGraphicsLineItem::setPen()函数,但是QGraphicsItem没有setPen()函数。所以,需要使用图形项的强制类型转换函数qgraphicsitem_cast()将item转换为QGraphicsLineItem类型的theItem,即:

复制代码
QGraphicsLineItem *theItem;
theItem =qgraphicsitem_cast<QGraphicsLineItem*>(item);

然后就调用QColorDialog::getColor()函数选择颜色,设置为theItem的线条颜色。

QGraphicsItem类同样没有setFont()和setBrush()函数,当选择的图形项是文字对象QGraphicsTextItem时,需要将其强制转换为QGraphicsTextItem类型的变量,调用字体选择对话框后,用QGraphicsTextItem::setFont()函数设置字体。

对于QGraphicsRectItem、QGraphicsEllipseItem或QGraphicsPolygonItem,可以调用这3个类的setBrush()函数设置填充颜色,这里用一个函数setBrushColor()为3种不同类的对象进行填充色的设置。setBrushColor()并不是使用不同类型参数的同名重载函数,而是一个在mainwindow.cpp文件中定义的一个独立的模板函数。

复制代码
template<class T> void setBrushColor(T *item)
{//函数模板
   QColor   color=item->brush().color();
   color=QColorDialog::getColor(color,NULL,"选择填充颜色");
   if (color.isValid())
      item->setBrush(QBrush(color));
}

编译器会自动根据调用setBrushColor()的参数的类型生成3个不同参数类型的函数,减少了代码的冗余性。

  • 按键操作

在选中一个图形项之后,可以通过键盘按键实现一些快捷操作,例如缩放、旋转、移动等。on_keyPress()槽函数实现对按键的响应,其代码如下:

复制代码
void MainWindow::on_keyPress(QKeyEvent *event)
{ //按键事件
   if (scene->selectedItems().count()!=1)
      return; //没有选中的图形项,或选中的多于1个

   QGraphicsItem   *item=scene->selectedItems().at(0);
   if (event->key()==Qt::Key_Delete)//删除
      scene->removeItem(item);
   else if (event->key()==Qt::Key_Space) //顺时针旋转90度
      item->setRotation(90+item->rotation());
   else if (event->key()==Qt::Key_PageUp)//放大
      item->setScale(0.1+item->scale());
   else if (event->key()==Qt::Key_PageDown) //缩小
      item->setScale(-0.1+item->scale());
   else if (event->key()==Qt::Key_Left)  //左移
      item->setX(-1+item->x());
   else if (event->key()==Qt::Key_Right) //右移
      item->setX(1+item->x());
   else if (event->key()==Qt::Key_Up) //上移
      item->setY(-1+item->y());
   else if (event->key()==Qt::Key_Down) //下移
      item->setY(1+item->y());
}

这段代码限定只有一个图形项被选中时才可以执行键盘操作。

相关推荐
xieliyu.1 小时前
JVM 垃圾回收机制详解:从标记过程、回收算法到垃圾收集器
java·jvm·笔记·java-ee
xieliyu.1 小时前
前端基础:常见CSS选择器用法
前端·css·笔记·vscode
周玉奎先生1 小时前
河南少商新材料CGM灌浆料:扎根河南工程现场的水泥基灌浆材料
经验分享·笔记·零售
西西弗Sisyphus1 小时前
Qt 实现一个 水波进度球
c++·qt·c
动词ing1 小时前
【学习笔记】动态规划+背包问题
笔记·学习
智者知已应修善业12 小时前
【4060BD_5V应用电路图】2022-12-24
驱动开发·经验分享·笔记·硬件架构·硬件工程
小李不想当小白15 小时前
DMA直接存储器存取(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·分享
RAN114267600815 小时前
1.3第一个程序的介绍
开发语言·qt
2601_9653842617 小时前
口腔黏膜清洁的工程化路径:从表面形貌表征到清洁策略匹配
经验分享·笔记