Qt绘制系统
绘制系统
Qt 的绘图系统允许使用相同的 API 在屏幕和其它打印设备上进行绘制。整个绘图系统基于 QPainter,QPainterDevice 和 QPaintEngine 三个类。
- QPainter: 来执行绘制操作
- QPainterDevice: 是一个二维空间的抽象,这个二维空间允许QPainter 在其上面进行绘制,也就是 QPainter 工作的空间。
- QPainterEngine: 提供了画笔(QPainter)在不同的设备上进行绘制的统一的接口。QPaintEngine 类应用于 QPainter和 QPaintDevice 之间,通常对开发人员是透明的。除非你需要自定义一个设备,否则你是不需要关心 QPaintEngine 这个类的。
#include "paintedwidget.h"
#include <QPainter>
#include <QPaintEvent>
PaintedWidget::PaintedWidget(QWidget *parent):QWidget(parent)
{
setFixedSize(600, 400);
setWindowTitle("Paint");
}
void PaintedWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.drawLine(80, 100, 650, 500);
painter.setPen(Qt::red);
painter.drawRect(10, 10, 100, 400);
painter.setPen(QPen(Qt::green, 5));
painter.setBrush(Qt::blue);
painter.drawEllipse(50, 150, 400, 200);
}
QPainter 接收一个 QPaintDevice 指针作为参数。QPaintDevice 有很多子类,比如 QImage,以及 QWidget。注意回忆一下,QPaintDevice 可以理解成要在哪里去绘制,而现在我们希望画在这个组件,因此传入的是 this 指针。
#include "paintedwidget.h"
#include <stdio.h>
#include <QPainter>
#include <QPaintEvent>
#include <QMouseEvent>
#include <QWheelEvent>
PaintedWidget::PaintedWidget(QWidget *parent):QWidget(parent)
{
// setFixedSize(600, 400);
setWindowTitle("Paint");
}
void PaintedWidget::paintEvent(QPaintEvent *) {
QPainter *painter = new QPainter(this);
painter->drawEllipse(x-r/2, y-r/2, r, r);
delete painter;
}
void PaintedWidget::wheelEvent(QWheelEvent *event) {
QPoint degree = event->angleDelta();
if (degree.y() > 0) {
r++;
} else {
r--;
}
if (r <= 0) {
r = 0;
}
x = event->position().x();
y = event->position().y();
this->update();
}
画刷和画笔
- 画刷: QBrush,通常用来进行填充
- 画笔: QPush,通常用来绘制轮廓
QBrush 定义了 QPainter 的填充模式,具有样式、颜色、渐变以及纹理等属性。
画刷
style()
画刷的style()
定义了填充的样式,使用Qt::BrushStyle
枚举,默认值是Qt::NoBrush
,也就是不进行任何填充。我们可以从下面的图示中看到各种填充样式的区别:
color()
画刷的 color()定义了填充模式的颜色。这个颜色可以是 Qt 预定义的颜色常量,也就是Qt::GlobalColor,也可以是任意 QColor 对象。
gradient()
画刷的gradient()
定义了渐变填充。这个属性只有在样式是Qt::LinearGradientPattern
、Qt::RadialGradientPattern
或者Qt::ConicalGradientPattern
之一时才有效。渐变可以由QGradient
对象表示。Qt 提供了三种渐变:QLinearGradient
、QConicalGradient
和QRadialGradient
,它们都是QGradient
的子类。我们可以使用如下代码片段来定义一个渐变的画刷:
QRadialGradient gradient(50, 50, 50, 50, 50);
gradient.setColorAt(0, QColor::fromRgbF(0, 1, 0, 1));
gradient.setColorAt(1, QColor::fromRgbF(0, 0, 0, 0));
QBrush brush(gradient);
当画刷样式是 Qt::TexturePattern
时,texture()
定义了用于填充的纹理。注意,即使你没有设置样式为Qt::TexturePattern
,当你调用setTexture()
函数时,QBrush
会自动将style()
设置为Qt::TexturePattern
。
画笔
画笔具有样式、宽度、画刷、笔帽样式和连接样式等属性。
style()
:定义线的样式capStyle()
:定义线的末端样式joinStyle()
:定义两条线连接的方式width()
:定义画笔的宽度。假设你设置 width 为 0,QPainter
依然会绘制出一条线,而这个线的宽度为 1 像素。也就是说,画笔宽度通常至少是 1 像素。
style()
下面是画笔样式的示例:
你也可以使用setDashPattern()
函数自定义样式,例如如下代码片段:
QPen pen;
QVector<qreal> dashes;
qreal space = 4;
dashes << 1 << space << 3 << space << 9 << space << 27 << space << 9 << space;
pen.setDashPattern(dashes);
capStyle()
笔帽定义了画笔末端的样式,例如:
他们之间的区别是,Qt::SquareCap
是一种包含了最后一个点的方形端点,使用半个线宽覆盖;Qt::FlatCap
不包含最后一个点;Qt::RoundCap
是包含最后一个点的圆形端点。具体可以参考下面的示例(出自《C++ GUI Programming with Qt 4, 2nd Edition》):
joinStyle()
连接样式定义了两条线连接时的样式,例如:
bevel
:斜角, 斜角规, 倾斜, 斜面miter
:僧帽, 主教冠, 斜接, 斜榫
同样,可以参考下面图示来理解这几种连接样式的细节(出自《C++ GUI Programming with Qt 4, 2nd Edition》):
注意,我们前面说了,QPainter
也是一个状态机,这里我们所说的这些属性都是处于这个状态机之中的,因此,我们应该记得是否要将其保存下来或者是重新构建。
反走样
在光栅图形显示器上绘制非水平、非垂直的直线或多边形边界时,或多或少会呈现锯齿状外观。这是因为直线和多边形的边界是连续的,而光栅则是由离散的点组成。在光栅显示设备上表现直线、多边形等,必须在离散位置采样。由于采样不充分重建后造成的信息失真,就叫走样;用于减少或消除这种效果的技术,就称为反走样。
反走样是图形学中的重要概念,用以防止通常所说的“锯齿”现象的出现。很多系统的绘图 API 里面都内置了有关反走样的算法,不过由于性能问题,默认一般是关闭的,Qt 也不例外。下面我们来看看代码:
#include "mainwindow.h"
#include <QPainter>
#include <QPaintEvent>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setFixedSize(600, 400);
}
MainWindow::~MainWindow()
{
}
void MainWindow::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setPen(QPen(Qt::black, 5, Qt::DashDotDotLine, Qt::RoundCap));
painter.setBrush(Qt::yellow);
painter.drawEllipse(50, 150, 200, 150);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.drawEllipse(300, 150, 200, 150);
}
painter.setRenderHint(QPainter::Antialiasing, true);
打开反走样。
QPainter
是一个状态机,因此,只要这里我们打开了它,之后所有的代码都会是反走样绘制的了。为了提高效率,一般的图形绘制系统,如 Java2D、OpenGL 之类都是默认不进行反走样的。
渐变
渐变是绘图中很常见的一种功能,简单来说就是可以把几种颜色混合在一起,让它们能够自然地过渡,而不是一下子变成另一种颜色。渐变的算法比较复杂,写得不好的话效率会很低。
线性渐变 QLinearGradient
辐射渐变 QTadialGradient
角度渐变 QConicalGradient
示例
void paintEvent(QPaintEvent *){
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
QLinearGradient linearGradient(60, 50, 200, 200);
linearGradient.setColorAt(0.2, Qt::white);
linearGradient.setColorAt(0.6, Qt::green);
linearGradient.setColorAt(1.0, Qt::black);
painter.setBrush(QBrush(linearGradient));
painter.drawEllipse(50, 50, 200, 150);
}
QLinearGradient
也就是线性渐变,其构造函数有四个参数,分别是 x1,y1,x2,y2,即渐变的起始点和终止点。在这里,我们从 (60, 50) 点开始渐变,到 (200, 200) 点止。
void ColorWheel::paintEvent(QPaintEvent *){
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const int r = 150;
QConicalGradient conicalGradient(0, 0, 0);
conicalGradient.setColorAt(0.0, Qt::red);
conicalGradient.setColorAt(60.0/360.0, Qt::yellow);
conicalGradient.setColorAt(120.0/360.0, Qt::green);
conicalGradient.setColorAt(180.0/360.0, Qt::cyan);
conicalGradient.setColorAt(240.0/360.0, Qt::blue);
conicalGradient.setColorAt(300.0/360.0, Qt::magenta);
conicalGradient.setColorAt(1.0, Qt::red);
painter.translate(r, r);
QBrush brush(conicalGradient);
painter.setPen(Qt::NoPen);
painter.setBrush(brush);
painter.drawEllipse(QPoint(0, 0), r, r);
}
QConicalGradient::QConicalGradient ( qreal cx, qreal cy, qreal angle )
前两个参数 cx 和 cy 组成角度渐变的中心点,第三个参数是渐变的起始角度。在我们的例子中,我们将渐变中心点设置为 (0, 0),起始角度为 0。类似线性渐变,角度渐变的setColorAt()
函数同样接受两个参数,第一个是角度比例,第二个是颜色。
坐标系统
坐标系统,也就是QPaintDevice
上面的坐标。默认坐标系统位于设备的左上角,也就是坐标原点 (0, 0)。x 轴方向向右;y 轴方向向下。在基于像素的设备上(比如显示器),坐标的默认单位是像素,在打印机上则是点(1/72 英寸)。
将QPainter
的逻辑坐标与QPaintDevice
的物理坐标进行映射的工作,是由QPainter
的变换矩阵(transformation matrix)、视口(viewport)和窗口(window)完成的。对图形的操作,底层的数学都是进行的矩阵变换、相乘等运算。
QPainter
是一个状态机。那么,有时我想保存下当前的状态:当我临时绘制某些图像时,就可能想这么做。当然,我们有最原始的办法:将可能改变的状态,比如画笔颜色、粗细等,在临时绘制结束之后再全部恢复。对此,QPainter
提供了内置的函数:save()
和restore()
。save()
就是保存下当前状态;restore()
则恢复上一次保存的结果。这两个函数必须成对出现:QPainter
使用栈来保存数据,每一次save()
,将当前状态压入栈顶,restore()
则弹出栈顶进行恢复。
Qt 的坐标分为逻辑坐标和物理坐标。在我们绘制时,提供给QPainter
的都是逻辑坐标。之前我们看到的坐标变换,也是针对逻辑坐标的。所谓物理坐标,就是绘制底层QPaintDevice
的坐标。单单只有逻辑坐标,我们是不能在设备上进行绘制的。要想在设备上绘制,必须提供设备认识的物理坐标。Qt 使用 viewport-window 机制将我们提供的逻辑坐标转换成绘制设备使用的物理坐标,方法是,在逻辑坐标和物理坐标之间提供一层“窗口”坐标。视口是由任意矩形指定的物理坐标;窗口则是该矩形的逻辑坐标表示。默认情况下,物理坐标和逻辑坐标是一致的,都等于设备矩形。
视口坐标(也就是物理坐标)和窗口坐标是一个简单的线性变换。比如一个 400×400 的窗口,我们添加如下代码:
void PaintDemo::paintEvent(QPaintEvent *){
QPainter painter(this);
painter.setWindow(0, 0, 200, 200);
painter.fillRect(0, 0, 200, 200, Qt::red);
}
我们将窗口矩形设置为左上角坐标为 (0, 0),长和宽都是 200px。此时,坐标原点不变,还是左上角,但是,对于原来的 (400, 400) 点,新的窗口坐标是 (200, 200)。我们可以理解成,逻辑坐标被“重新分配”。这有点类似于translate()
,但是,translate()
函数只是简单地将坐标原点重新设置,而setWindow()
则是将整个坐标系进行了修改。这段代码的运行结果是将整个窗口进行了填充。
下面我们再来理解下视口的含义。还是以一段代码为例:
void PaintDemo::paintEvent(QPaintEvent *){
QPainter painter(this);
painter.setViewport(0, 0, 200, 200);
painter.fillRect(0, 0, 200, 200, Qt::red);
}
这段代码和前面一样,只是把setWindow()
换成了setViewport()
。前面我们说过,window 代表窗口坐标,viewport 代表物理坐标。也就是说,我们将物理坐标区域定义为左上角位于 (0, 0),长高都是 200px 的矩形。然后还是绘制和上面一样的矩形。如果你认为运行结果是 1/4 窗口被填充,那就错了。实际是只有 1/16 的窗口被填充。这是由于,我们修改了物理坐标,但是没有修改相应的窗口坐标。默认的逻辑坐标范围是左上角坐标为 (0, 0),长宽都是 400px 的矩形。当我们将物理坐标修改为左上角位于 (0, 0),长高都是 200px 的矩形时,窗口坐标范围不变,也就是说,我们将物理宽 200px 映射成窗口宽 400px,物理高 200px 映射成窗口高 400px,所以,原始点 (200, 200) 的坐标变成了 ((0 + 200 200 / 400), (0 + 200 200 / 400)) = (100, 100)。
绘制设备
绘图设备是继承QPainterDevice
的类。QPaintDevice
就是能够进行绘制的类,也就是说,QPainter
可以在任何QPaintDevice
的子类上进行绘制。现在,Qt 提供了若干这样的类:
Qt5 中,QGLPixelBuffer
已经被废弃。
QGLWidget
和QGLFramebufferObject
,顾名思义,就是关于 OpenGL 的相关类。在 Qt 中,我们可以方便地结合 OpenGL 进行绘制。
QPixmap
QPixmap
专门为图像在屏幕上的显示做了优化;QBitmap
是QPixmap
的一个子类,它的色深限定为1,你可以使用QPixmap
的isQBitmap()
函数来确定这个QPixmap
是不是一个QBitmap
。
QPixmap
也可以接受一个字符串作为一个文件的路径来显示这个文件,比如你想在程序之中打开 png、jpeg 之类的文件,就可以使用QPixmap
。使用QPainter::drawPixmap()
函数可以把这个文件绘制到一个QLabel
、QPushButton
或者其他的设备上面。
QPixmap
提供了静态的grabWidget()
和grabWindow()
函数,用于将自身图像绘制到目标上。同时,在使用QPixmap
时,你可以直接使用传值的形式,不需要传指针,因为QPixmap
提供了“隐式数据共享”。
void MainWindow::paintEvent(QPaintEvent *) {
QPainter painter(this);
QPixmap pixmap(":/images/qt.png");
QBitmap bitmap(":/images/qt.png");
painter.drawPixmap(10, 10, 250, 125, pixmap);
painter.drawPixmap(270, 10, 250, 125, bitmap);
}
QImage
QPixmap
使用底层平台的绘制系统进行绘制,无法提供像素级别的操作,而QImage
则是使用独立于硬件的绘制系统,实际上是自己绘制自己,因此提供了像素级别的操作,并且能够在不同系统之上提供一个一致的显示形式。
QPicture
QPicture
是平台无关的,因此它可以使用在多种设备之上,比如 svg、pdf、ps、打印机或者屏幕。
Picture picture;
QPainter painter;
painter.begin(&picture); // 在 picture 进行绘制
painter.drawEllipse(10, 20, 80, 70); // 绘制一个椭圆
painter.end(); // 绘制完成
picture.save("drawing.pic"); // 保存 picture
如果我们要重现命令,首先要使用 QPicture::load() 函数进行装载:
QPicture picture;
picture.load("drawing.pic"); // 加载
pictureQPainter painter;
painter.begin(&myImage); // 在 myImage 上开始绘制
painter.drawPicture(0, 0, picture); // 在 (0, 0) 点开始绘制
picturepainter.end(); // 绘制完成