zoukankan      html  css  js  c++  java
  • 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();               // 绘制完成
    
  • 相关阅读:
    SQL练习(Navicat premium)
    jmeter Thread Name 后面数字1-1 1-2的意思
    jmeter用Stepping Thread Group 递增并发数
    打开文件提示“已被macos使用“,不用每次都设置一遍
    查看访问网页的接口
    mac常用快捷键
    mac修改hosts保存报错
    文本编辑器
    excel时间戳转化为日期
    jmeter察看结果树左侧的请求名称显示为空 开始时间显示1970-01-01
  • 原文地址:https://www.cnblogs.com/nsfoxer/p/14391937.html
Copyright © 2011-2022 走看看