QT-第六节 绘图和绘图设备、文件系统

绘图和绘图设备

  1. Qt 的绘图系统允许使用相同的 API 在屏幕和其它打印设备上进行绘制。整个绘图系统基于QPainter,QPainterDevice和QPaintEngine三个类。
    1. QPainter用来执行绘制的操作;
    2. QPaintDevice是一个二维空间的抽象,这个二维空间允许QPainter在其上面进行绘制,也就是QPainter工作的空间;
    3. QPaintEngine提供了画笔(QPainter)在不同的设备上进行绘制的统一的接口。QPaintEngine类应用于QPainter和QPaintDevice之间,通常对开发人员是透明的。除非你需要自定义一个设备,否则你是不需要关心QPaintEngine这个类的。
  2. 我们可以把QPainter理解成画笔;把QPaintDevice理解成使用画笔的地方,比如纸张、屏幕等;而对于纸张、屏幕而言,肯定要使用不同的画笔绘制,为了统一使用一种画笔,我们设计了QPaintEngine类,这个类让不同的纸张、屏幕都能使用一种画笔。
  3. Qt 的绘图系统实际上是,使用QPainter在QPainterDevice上进行绘制,它们之间使用QPaintEngine进行通讯(也就是翻译QPainter的指令)。

QPainter

  1. 重写QWidget的paintEvent()函数。接下来就是PaintedWidget的源代码:

     #include "widget.h"
     #include "ui_widget.h"
     #include<QPainter>
        
     Widget::Widget(QWidget *parent) :
         QWidget(parent),
         ui(new Ui::Widget)
     {
         ui->setupUi(this);
     }
        
     Widget::~Widget()
     {
         delete ui;
     }
     //绘图事件
     //调用时刻,系统自动调用,相当于iOS 的drawract
     void Widget:: paintEvent(QPaintEvent *){
         //创建画家
         //构造函数需要制定一个画板QPaintDevice,把this作为画板
         //因为QWidget继承自QObject/QPaintDevice,因此可以做画板
         QPainter painter(this);
        
         //设置画笔属性
         //画笔颜色
     //    painter.setPen(Qt::blue);
         QPen pen(QColor(255,0,0));
         //设置线宽
         pen.setWidth(10);
         //设置风格(虚线)
         pen.setStyle(Qt::DotLine);
         painter.setPen(pen);
        
         //填充封闭图形的颜色
         QBrush bursh(Qt::green);
         //设置风格
         bursh.setStyle(Qt::Dense1Pattern);
         painter.setBrush(bursh);
        
         //画家画画
         //划线
         painter.drawLine(QPoint(0,0),QPoint(100,100));
         //画圆
         painter.drawEllipse(QPoint(100,100),50,50);
         //矩形
         painter.drawRect(QRect(200,200,100,50));
         //画一个字
         painter.drawText(QRect(100,200,100,50),"好好学习");
     }
    
    
  2. 高级设置

     QPainter painter(this);
     //    painter.drawEllipse(QPoint(100,100),50,50);
     //    //高级设置
     //    //设置抗锯齿能力,圆会更好,但是效率低
     //    painter.setRenderHint(QPainter:: Antialiasing);
     //    painter.drawEllipse(QPoint(200,100),50,50);
        
     //矩形
     painter.drawRect(QRect(20,20,50,50));
     //移动(本来应该是重合的,但是移动就不会重合了)
     painter.translate(100,0);
     //保存状态
     painter.save();
     painter.drawRect(QRect(20,20,50,50));
            
     //这个移动将失效
     painter.translate(100,0);
     //取出状态,此时第三个矩形,就不会移动了,就会和第二个矩形重合
     //因为此时拿到第二个矩形保存的状态,之前的移动就没用了
     painter.restore();
     painter.drawRect(QRect(20,20,50,50));
        
    
  3. 画图

     Widget::Widget(QWidget *parent) :QWidget(parent), ui(new Ui::Widget)
     {
         ui->setupUi(this);
         //点击按钮移动图片
         pointx = 10;
         connect(ui->pushButton,&QPushButton::clicked,[=](){
             pointx+=10;
             //!!!不能手动调用paintEvent
             //需要通过update,会自动调用paintEvent
             update();
         });
     }
        
     Widget::~Widget()
     {
         delete ui;
     }
     void Widget:: paintEvent(QPaintEvent *){
         //创建画家
         //构造函数需要制定一个画板QPaintDevice,把this作为画板
         //因为QWidget继承自QObject/QPaintDevice,因此可以做画板
         QPainter painter(this);
         //如果出屏幕,则返回原位置
         if(pointx> this->width()){
             pointx = 10;
         }
         painter.drawPixmap(pointx,10,QPixmap(":/Image/Luffy.png"));
     }
    

