一、QPainter:绘图对象
Qt 的绘图系统允许使用相同的 API 在屏幕和其它打印设备上进行绘制。整个绘图系统基于 QPainter,QPainterDevice 和 QPaintEngine 三个类。
QPainter 用来执行绘制的操作(相当于画家);
QPaintDevice 是一个绘图设备,允许 QPainter 在其上面进行绘制,也就是 QPainter 工作的空间(相当于画板);
QPaintEngine 提供了 QPainter 在不同的设备上进行绘制的统一的接口(相当于一种协议)。
QPaintEngine 类应用于 QPainter 和 QPaintDevice 之间,通常对开发人员是透明的。除非你需要自定义一个设备,否则你是不需要关心 QPaintEngine 这个类的。我们可以把 QPainter 理解成画家;把 QPaintDevice 理解成画板,比如纸张、屏幕等都可以作为画板;而对于纸张、屏幕而言,肯定要使用不同的画笔绘制,为了统一使用一种画笔,Qt 设计了 QPaintEngine 类,这个类让不同的纸张、屏幕都能使用一种画笔。
下图给出了这三个类之间的层次结构:
上面的示意图告诉我们,Qt 的绘图系统实际上是,使用 QPainter 在 QPainterDevice 上进行绘制,它们之间使用QPaintEngine 进行通讯(也就是翻译 QPainter 的指令)。
二、画图测试:
新建一个基于 QWidget 的项目,在头文件中重写 paintEvent() 事件方法:
protected:void paintEvent(QPaintEvent *); // 重写绘图事件
然后在源文件中实现 paintEvent() 事件方法,paintEvent() 就是当窗口发生重绘时触发的事件方法,那么我们需要画图的代码就应该写在 paintEvent() 事件方法中。
1、画一条直线:
// 窗体发生重绘时触发的事件
void Widget::paintEvent(QPaintEvent *)
{// 创建一个局部变量 QPainter 对象,即每次运行 paintEvent() 方法的时候都会重新创建该对象。// QPainter 接收一个 QPaintDevice 指针作为参数,QPaintDevice 表示绘图设备,// 即指定我们在哪个设备上进行绘图。this 表示当前主窗口对象,即在当前窗口上进行绘图。QPainter painter(this);// 画一条直线:参数为两个点(两点决定一条直线)painter.drawLine(QPoint(10, 10), QPoint(100, 100));
}
2、画一个矩形,并指定矩形边框的颜色:
void Widget::paintEvent(QPaintEvent *)
{QPainter painter(this);// 设置一个红色的画笔painter.setPen(Qt::red);// 画一个矩形painter.drawRect(QRect(10, 10, 100, 100));
}
3、画一个椭圆形,指定边框的颜色和粗细,并指定图形的填充颜色:
void Widget::paintEvent(QPaintEvent *)
{QPainter painter(this);// 设置一个绿色的画笔,并指定画笔的宽度为 8painter.setPen(QPen(Qt::green, 8));// 设置一个蓝色的画刷,用来填充绘制的图形painter.setBrush(Qt::blue);// 画一个椭圆:指定圆心,宽和高painter.drawEllipse(QPoint(100, 100), 80, 50);
}
三、QPaintDevice:绘图设备
绘图设备是指继承 QPainterDevice 的子类。
Qt一共提供了四个这样的类,分别是 QPixmap、QBitmap、QImage 和 QPicture。
-
QPixmap:专门为图像在屏幕上的显示做了优化;
-
QBitmap:是 QPixmap 的子类,它的色深限定为 1,可以使用 QPixmap 的 isQBitmap() 方法来确定这个 QPixmap 是不是一个 QBitmap;
-
QImage:专门为图像的像素级访问做了优化;
-
QPicture:可以记录和重现 QPainter 的各条指令;
-
1、QPixmap:可以接收一个字符串作为一个文件的路径来显示这个文件,使用 QPinter 的 drawPixmap() 方法可以把这个文件绘制到一个指定的绘图设备上。QPixmap 是针对屏幕进行特殊优化的,因此,它与实际的底层显示设备息息相关。注意,这里说的显示设备并不是硬件,而是操作系统提供的原生的绘图引擎。所以,在不同的操作系统平台下,QPixmap 的显示可能会有所差别。
void Widget::paintEvent(QPaintEvent *)
{// 创建一个绘图对象,指定绘图设备为当前主窗口QPainter painter(this);// 绘制一个图像:参数为显示图像的开始 x、y 坐标,和要显示的图像// 显示的是图像的原始尺寸大小painter.drawPixmap(10, 10, QPixmap(":/Image/Luffy.png"));
}
还可以自己指定显示图像的大小:
void Widget::paintEvent(QPaintEvent *)
{// 创建一个绘图对象,指定绘图设备为当前主窗口QPainter painter(this);// 绘制一个图像:参数为显示图像的开始 x、y 坐标,和显示图像的宽度和高度,以及要显示的图像painter.drawPixmap(10, 10, 100, 100, QPixmap(":/Image/Luffy.png"));
}
还可以在指定的控件上绘制图像,我最开始的做法如下:
void Widget::paintEvent(QPaintEvent *)
{// 创建一个 QLabel 对象QLabel *label = new QLabel(this);label->setGeometry(10, 10, 200, 200);// 创建一个绘图对象,指定绘图设备为 QLabelQPainter painter(label);// 绘制一个图像painter.drawPixmap(0, 0, 200, 200, QPixmap(":/Image/Luffy.png"));
}
程序运行的时候报错:
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
后来测试发现,好像是想要在哪个控件上绘制,就要在哪个控件类中重写 paintEvent() 方法才行。比如想要在 QLabel 上绘图,就需要新建一个类继承于 QLabel,然后重写 paintEvent() 方法,如下所示:
mylabel.h:
#include <QLabel>class MyLabel : public QLabel
{Q_OBJECT
public:explicit MyLabel(QWidget *parent = 0);protected:void paintEvent(QPaintEvent *); // 重写绘图事件
};
mylabel.cpp:
#include "mylabel.h"
#include <QPainter>MyLabel::MyLabel(QWidget *parent) : QLabel(parent)
{
}// 在控件发生重绘时触发的事件
void MyLabel::paintEvent(QPaintEvent *)
{// 创建一个绘图对象,指定绘图设备为 QLabelQPainter painter(this);// 绘制一个图像painter.drawPixmap(0, 0, this->width(), this->height(), QPixmap(":/Image/Luffy.png"));
}
在主窗口程序中调用 MyLabel:
Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget)
{ui->setupUi(this);// 调用 MylabelMyLabel *label = new MyLabel(this);label->setGeometry(10, 10, 100, 100);
}
2、QBitmap:继承自 QPixmap,因此具有 QPixmap 的所有特性。QBitmap 的色深始终为 1,色深这个概念来自于计算机图形学,是指用于表现颜色的二进制位数。我们知道,计算机中的数据都是使用二进制来表示的。为了表示一种颜色,我们也会使用二进制。比如我们要表示 8 种颜色,需要用 3 个二进制位,这时我们就说色深是 3。因此,所谓色深为 1,也就是使用 1 个二进制表示颜色。一个二进制数只有两种状态,0 和 1,因此他所表示的颜色只有两种,黑和白。所以,QBitmap 实际上是只有黑白两色的图像数据。
由于 QBitmap 色深小,因此只占用很少的内存空间,所以适合做光标文件和笔刷。
下面我们来看同一个图像文件在 QPixmap 和 QBitmap 下的不同表现:
void Widget::paintEvent(QPaintEvent *)
{// 创建两个 QPixmap 图像QPixmap pixmap(":/Image/butterfly.png");QPixmap pixmap1(":/Image/butterfly1.png");// 创建两个 QBitmap 图像QBitmap bitmap(":/Image/butterfly.png");QBitmap bitmap1(":/Image/butterfly1.png");QPainter painter(this);// 绘图painter.drawPixmap(0, 0, pixmap);painter.drawPixmap(200, 0, pixmap1);painter.drawPixmap(0, 130, bitmap);painter.drawPixmap(200, 130, bitmap1);
}
显示效果如下:
我们使用了两张图片,butterfly.png 具有透明色的背景,butterfly1.png 是没有透明色的纯白背景。使用 QPixmap 加载出的图像是正常效果;但是使用 QBitmap 加载,可以看到 butterfly.png 的透明色背景变成了黑色,而 butterfly1.png 的纯白色背景反而消失了,变成了透明色。至于图像的其他颜色,则是使用点的疏密程度来表现的。
3、QImage: QPixmap 使用底层平台绘制系统进行绘制,无法提供像素级别的操作;而 QImage 则是使用独立于硬件的绘制系统,实际上是自己绘制自己,因此提供了像素级别的操作,并且能够在不同系统之上提供一个一致的显示形式。
我们声明一个 QImage 对象,大小是 200x200,颜色模式是 RGB32,即使用 32 位数值表示一个颜色的 RGB 值,也就是说每种颜色使用 8 位。然后我们为每个像素进行颜色赋值,从而构成了这个图像。
void Widget::paintEvent(QPaintEvent *)
{QPainter painter(this);// 创建一个 QImage 对象QImage image(200, 200, QImage::Format_RGB32);QRgb value;// 将图片背景填充为绿色image.fill(Qt::green);// 改变指定区域的像素点的值for (int i = 50; i < 100; i++){for(int j = 50; j < 100; j++){value = qRgb(255, 0, 0); // 红色image.setPixel(i, j, value); // 设置每个像素点的值}}// 将图像绘制到窗口中painter.drawImage(10, 10, image);
}
QImage 与 QPixmap 的区别:
-
QPixmap 主要用于绘图,针对屏幕显示而最佳化设计,QImage 主要是为图像 I/O、图片访问和像素修改而设计的。
-
QPixmap 依赖于所在平台的绘图引擎,顾例如反锯齿等一些效果在不同的平台上可能会有不同的效果,QImage 使用 Qt 自身的绘图引擎,可在不同平台上具有相同的显示效果。
-
由于 QImage 是独立于硬件的,也是一种 QPaintDevie,因此我们可以在另一个线程中对其进行绘制,而不需要在 GUI 线程中处理,使用这一方式可以很大程序提高 UI 响应速度。
-
QImage 可通过 setPixel() 和 pixel() 等方法直接存取指定的像素。
QImage 和 QPixmap 的相互转换:
// 将 QImage 转换成 QPixmap:使用 QPixmap 的静态成员函数 fromImage();QPixmap pixmap = QPixmap::fromImage(image);// 将 QPixmap 转换成 QImage:使用 QPixmap 类的成员函数 toImage();QImage img = pixmap.toImage();
4、QPicture:这是一个可以记录和重现 QPainter 命令的绘图设备。QPicture 将 QPainter 的命令序列化到一个 IO 设备,保存为一个平台独立的文件格式。这种格式有时候会是 “元文件(mate-files)”。Qt 的这种格式是二进制的,不同于某些本地的元文件,Qt 的 pictures 没有内容上的限制,只要是能够被 QPainter 绘制的元素,不论是字体,还是 pixmap,或者是变换,都可以保存在一个 picture 文件中。
QPicture 是平台无关的,因此他可以使用在多种设备之上,比如 svg、pdf、ps、打印机或者屏幕。QPicture 使用系统的分辨率,并且可以调整 QPainter 来消除不同设备之间的显示差异。
如果我们要记录下 QPainter 的命令,首先要使用 QPainter::begin() 函数,将 QPicture 实例作为参数传递进去,以便告诉系统开始记录,记录完毕后使用 QPainter::end() 函数终止。
void Widget::paintEvent(QPaintEvent *)
{QPicture picture;QPainter painter;// 将图像绘制到 QPicture 中,并保存到文件// 开始记录painter.begin(&picture);// 开始绘图painter.drawEllipse(10, 10, 100, 50); // 画一个椭圆painter.fillRect(10, 70, 100, 50, Qt::red); // 填充一个矩形// 结束记录painter.end();// 保存文件picture.save("D://drawing.pic");// 加载保存的绘图动作picture.load("D://drawing.pic");// 将保存的绘图重新绘制到设备上 painter.begin(this);painter.drawPicture(10, 10, picture);painter.end();
}
四、手动更新窗口
如下图所示:实现一个当按钮被点击时,图片向右移动的功能,使用绘图事件实现。
widget.h:
class Widget : public QWidget
{Q_OBJECTpublic:explicit Widget(QWidget *parent = 0);~Widget();private:Ui::Widget *ui;// 声明一个全局变量int x = 0;protected:void paintEvent(QPaintEvent *); // 重写绘图事件private slots:void on_btnMove_clicked();
};
widget.cpp:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QPainter>Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget)
{ui->setupUi(this);
}Widget::~Widget()
{delete ui;
}void Widget::paintEvent(QPaintEvent *)
{// 在主窗口上画一个图片QPainter painter(this); painter.drawPixmap(x, 100, QPixmap(":/Image/LuffyQ.png"));
}// 点击一下移动按钮,重绘窗口,将图片向右移动
void Widget::on_btnMove_clicked()
{// 绘制图片的 x 坐标增加,向右移动x += 20;// 如果图片移动超出窗口宽度,则恢复 x 坐标为初始值if (x > this->width()){x = 0;}// 重绘窗口,整个窗口都会重绘
// this->update();// 重绘指定的区域this->update(x - 20, 100, 150, 150);
}