QT-第六节 绘图和绘图设备、文件系统
绘图和绘图设备
- Qt 的绘图系统允许使用相同的 API 在屏幕和其它打印设备上进行绘制。整个绘图系统基于QPainter,QPainterDevice和QPaintEngine三个类。
- QPainter用来执行绘制的操作;
- QPaintDevice是一个二维空间的抽象,这个二维空间允许QPainter在其上面进行绘制,也就是QPainter工作的空间;
- QPaintEngine提供了画笔(QPainter)在不同的设备上进行绘制的统一的接口。QPaintEngine类应用于QPainter和QPaintDevice之间,通常对开发人员是透明的。除非你需要自定义一个设备,否则你是不需要关心QPaintEngine这个类的。
- 我们可以把QPainter理解成画笔;把QPaintDevice理解成使用画笔的地方,比如纸张、屏幕等;而对于纸张、屏幕而言,肯定要使用不同的画笔绘制,为了统一使用一种画笔,我们设计了QPaintEngine类,这个类让不同的纸张、屏幕都能使用一种画笔。
- Qt 的绘图系统实际上是,使用QPainter在QPainterDevice上进行绘制,它们之间使用QPaintEngine进行通讯(也就是翻译QPainter的指令)。
QPainter
-
重写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),"好好学习"); }
-
高级设置
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));
-
画图
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")); }
绘图设备
-
绘图设备是指继承QPainterDevice的子类。Qt一共提供了四个这样的类,分别是QPixmap、QBitmap、QImage和 QPicture。
QPixmap专门为图像在屏幕上的显示做了优化 QBitmap是QPixmap的一个子类,它的色深限定为1,可以使用 QPixmap的isQBitmap()函数来确定这个QPixmap是不是一个QBitmap。 QImage专门为图像的像素级访问做了优化。 QPicture则可以记录和重现QPainter的各条命令。
QPixmap、QBitmap、QImage
- QPixmap:
- 继承了QPaintDevice,因此,你可以使用QPainter直接在上面绘制图形。
- QPixmap也可以接受一个字符串作为一个文件的路径来显示这个文件,比如你想在程序之中打开png、jpeg之类的文件,就可以使用 QPixmap。
- 使用QPainter的drawPixmap()函数可以把这个文件绘制到一个QLabel、QPushButton或者其他的设备上面。
- QPixmap是针对屏幕进行特殊优化的,因此,它与实际的底层显示设备息息相关。
- 注意,这里说的显示设备并不是硬件,而是操作系统提供的原生的绘图引擎。所以,在不同的操作系统平台下,QPixmap的显示可能会有所差别。
- QBitmap
- 继承自QPixmap,因此具有QPixmap的所有特性,提供单色图像。
- QBitmap的色深始终为1. 色深这个概念来自计算机图形学,是指用于表现颜色的二进制的位数。我们知道,计算机里面的数据都是使用二进制表示的。为了表示一种颜色,我们也会使用二进制。比如我们要表示8种颜色,需要用3个二进制位,这时我们就说色深是3. 因此,所谓色深为1,也就是使用1个二进制位表示颜色。1个位只有两种状态:0和1,因此它所表示的颜色就有两种,黑和白。所以说,QBitmap实际上是只有黑白两色的图像数据。
- 由于QBitmap色深小,因此只占用很少的存储空间,所以适合做光标文件和笔刷。
- QImage
- QPixmap使用底层平台的绘制系统进行绘制,无法提供像素级别的操作,而QImage则是使用独立于硬件的绘制系统,实际上是自己绘制自己,因此提供了像素级别的操作,并且能够在不同系统之上提供一个一致的显示形式。
-
代码示例
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); }
- QImage与QPixmap的区别
- QPixmap主要是用于绘图,针对屏幕显示而最佳化设计,QImage主要是为图像I/O、图片访问和像素修改而设计的
- QPixmap依赖于所在的平台的绘图引擎,故例如反锯齿等一些效果在不同的平台上可能会有不同的显示效果,QImage使用Qt自身的绘图引擎,可在不同平台上具有相同的显示效果
- 由于QImage是独立于硬件的,也是一种QPaintDevice,因此我们可以在另一个线程中对其进行绘制,而不需要在GUI线程中处理,使用这一方式可以很大幅度提高UI响应速度。
- QImage可通过setPixpel()和pixel()等方法直接存取指定的像素。
- QImage与QPixmap之间的转换:
-
QImage转QPixmap:使用QPixmap的静态成员函数: fromImage()
QPixmap fromImage(const QImage & image, Qt::ImageConversionFlags flags = Qt::AutoColor)
-
QPixmap转QImage:使用QPixmap类的成员函数: toImage()
QImage toImage() const
-
-
代码示例:
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
- QPicture是一个可以记录和重现QPainter命令的绘图设备。
- 不是用来画图的,是用来记录画图指令,然后重现画图指令的
- QPicture将QPainter的命令序列化到一个IO设备,保存为一个平台独立的文件格式。这种格式有时候会是“元文件(meta- files)”。
- Qt的这种格式是二进制的,不同于某些本地的元文件,Qt的pictures文件没有内容上的限制,只要是能够被QPainter绘制的元素,不论是字体还是pixmap,或者是变换,都可以保存进一个picture中。
- QPicture是平台无关的,因此它可以使用在多种设备之上,比如svg、pdf、ps、打印机或者屏幕。回忆下我们这里所说的QPaintDevice,实际上是说可以有QPainter绘制的对象。QPicture使用系统的分辨率,并且可以调整 QPainter来消除不同设备之间的显示差异。
-
如果我们要记录下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); }
文件系统
- 文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。
- Qt 通过QIODevice提供了对 I/O 设备的抽象,这些设备具有读写字节块的能力。下面是 I/O 设备的类图(Qt5):
- 类说明:
- QIODevice:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;
- QFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现。
- QFlie:访问本地文件或者嵌入资源;
- QTemporaryFile:创建和访问本地文件系统的临时文件;
- QBuffer:读写QbyteArray, 内存文件;
- QProcess:运行外部程序,处理进程间通讯;
- QAbstractSocket:所有套接字类的父类;
- QTcpSocket:TCP协议网络数据传输;
- QUdpSocket:传输 UDP 报文;
- QSslSocket:使用 SSL/TLS 传输数据;
- 文件系统分类
- 顺序访问设备:
- 是指它们的数据只能访问一遍:从头走到尾,从第一个字节开始访问,直到最后一个字节,中途不能返回去读取上一个字节,这其中,QProcess、QTcpSocket、QUdpSoctet和QSslSocket是顺序访问设备。
- 随机访问设备:
- 可以访问任意位置任意次数,还可以使用QIODevice::seek()函数来重新定位文件访问位置指针,QFile、QTemporaryFile和QBuffer是随机访问设备,
- 顺序访问设备:
基本文件操作
- 在所有的 I/O 设备中,文件 I/O 是最重要的部分之一。因为我们大多数的程序依旧需要首先访问本地文件(当然,在云计算大行其道的将来,这一观点可能改变)。
- QFile提供了从文件中读取和写入数据的能力。
- 我们通常会将文件路径作为参数传给QFile的构造函数。
- 不过也可以在创建好对象最后,使用setFileName()来修改。
- QFile需要使用
/
作为文件分隔符,不过,它会自动将其转换成操作系统所需要的形式。例如 C:/windows 这样的路径在 Windows 平台下同样是可以的。 - QFile主要提供了有关文件的各种操作,比如打开文件、关闭文件、刷新文件等。
- 我们可以使用
QDataStream
或QTextStream
类来读写文件,也可以使用QIODevice
类提供的read()、readLine()、readAll()以及write()
这样的函数。
- QFileInfo
- 有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过
QFileInfo
获取,而不是自己分析文件路径字符串。
- 有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过
-
下面我们使用一段代码来看看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(); }
-
我们可以由下面的示例看到,
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"
- 代码举例:
- 功能:
- 点击“选择文件” ,找到文件,打开
- 将路径显示在lineEdit
- 将文件内容显示在textEdit
-
代码示例
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(); }
- 功能:
二进制文件读写
- QDataStream提供了基于QIODevice的二进制数据的序列化。
- 数据流是一种二进制流,这种流完全不依赖于底层操作系统、CPU 或者字节顺序(大端或小端)。
- 例如,在安装了 Windows 平台的 PC 上面写入的一个数据流,可以不经过任何处理,直接拿到运行了 Solaris 的 SPARC 机器上读取。
- 由于数据流就是二进制流,因此我们也可以直接读写没有编码的二进制数据,例如图像、视频、音频等。
- QDataStream既能够存取 C++ 基本类型,如 int、char、short 等,也可以存取复杂的数据类型,例如自定义的类。实际上,QDataStream对于类的存储,是将复杂的类分割为很多基本单元实现的。
-
结合QIODevice,QDataStream可以很方便地对文件、网络套接字等进行读写操作。我们从代码开始看起:
QFile file("file.dat"); file.open(QIODevice::WriteOnly); QDataStream out(&file); out << QString("the answer is"); out << (qint32)42;
- 在这段代码中,我们首先打开一个名为 file.dat 的文件(注意,我们为简单起见,并没有检查文件打开是否成功,这在正式程序中是不允许的)。然后,我们将刚刚创建的file对象的指针传递给一个QDataStream实例out。类似于std::cout标准输出流,QDataStream也重载了输出重定向«运算符。后面的代码就很简单了:将“the answer is”和数字 42 输出到数据流。由于我们的 out 对象建立在file之上,因此相当于将问题和答案写入file。
- 需要指出一点:最好使用 Qt 整型来进行读写,比如程序中的qint32。这保证了在任意平台和任意编译器都能够有相同的行为。
-
如果你直接运行这段代码,你会得到一个空白的 file.dat,并没有写入任何数据。这是因为我们的file没有正常关闭。为性能起见,数据只有在文件关闭时才会真正写入。因此,我们必须在最后添加一行代码:
file.close(); // 如果不想关闭文件,可以使用 file.flush();
-
接下来我们将存储到文件中的答案取出来
QFile file("file.dat"); file.open(QIODevice::ReadOnly); QDataStream in(&file); QString str; qint32 a; in >> str >> a;
- 唯一需要注意的是,你必须按照写入的顺序,将数据读取出来。顺序颠倒的话,程序行为是不确定的,严重时会直接造成程序崩溃。
-
那么,既然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;
文本文件读写
- 二进制文件比较小巧,却不是人可读的格式。而文本文件是一种人可读的文件。为了操作这种文件,我们需要使用QTextStream类。QTextStream和QDataStream的使用类似,只不过它是操作纯文本文件的。
- QTextStream会自动将 Unicode 编码同操作系统的编码进行转换,这一操作对开发人员是透明的。它也会将换行符进行转换,同样不需要自己处理。QTextStream使用 16 位的QChar作为基础的数据存储单位,同样,它也支持 C++ 标准类型,如 int 等。实际上,这是将这种标准类型与字符串进行了相互转换。
-
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; }
-
这里,我们在open()函数中增加了QIODevice::Truncate打开方式。我们可以从下表中看到这些打开方式的区别:
枚举值 描述 QIODevice::NotOpen 未打开 QIODevice::ReadOnly 以只读方式打开 QIODevice::WriteOnly 以只写方式打开 QIODevice::ReadWrite 以读写方式打开 QIODevice::Append 以追加的方式打开,新增加的内容将被追加到文件末尾 QIODevice::Truncate 以重写的方式打开,在写入新的数据时会将原有数据全部清除,游标设置在文件开头。 QIODevice::Text 在读取时,将行结束符转换成 \n;在写入时,将行结束符转换成本地格式,例如 Win32 平台上是 \r\n QIODevice::Unbuffered 忽略缓存
-
我们在这里使用了
QFile::WriteOnly | QIODevice::Truncate
,也就是以只写并且覆盖已有内容的形式操作文件。注意,QIODevice::Truncate
会直接将文件内容清空。
-
-
虽然QTextStream的写入内容与QDataStream一致,但是读取时却会有些困难:
QFile data("file.txt"); if (data.open(QFile::ReadOnly)) { QTextStream in(&data); QString str; int ans = 0; in >> str >> ans; }
-
在使用QDataStream的时候,这样的代码很方便,但是使用了QTextStream时却有所不同:读出的时候,str 里面将是
The answer is 42,ans 是 0
。这是因为当使用QDataStream写入的时候,实际上会在要写入的内容前面,额外添加一个这段内容的长度值。而以文本形式写入数据,是没有数据之间的分隔的。因此,使用文本文件时,很少会将其分割开来读取,而是使用诸如使用:``` QTextStream::readLine() 读取一行 QTextStream::readAll()读取所有文本 ```
- 这种函数之后再对获得的QString对象进行处理。
- 默认情况下,QTextStream的编码格式是 Unicode,如果我们需要使用另外的编码,可以使用:
stream.setCodec("UTF-8");
这样的函数进行设置。
-
-
代码举例:
//文件流读写文件 //分类:文本流(基础数据类型) 和数据流(大型数据) //文本流 //创建一个文件对象 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;