绘图设备

  1. 绘图设备是指继承QPainterDevice的子类。Qt一共提供了四个这样的类,分别是QPixmap、QBitmap、QImage和 QPicture。

     QPixmap专门为图像在屏幕上的显示做了优化
     QBitmap是QPixmap的一个子类,它的色深限定为1,可以使用 QPixmap的isQBitmap()函数来确定这个QPixmap是不是一个QBitmap。
     QImage专门为图像的像素级访问做了优化。 
     QPicture则可以记录和重现QPainter的各条命令。
    

QPixmap、QBitmap、QImage

  1. QPixmap:
    1. 继承了QPaintDevice,因此,你可以使用QPainter直接在上面绘制图形。
    2. QPixmap也可以接受一个字符串作为一个文件的路径来显示这个文件,比如你想在程序之中打开png、jpeg之类的文件,就可以使用 QPixmap。
    3. 使用QPainter的drawPixmap()函数可以把这个文件绘制到一个QLabel、QPushButton或者其他的设备上面。
    4. QPixmap是针对屏幕进行特殊优化的,因此,它与实际的底层显示设备息息相关。
    5. 注意,这里说的显示设备并不是硬件,而是操作系统提供的原生的绘图引擎。所以,在不同的操作系统平台下,QPixmap的显示可能会有所差别。
  2. QBitmap
    1. 继承自QPixmap,因此具有QPixmap的所有特性,提供单色图像。
    2. QBitmap的色深始终为1. 色深这个概念来自计算机图形学,是指用于表现颜色的二进制的位数。我们知道,计算机里面的数据都是使用二进制表示的。为了表示一种颜色,我们也会使用二进制。比如我们要表示8种颜色,需要用3个二进制位,这时我们就说色深是3. 因此,所谓色深为1,也就是使用1个二进制位表示颜色。1个位只有两种状态:0和1,因此它所表示的颜色就有两种,黑和白。所以说,QBitmap实际上是只有黑白两色的图像数据。
    3. 由于QBitmap色深小,因此只占用很少的存储空间,所以适合做光标文件和笔刷。
  3. QImage
    1. QPixmap使用底层平台的绘制系统进行绘制,无法提供像素级别的操作,而QImage则是使用独立于硬件的绘制系统,实际上是自己绘制自己,因此提供了像素级别的操作,并且能够在不同系统之上提供一个一致的显示形式。
    2. 代码示例

       void Widget:: paintEvent(QPaintEvent *){
           QImage img;
           img.load(":/Image/Luffy.png");
              
           //img可以修改像素点
           //把这张图片的某一块像素点给设置成红色
           for (int i= 50;i<100;i++) {
               for (int j = 50;j<100;j++) {
                   QRgb value = qRgb(255,0,0);
                   img.setPixel(i,j,value);
               }
           }
           QPainter painter(this);
           painter.drawImage(QPoint(50,50),img);
       }
      
  4. QImage与QPixmap的区别
    1. QPixmap主要是用于绘图,针对屏幕显示而最佳化设计,QImage主要是为图像I/O、图片访问和像素修改而设计的
    2. QPixmap依赖于所在的平台的绘图引擎,故例如反锯齿等一些效果在不同的平台上可能会有不同的显示效果,QImage使用Qt自身的绘图引擎,可在不同平台上具有相同的显示效果
    3. 由于QImage是独立于硬件的,也是一种QPaintDevice,因此我们可以在另一个线程中对其进行绘制,而不需要在GUI线程中处理,使用这一方式可以很大幅度提高UI响应速度。
    4. QImage可通过setPixpel()和pixel()等方法直接存取指定的像素。
    5. QImage与QPixmap之间的转换:
      1. QImage转QPixmap:使用QPixmap的静态成员函数: fromImage()

         QPixmap	fromImage(const QImage & image, 
         Qt::ImageConversionFlags flags = Qt::AutoColor)
        
      2. QPixmap转QImage:使用QPixmap类的成员函数: toImage()

         QImage toImage() const
        
    6. 代码示例:

       Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget)
       {
           ui->setupUi(this);
      
           //QPixmap 做绘图设备
           //对不同平台进行了优化
           QPixmap pix(300,300);
           //设置pix,默认填充色,即画面的背景色
           pix.fill(Qt::white);
           QPainter painter(&pix);
           painter.setPen(QPen(Qt::red));
           painter.drawEllipse(QPoint(150,150),50,50);
           //保存这张图片
           pix.save("C:\\pix.png");
                  
           //QImage 做绘图设备
           //QImage没有直接设置宽高的构造函数
           //对像素进行了优化
           QImage img(300,300,QImage::Format_RGB32);
           img.fill(Qt::white);
           QPainter painter(&img);
           painter.setPen(QPen(Qt::blue));
           painter.drawEllipse(QPoint(150,150),50,50);
           //保存这张图片
           img.save("C:\\img.png");
                  
           //上面这么做运行程序是什么也看不到的,因为并没有在Widget窗口上画画
           //打开C盘目录下找到pix.png打开,可以看见
       }
      

