12_绘制系统.md

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);
}

image-20210208225335004

​ 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::LinearGradientPatternQt::RadialGradientPattern或者Qt::ConicalGradientPattern之一时才有效。渐变可以由QGradient对象表示。Qt 提供了三种渐变:QLinearGradientQConicalGradientQRadialGradient,它们都是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()

下面是画笔样式的示例:

画笔样式 Pen 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()

笔帽定义了画笔末端的样式,例如:

笔帽样式 Cap Style

他们之间的区别是,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);

}

image-20210211001528798

painter.setRenderHint(QPainter::Antialiasing, true);打开反走样。

QPainter是一个状态机,因此,只要这里我们打开了它,之后所有的代码都会是反走样绘制的了。为了提高效率,一般的图形绘制系统,如 Java2D、OpenGL 之类都是默认不进行反走样的。

渐变

​ 渐变是绘图中很常见的一种功能,简单来说就是可以把几种颜色混合在一起,让它们能够自然地过渡,而不是一下子变成另一种颜色。渐变的算法比较复杂,写得不好的话效率会很低。

线性渐变 QLinearGradient

QLinearGradient 示例

辐射渐变 QTadialGradient

QRadialGradient 示例

角度渐变 QConicalGradient

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)

Qt5 中,QGLPixelBuffer已经被废弃。

QGLWidgetQGLFramebufferObject,顾名思义,就是关于 OpenGL 的相关类。在 Qt 中,我们可以方便地结合 OpenGL 进行绘制。

QPixmap

QPixmap专门为图像在屏幕上的显示做了优化;QBitmapQPixmap的一个子类,它的色深限定为1,你可以使用QPixmapisQBitmap()函数来确定这个QPixmap是不是一个QBitmap

QPixmap也可以接受一个字符串作为一个文件的路径来显示这个文件,比如你想在程序之中打开 png、jpeg 之类的文件,就可以使用QPixmap。使用QPainter::drawPixmap()函数可以把这个文件绘制到一个QLabelQPushButton或者其他的设备上面。

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);
}

image-20210211113953276

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();               // 绘制完成
原文地址:https://www.cnblogs.com/nsfoxer/p/14391937.html