QPicture

  1. QPicture是一个可以记录和重现QPainter命令的绘图设备。
    1. 不是用来画图的,是用来记录画图指令,然后重现画图指令的
  2. QPicture将QPainter的命令序列化到一个IO设备,保存为一个平台独立的文件格式。这种格式有时候会是“元文件(meta- files)”。
  3. Qt的这种格式是二进制的,不同于某些本地的元文件,Qt的pictures文件没有内容上的限制,只要是能够被QPainter绘制的元素,不论是字体还是pixmap,或者是变换,都可以保存进一个picture中。
  4. QPicture是平台无关的,因此它可以使用在多种设备之上,比如svg、pdf、ps、打印机或者屏幕。回忆下我们这里所说的QPaintDevice,实际上是说可以有QPainter绘制的对象。QPicture使用系统的分辨率,并且可以调整 QPainter来消除不同设备之间的显示差异。
  5. 如果我们要记录下QPainter的命令,首先要使用QPainter::begin()函数,将QPicture实例作为参数传递进去,以便告诉系统开始记录,记录完毕后使用QPainter::end()命令终止。代码示例如下:

     Widget::Widget(QWidget *parent) :QWidget(parent), ui(new Ui::Widget)
     {
         ui->setupUi(this);
        
         //QPicture 绘图设备
         //不是用来画图的,是用来记录画图指令,然后重现画图指令的
         //1. 记录画图指令
         QPicture pic; //用于重新记录绘图指令
         QPainter painter;
         //开始记录指令
         painter.begin(&pic);
         painter.setPen(QPen(Qt::green));
         painter.drawEllipse(QPoint(150,150),50,50);
         //结束指令
         painter.end();
         //保存,后缀名随便写,即使定义成png,也不能打开
         pic.save("C:\\pic.zt");
     }
        
     void Widget:: paintEvent(QPaintEvent *){
         //2、重现绘图指令
         QPicture pic;
         pic.load("c:\\pic.zt");
         QPainter painter(this);
         painter.drawPicture(0,0,pic);
     }
    

文件系统

  1. 文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。
  2. Qt 通过QIODevice提供了对 I/O 设备的抽象,这些设备具有读写字节块的能力。下面是 I/O 设备的类图(Qt5): 图4
  3. 类说明:
    • QIODevice:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;
    • QFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现。
    • QFlie:访问本地文件或者嵌入资源;
    • QTemporaryFile:创建和访问本地文件系统的临时文件;
    • QBuffer:读写QbyteArray, 内存文件;
    • QProcess:运行外部程序,处理进程间通讯;
    • QAbstractSocket:所有套接字类的父类;
    • QTcpSocket:TCP协议网络数据传输;
    • QUdpSocket:传输 UDP 报文;
    • QSslSocket:使用 SSL/TLS 传输数据;
  4. 文件系统分类
    1. 顺序访问设备:
      1. 是指它们的数据只能访问一遍:从头走到尾,从第一个字节开始访问,直到最后一个字节,中途不能返回去读取上一个字节,这其中,QProcess、QTcpSocket、QUdpSoctet和QSslSocket是顺序访问设备。
    2. 随机访问设备:
      1. 可以访问任意位置任意次数,还可以使用QIODevice::seek()函数来重新定位文件访问位置指针,QFile、QTemporaryFile和QBuffer是随机访问设备,

基本文件操作

  1. 在所有的 I/O 设备中,文件 I/O 是最重要的部分之一。因为我们大多数的程序依旧需要首先访问本地文件(当然,在云计算大行其道的将来,这一观点可能改变)。
  2. QFile提供了从文件中读取和写入数据的能力。
    1. 我们通常会将文件路径作为参数传给QFile的构造函数。
    2. 不过也可以在创建好对象最后,使用setFileName()来修改。
    3. QFile需要使用/作为文件分隔符,不过,它会自动将其转换成操作系统所需要的形式。例如 C:/windows 这样的路径在 Windows 平台下同样是可以的。
    4. QFile主要提供了有关文件的各种操作,比如打开文件、关闭文件、刷新文件等。
    5. 我们可以使用QDataStreamQTextStream类来读写文件,也可以使用QIODevice类提供的read()、readLine()、readAll()以及write()这样的函数。
  3. QFileInfo
    1. 有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo获取,而不是自己分析文件路径字符串。
  4. 下面我们使用一段代码来看看QFile的有关操作:

     int main(int argc, char *argv[])
     {
         QApplication app(argc, argv);
      	   //创建文件对象
         QFile file("in.txt");
         //打开文件:形式是只读方式,文本格式。
         if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
             qDebug() << "Open file failed.";
             return -1;
         } else {
             //file.atEnd文件是否结尾,文件每一行的数据file.readLine
             while (!file.atEnd()) {
                 qDebug() << file.readLine();
             }
         }
            
         //QFileInfo对象获取文件信息
         QFileInfo info(file);
         //检查该文件是否是目录
         qDebug() << info.isDir();
         //检查该文件是否是可执行文件等。
         qDebug() << info.isExecutable();
         //可以直接获得文件名;
         qDebug() << info.baseName();
         //获取完整的文件名
         qDebug() << info.completeBaseName();
         //直接获取文件后缀名。
         qDebug() << info.suffix();
         //获取完整的文件后缀
         qDebug() << info.completeSuffix();
         
         return app.exec();
     }
    
  5. 我们可以由下面的示例看到,baseName()completeBaseName(),以及suffix()completeSuffix()的区别:

     QFileInfo fi("/tmp/archive.tar.gz");
     QString base  = fi.baseName();  // base = "archive"
     QString base  = fi.completeBaseName();  // base = "archive.tar"
     QString ext   = fi.suffix();  // ext = "gz"
     QString ext   = fi.completeSuffix();  // ext = "tar.gz"
    
  6. 代码举例:
    1. 功能:
      1. 点击“选择文件” ,找到文件,打开
      2. 将路径显示在lineEdit
      3. 将文件内容显示在textEdit
    2. 代码示例

       Widget::Widget(QWidget *parent) : QWidget(parent),ui(new Ui::Widget)
       {
       ui->setupUi(this);
       //1. 点击选择文件
       connect(ui->pushButton,&QPushButton::clicked,[=](){
           //设置默认打开的路径: C:\Users\Administrator\Desktop
         QString path =  QFileDialog :: getOpenFileName(this,"打开文件","C:\\Users\\Administrator\\Desktop");
         if(path.isEmpty()){
           QMessageBox::warning(this,"警告","打开失败");
         }else {
             //2. 将路径放入到LineEidt
             ui->lineEdit->setText(path);
             //3. 读取文件
          
             QFile file(path);
             //设置打开方式(只读方式)
             file.open(QIODevice::ReadOnly);
             //读出所有内容
             QByteArray array;
             array = file.readAll();
             //一行一行读出
             //while(!file.atEnd()){
             //array+= file.readLine();
             //}
             //4. 显示到文本编辑框中TextEdit
             //如果文件是utf8则不用转
              ui->textEdit->setText(array);
             //QFile默认只支持utf8格式的文件
             //设置读取其他格式,比如gbk
             //QTextCodec *codec =QTextCodec::codecForName("gbk");
             //读gdk
             //ui->textEdit->setText(codec->toUnicode(array));
             file.close();
          
             //获取文件信息
               QFileInfo info(path);
               qDebug()<<"路径:"<<info.filePath()<<"名称"<<info.fileName()<<"文件大小"<<info.size();
               //创建日期
               qDebug()<<"创建日期:"<<info.created().toString("yyyy-MM-dd hh:mm:ss");
               //修改日期
               qDebug()<<"修改日期:"<<info.lastModified().toString("yyyy-MM-dd hh:mm:ss");
             //写文件
            //重新制定打开方式
           //file.open(QFileDevice::Append);
           //file.write("哈哈哈哈哈");
           //file.close();
       }
      

二进制文件读写

  1. QDataStream提供了基于QIODevice的二进制数据的序列化。
  2. 数据流是一种二进制流,这种流完全不依赖于底层操作系统、CPU 或者字节顺序(大端或小端)。
    1. 例如,在安装了 Windows 平台的 PC 上面写入的一个数据流,可以不经过任何处理,直接拿到运行了 Solaris 的 SPARC 机器上读取。
  3. 由于数据流就是二进制流,因此我们也可以直接读写没有编码的二进制数据,例如图像、视频、音频等。
  4. QDataStream既能够存取 C++ 基本类型,如 int、char、short 等,也可以存取复杂的数据类型,例如自定义的类。实际上,QDataStream对于类的存储,是将复杂的类分割为很多基本单元实现的。
  5. 结合QIODevice,QDataStream可以很方便地对文件、网络套接字等进行读写操作。我们从代码开始看起:

     QFile file("file.dat");
     file.open(QIODevice::WriteOnly);
     QDataStream out(&file);
     out << QString("the answer is");
     out << (qint32)42;
    
    1. 在这段代码中,我们首先打开一个名为 file.dat 的文件(注意,我们为简单起见,并没有检查文件打开是否成功,这在正式程序中是不允许的)。然后,我们将刚刚创建的file对象的指针传递给一个QDataStream实例out。类似于std::cout标准输出流,QDataStream也重载了输出重定向«运算符。后面的代码就很简单了:将“the answer is”和数字 42 输出到数据流。由于我们的 out 对象建立在file之上,因此相当于将问题和答案写入file。
    2. 需要指出一点:最好使用 Qt 整型来进行读写,比如程序中的qint32。这保证了在任意平台和任意编译器都能够有相同的行为。
    3. 如果你直接运行这段代码,你会得到一个空白的 file.dat,并没有写入任何数据。这是因为我们的file没有正常关闭。为性能起见,数据只有在文件关闭时才会真正写入。因此,我们必须在最后添加一行代码:

       file.close(); // 如果不想关闭文件,可以使用 file.flush();
      
  6. 接下来我们将存储到文件中的答案取出来

     QFile file("file.dat");
     file.open(QIODevice::ReadOnly);
     QDataStream in(&file);
     QString str;
     qint32 a;
     in >> str >> a;
    
    1. 唯一需要注意的是,你必须按照写入的顺序,将数据读取出来。顺序颠倒的话,程序行为是不确定的,严重时会直接造成程序崩溃。
  7. 那么,既然QIODevice提供了read()、readLine()之类的函数,为什么还要有QDataStream呢?QDataStream同QIODevice有什么区别?区别在于,QDataStream提供流的形式,性能上一般比直接调用原始 API 更好一些。我们通过下面一段代码看看什么是流的形式:

     QFile file("file.dat");
     file.open(QIODevice::ReadWrite);
     QDataStream stream(&file);
     QString str = "the answer is 42";
     stream << str;
    

文本文件读写

  1. 二进制文件比较小巧,却不是人可读的格式。而文本文件是一种人可读的文件。为了操作这种文件,我们需要使用QTextStream类。QTextStream和QDataStream的使用类似,只不过它是操作纯文本文件的。
  2. QTextStream会自动将 Unicode 编码同操作系统的编码进行转换,这一操作对开发人员是透明的。它也会将换行符进行转换,同样不需要自己处理。QTextStream使用 16 位的QChar作为基础的数据存储单位,同样,它也支持 C++ 标准类型,如 int 等。实际上,这是将这种标准类型与字符串进行了相互转换。
  3. QTextStream同QDataStream的使用基本一致,例如下面的代码将把“The answer is 42”写入到 file.txt 文件中:

     QFile data("file.txt");
     if (data.open(QFile::WriteOnly | QIODevice::Truncate)) 
     {
         QTextStream out(&data);
         out << "The answer is " << 42;
     }
    
    1. 这里,我们在open()函数中增加了QIODevice::Truncate打开方式。我们可以从下表中看到这些打开方式的区别:

       枚举值	               描述
       QIODevice::NotOpen	   未打开
       QIODevice::ReadOnly	以只读方式打开
       QIODevice::WriteOnly	以只写方式打开
       QIODevice::ReadWrite	以读写方式打开
       QIODevice::Append		以追加的方式打开,新增加的内容将被追加到文件末尾
       QIODevice::Truncate   以重写的方式打开,在写入新的数据时会将原有数据全部清除,游标设置在文件开头。
       QIODevice::Text		   在读取时,将行结束符转换成 \n;在写入时,将行结束符转换成本地格式,例如 Win32 平台上是 \r\n
       QIODevice::Unbuffered 忽略缓存
      
    2. 我们在这里使用了QFile::WriteOnly | QIODevice::Truncate,也就是以只写并且覆盖已有内容的形式操作文件。注意,QIODevice::Truncate会直接将文件内容清空。

  4. 虽然QTextStream的写入内容与QDataStream一致,但是读取时却会有些困难:

     QFile data("file.txt");
     if (data.open(QFile::ReadOnly)) 
     {
         QTextStream in(&data);
         QString str;
         int ans = 0;
         in >> str >> ans;
     }
    
    1. 在使用QDataStream的时候,这样的代码很方便,但是使用了QTextStream时却有所不同:读出的时候,str 里面将是 The answer is 42,ans 是 0。这是因为当使用QDataStream写入的时候,实际上会在要写入的内容前面,额外添加一个这段内容的长度值。而以文本形式写入数据,是没有数据之间的分隔的。因此,使用文本文件时,很少会将其分割开来读取,而是使用诸如使用:

       ```
       QTextStream::readLine() 读取一行
       QTextStream::readAll()读取所有文本
       ```
      
    2. 这种函数之后再对获得的QString对象进行处理。
    3. 默认情况下,QTextStream的编码格式是 Unicode,如果我们需要使用另外的编码,可以使用:stream.setCodec("UTF-8");这样的函数进行设置。
  5. 代码举例:

     //文件流读写文件
     //分类:文本流(基础数据类型) 和数据流(大型数据)
     //文本流
     //创建一个文件对象
     QFile file("aaa.txt");
     file.open(QFileDevice::WriteOnly);
     //创建一个流对象,相当于创建一个管道专门用于向aaa.txt输入流数据
     QTextStream stream(&file);
     //流入数据
     stream<<QString("hello world !")<< 123456;
     file.close();
     //运行可以查看aaa.txt
     //file没有设置路径,那么默认就在编译后dedebug文件根目录下
    
     //读
     file.open(QFileDevice::ReadOnly);
     QString str;
     //将aaa.txt中的流输出
     //这种方式一遇见空格就结束,只能读出来hello
     //    stream >> str;
     str = stream.readAll();
     qDebug()<<str;
    
    
     //数据流
     QFile file2("bbb.txt");
     file2.open(QFileDevice::WriteOnly);
     QDataStream stream2(&file2);
     stream2<<QString("hello world !")<< 123456;
     file2.close();
     //上面的文件打开是看都不懂里面的数据的,是经过压缩的。但是可以正常的读取数据
     //读数据
     file2.open(QFileDevice::ReadOnly);
     QString str2;
     int num2;
     stream2 >> str2 >> num2;
     qDebug()<<str2 <<num2;
    
Table of Contents