title: 文本读写、绘图与图表 tags: [Qt, QFile, QTextStream, QPainter, QChart, QtCharts, 文本读写, 绘图, 图表, QPen, QBrush, QColor, QIODevice, IMX6ULL, 嵌入式Qt] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[05-Qt应用开发/04-Qt控件详解]]、[[05-Qt应用开发/05-布局容器与模型视图]]、[[05-Qt应用开发/07-多线程网络多媒体与数据库]];延伸阅读:[[编程学习项目/使用Qt和C++构造UI界面/10 绘图和绘图设备]]、[[编程学习项目/使用Qt和C++构造UI界面/11 文件系统]]
本篇把 Qt 里三类「偏底层但天天要用」的能力放在一起讲:
QFile 直接读写文件,或者用 QTextStream 以流的方式读写,这是歌词显示、配置文件、日志、字符设备读写的基础;QPainter 在窗口(画布)上画点、线、矩形、圆、文字、图片,并做旋转、缩放等坐标变换,这是自定义控件和界面重绘的基础;Qt Charts 模块(QChart / QChartView / 各种 Series)画出折线图、曲线图、柱状图、饼图,这是数据可视化的基础。三者都是 Qt GUI 应用的能力,和具体硬件无关,因此在 Ubuntu 与 I.MX6ULL 开发板上运行效果一致。本篇示例对应 PDF 第二篇提高篇第八章、第九章,例程编号 Qt/02/01_qfile ~ Qt/02/04_qtchart。
程序运行时经常需要读写文本文件,例如:
QFile 往字符设备写 '0' / '1' 控制硬件。Qt 提供了两条主要路线:
| 路线 | 核心类 | 特点 | 适用场景 |
|---|---|---|---|
| 直接 I/O | QFile(继承 QIODevice) |
一次读全部 / 按行读 / 直接写字节 | 小文件、整体读写、字符设备 |
| 流式 I/O | QTextStream + QFile |
支持 << >> 运算符、格式化、编码转换 |
需要按字段/格式化读写文本 |
两者的关系是:QFile 负责「打开/关闭/定位/读写字节流」,QTextStream 负责「把字节流解释成文本并按格式处理」。QTextStream 不能独立存在,它必须绑定到一个 QIODevice(如 QFile)、QByteArray 或 QString 上。
flowchart LR
subgraph 应用层
A["QTextStream<br/>格式化 / 编码 / 行读取"]
end
subgraph 设备层
B["QFile<br/>打开 / 关闭 / 定位 / 字节读写"]
end
C["磁盘文件 / 字符设备 / 资源文件"]
A -->|"绑定 QIODevice"| B
B -->|"read()/write()"| C
本章只讲「简单的文本读写」,目的是掌握基本操作。更复杂的文件系统操作(文件信息、目录遍历)由
QFileInfo、QDir提供。
QFile 是一个读写文本文件、二进制文件和 Qt 资源文件的 I/O 设备类。它的核心定位是:把文件抽象成一个可从其中读、可向其写的字节流。
要点:
QFile 可以单独使用,也可以更方便地和 QTextStream 或 QDataStream 组合使用;setFileName() 设置;/,不支持 \。无论在 Windows、Linux 还是 Mac,QFile 的文件分隔符都是 /。看到 Windows 用 \,也不能在 QFile 路径里写 \;exists() 判断文件是否存在,用 remove() 删除文件(更高级的文件系统操作由 QFileInfo、QDir 提供);open() 打开,用 close() 关闭,用 flush() 刷新;QDataStream 或 QTextStream 读写,但也可以调用从 QIODevice 继承来的 read()、readLine()、readAll()、write();QFile 还继承了 getChar()、putChar()、ungetChar(),一次只处理一个字符;size() 返回;用 pos() 获取当前文件位置,用 seek() 移动到新位置;到达末尾时 atEnd() 返回 true。QFile::open() 需要一个 QIODevice::OpenModeFlag 枚举参数,决定以什么方式打开文件:
| 枚举值 | 含义 | 说明 |
|---|---|---|
QIODevice::ReadOnly |
以只读方式打开 | 用于载入文件 |
QIODevice::WriteOnly |
以只写方式打开 | 用于保存文件 |
QIODevice::ReadWrite |
以读写方式打开 | 同时可读可写 |
QIODevice::Append |
以添加模式打开 | 新写入的数据追加到文件尾部 |
QIODevice::Truncate |
以截取方式打开 | 文件原有内容全部被删除 |
QIODevice::Text |
以文本方式打开 | 读取时 \n 自动翻译为换行符;写入时字符串结束符自动翻译为平台编码,如 Windows 下是 \r\n |
这些取值可以按位或组合,例如 QIODevice::ReadOnly | QIODevice::Text 表示以「只读 + 文本」方式打开。
⚠️ 易错点:
WriteOnly单独使用时会清空原文件内容(等价于隐式Truncate)。如果想在文件末尾追加而不是覆盖,必须用QIODevice::Append。
| API | 返回类型 | 说明 |
|---|---|---|
QFile(const QString &name) |
— | 构造并指定文件名 |
QFile(QObject *parent) |
— | 构造,父对象为 parent |
setFileName(const QString &name) |
void |
设置文件名 |
fileName() const |
QString |
获取文件名 |
exists() const |
bool |
文件是否存在 |
remove() |
bool |
删除文件 |
open(OpenMode flags) |
bool |
按模式打开文件 |
close() |
void |
关闭文件 |
flush() |
bool |
刷新缓冲区 |
readAll() |
QByteArray |
读取全部剩余内容 |
read(qint64 maxSize) |
QByteArray |
读取最多 maxSize 字节 |
readLine(qint64 maxSize = 0) |
QByteArray |
读取一行 |
write(const QByteArray &byteArray) |
qint64 |
写入字节数组,返回实际写入字节数 |
size() const |
qint64 |
文件大小 |
pos() const |
qint64 |
当前读写位置 |
seek(qint64 pos) |
bool |
移动到指定位置 |
atEnd() const |
bool |
是否到达末尾 |
QFile 操作文本文件的流程固定为三步:
flowchart LR
A["创建 QFile 对象"] --> B["设置文件名 / 打开<br/>setFileName + open(模式)"]
B --> C["读写<br/>readAll / readLine / write"]
C --> D["关闭<br/>close()"]
对应代码骨架:
QFile file("test.txt"); /* 创建对象并指定文件名 */
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) /* 打开 */
return; /* 打开失败即返回 */
QString content = file.readAll(); /* 读全部内容 */
file.close(); /* 关闭 */
例程路径 Qt/02/01_qfile。功能:点击「打开」按钮弹出系统文件选择框,选中文本文件后把内容读入 QTextEdit;修改内容后点击「关闭」按钮,把 QTextEdit 的内容写回原文件并清空显示。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTextEdit>
#include <QFile>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* 用于读取文件后显示 */
QTextEdit *textEdit;
/* QFile 类型对象 */
QFile file;
/* 水平布局 */
QHBoxLayout *hBoxLayout;
/* 垂直布局 */
QVBoxLayout *vBoxLayout;
/* 水平布局 Widget */
QWidget *hWidget;
/* 垂直布局 Widget */
QWidget *vWidget;
/* 打开文件按钮 */
QPushButton *openPushButton;
/* 关闭文件按钮 */
QPushButton *closePushButton;
private slots:
/* 打开文本文件 */
bool openFile();
/* 关闭文本文件 */
void closeFile();
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include <QFileDialog>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置窗口的位置与大小 */
this->setGeometry(0, 0, 800, 480);
/* 布局设置 */
textEdit = new QTextEdit();
vBoxLayout = new QVBoxLayout();
hBoxLayout = new QHBoxLayout();
vWidget = new QWidget();
hWidget = new QWidget();
openPushButton = new QPushButton();
closePushButton = new QPushButton();
/* 设置两个按钮的大小 */
openPushButton->setMinimumHeight(50);
openPushButton->setMaximumWidth(120);
closePushButton->setMinimumHeight(50);
closePushButton->setMaximumWidth(120);
/* 设置两个按钮的文本 */
openPushButton->setText("打开");
closePushButton->setText("关闭");
/* 设置关闭按钮为不可用属性,需要打开文件才设置为可用属性 */
closePushButton->setEnabled(false);
/* 水平布局 */
hBoxLayout->addWidget(openPushButton);
hBoxLayout->addWidget(closePushButton);
hWidget->setLayout(hBoxLayout);
/* 垂直布局 */
vBoxLayout->addWidget(textEdit);
vBoxLayout->addWidget(hWidget);
vWidget->setLayout(vBoxLayout);
/* 居中 */
setCentralWidget(vWidget);
/* 信号槽连接 */
connect(openPushButton, SIGNAL(clicked()),
this, SLOT(openFile()));
connect(closePushButton, SIGNAL(clicked()),
this, SLOT(closeFile()));
}
MainWindow::~MainWindow()
{
}
bool MainWindow::openFile()
{
/* 获取文件的路径 */
QString fileName = QFileDialog::getOpenFileName(this);
/* 指向文件 */
file.setFileName(fileName);
/* 判断文件是否存在 */
if (!file.exists())
return false;
/* 以读写的方式打开 */
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return false;
/* 读取文本到 textEdit */
textEdit->setPlainText(file.readAll());
/* 设置打开按钮不可用,需要关闭再打开 */
openPushButton->setEnabled(false);
/* 设置关闭按钮为可用属性 */
closePushButton->setEnabled(true);
/* 关闭文件 */
file.close();
return true;
}
void MainWindow::closeFile()
{
/* 检测打开按钮是否可用,不可用时,说明已经打开了文件 */
if (!openPushButton->isEnabled()) {
/* 获取 textEdit 的文本内容 */
QString str = textEdit->toPlainText();
/* 以只写的方式打开 */
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
return;
/* 转换为字节数组 */
QByteArray strBytes = str.toUtf8();
/* 写入文件 */
file.write(strBytes, strBytes.length());
/* 清空 textEdit 的显示内容 */
textEdit->clear();
/* 关闭文件 */
file.close();
/* 重新设置打开和关闭按钮的属性 */
openPushButton->setEnabled(true);
closePushButton->setEnabled(false);
}
}
file 是成员变量而非局部变量:openFile() 打开、closeFile() 写入,两次操作之间要记住「当前操作的是哪个文件」,所以 QFile file 声明在头文件里,跨槽函数共享状态。QFileDialog::getOpenFileName(this):弹出系统文件选择对话框,返回用户选中文件的绝对路径;用户取消则返回空字符串。此时 file.setFileName(""),紧接着的 exists() 返回 false,函数直接返回,不会崩溃。openPushButton / closePushButton 的启用状态当作状态机:
openPushButton->setEnabled(false)、closePushButton->setEnabled(true);closeFile() 开头的 if (!openPushButton->isEnabled()):只有「已经打开过文件」才允许保存,防止用户没打开就直接点关闭而误写文件。str.toUtf8():QString 内部是 UTF-16,写入文件前必须转成字节序列。用 toUtf8() 保证中文不会乱码。file.write(strBytes, strBytes.length()):write() 写入字节数组,第二个参数是长度;也可以用 file.write(strBytes) 让 Qt 自动取长度。ReadOnly | Text,写用 WriteOnly | Text。写模式会覆盖原文件内容,这正是「另存/保存」的语义。Text 标志:读取时把平台换行统一成 \n,写入时把 \n 转成平台换行(Windows 为 \r\n),避免在 Windows 上写出的文件出现「双回车」。说明:实际项目里「打开/关闭文件」一般放在菜单栏用
QAction实现,并加上复制、粘贴、另存为、关闭等动作,完全可以仿照 Windows 记事本写一个 Qt 版记事本。
测试.txt;QTextEdit 中,可以直接修改;QTextEdit 被清空;其他常用读写方式(扩展):
/* 按行读取,适合日志、歌词等大文件 */
QFile file("lyric.txt");
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
while (!file.atEnd()) {
QByteArray line = file.readLine(); /* 含行尾换行符 */
qDebug() << line.trimmed(); /* trimmed() 去掉首尾空白 */
}
file.close();
}
/* 追加写入,不覆盖原内容 */
if (file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
file.write("新的一行\n");
file.close();
}
/* seek / pos 定位读写 */
file.open(QIODevice::ReadWrite);
file.seek(10); /* 移动到第 10 字节处 */
qint64 p = file.pos(); /* 当前读写位置 */
file.close();
QTextStream 类为读写文本提供了一个方便的接口,常与 QFile 结合使用。它可以在 QIODevice、QByteArray 或 QString 上操作。
它的好处:
<<(流插入运算符)和 >>(流提取运算符)就能方便地读写单词、行和数字;QTextStream 内部与 C++ 的 iostream 思路一致:把一个「设备」包成一个流,然后用运算符逐项读写。
flowchart LR
A["QString / QByteArray / QIODevice"] --> B["QTextStream"]
B -->|"operator<<"| C["写入文本"]
B -->|"operator>>"| D["读取文本"]
B -.-> E["编码 / 填充 / 对齐 / 数字格式"]
QTextStream 默认按本地编码(QTextCodec::codecForLocale())读写。跨平台或处理中文时,建议显式设置编码:
| API | 说明 |
|---|---|
setCodec(QTextCodec *codec) |
设置编解码器(Qt5 常见写法) |
setCodec(const char *codecName) |
用编码名设置,如 "UTF-8" |
setAutoDetectUnicode(bool) |
自动探测 Unicode |
setGenerateByteOrderMark(bool) |
写入时是否输出 BOM |
示例:
QFile file("data.txt");
file.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream out(&file);
out.setCodec("UTF-8"); /* 明确 UTF-8,含中文时避免乱码 */
out << "温度:25℃" << endl;
file.close();
⚠️ 说明:
setCodec是 Qt5 的常用写法;Qt6 中QTextStream默认使用 UTF-8,setCodec相关接口有所调整。正点原子教程基于 Qt5,嵌入式开发板上通常也是 Qt5 环境。
QTextStream 支持 C++ iostream 风格的格式化:
| API | 说明 |
|---|---|
setFieldWidth(int) |
字段总宽度 |
setFieldAlignment(FieldAlignment) |
对齐方式:AlignLeft / AlignRight / AlignCenter / AlignAccountingStyle |
setPadChar(QChar) |
填充字符,默认空格 |
setRealNumberPrecision(int) |
浮点数有效位数 |
setRealNumberNotation(RealNumberNotation) |
浮点表示法:SmartNotation / FixedNotation / ScientificNotation |
setIntegerBase(int) |
整数进制:2/8/10/16 |
setNumberFlags(NumberFlags) |
如 ShowBase、UppercaseBase、ForceSign 等 |
带对齐的表格化输出示例:
QTextStream out(stdout);
out.setFieldWidth(10);
out.setFieldAlignment(QTextStream::AlignCenter);
out << "姓名" << "成绩" << endl;
out.setFieldWidth(0); /* 取消字段宽度 */
out << "张三" << 95 << endl;
QTextStream 常用读写 API:
| API | 返回类型 | 说明 |
|---|---|---|
readAll() |
QString |
读取全部剩余文本 |
readLine(qint64 maxlen = 0) |
QString |
读取一行 |
read(qint64 maxlen) |
QString |
读取指定长度 |
atEnd() const |
bool |
是否到末尾 |
operator<<(T value) |
QTextStream & |
流插入,写入 |
operator>>(T &value) |
QTextStream & |
流提取,读取 |
flush() |
void |
刷新 |
seek(qint64 pos) |
bool |
定位 |
pos() const |
qint64 |
当前位置 |
例程路径 Qt/02/02_qtextstream。界面与 01_qfile 完全一样,唯一的区别是在 QFile 的基础上叠了一层 QTextStream。下面给出差异代码,其余代码与 01_qfile 相同。
bool MainWindow::openFile()
{
/* 获取文件的路径 */
QString fileName = QFileDialog::getOpenFileName(this);
/* 指向文件 */
file.setFileName(fileName);
/* 判断文件是否存在 */
if (!file.exists())
return false;
/* 以读写的方式打开 */
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return false;
/* 使用文本流读取文件 */
QTextStream stream(&file);
/* 读取文本到 textEdit */
textEdit->setPlainText(stream.readAll());
/* 设置打开按钮不可用,需要关闭再打开 */
openPushButton->setEnabled(false);
/* 设置关闭按钮为可用属性 */
closePushButton->setEnabled(true);
/* 关闭文件 */
file.close();
return true;
}
void MainWindow::closeFile()
{
/* 检测打开按钮是否可用,不可用时,说明已经打开了文件 */
if (!openPushButton->isEnabled()) {
/* 以只写的方式打开 */
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
return;
/* 用文本流读取文件 */
QTextStream stream(&file);
/* 获取 textEdit 的文本内容,转为字符串 */
QString str = textEdit->toPlainText();
/* 使用流提取运算符,写入文本流 */
stream << str;
/* 清空 textEdit 的显示内容 */
textEdit->clear();
/* 关闭文件 */
file.close();
/* 重新设置打开和关闭按钮的属性 */
openPushButton->setEnabled(true);
closePushButton->setEnabled(false);
}
}
头文件 mainwindow.h 与 01_qfile 完全一致。
QTextStream stream(&file);:构造时把 QFile 的地址传进去,表示这个流读写的是 file 指向的设备。构造必须在 file.open() 成功之后,否则流无法工作。stream.readAll() 返回 QString,比起 QFile::readAll() 返回 QByteArray,它省去了手动转编码的步骤。stream << str; 直接用流插入运算符。QFile 版本写入时要先 str.toUtf8() 转字节数组再 file.write();QTextStream 版本直接 stream << str,由流处理字符编码和换行翻译。为什么这个例子看不出区别:因为只做了简单的整体读写。QTextStream 真正的优势要等到需要字段填充/对齐、数字进制、浮点精度、按词读取时才体现,例如:
QFile file("score.txt");
file.open(QIODevice::ReadOnly | QIODevice::Text);
QTextStream in(&file);
QString name;
int score;
while (!in.atEnd()) {
in >> name >> score; /* 按空白分隔依次读取姓名和成绩 */
qDebug() << name << score;
}
file.close();
与 8.1.2 小节(即 01_qfile 的运行效果)完全一样:点击打开读入文本、修改后点击关闭写回文件。QFile 与 QTextStream 在简单场景下看起来没有区别。
Qt 里的所有绘图——包括一个按钮、一个 Label 的显示——都由绘图系统执行。绘图系统基于三个类:
| 类 | 角色 | 说明 |
| -- | ---- | ---- |
| QPainter | 画家 / 绘图操作类 | 提供 drawLine()、drawRect()、drawText() 等绘图接口 |
| QPaintDevice | 画布 / 绘图设备基类 | 提供绘制对象的基类,代表「能画在哪」 |
| QPaintEngine | 绘图引擎 | 提供一个平台上设备的抽象定义,起「抽象平台」的作用 |
它们的关系是:QPainter 是直接用来操作绘图的类;QPaintDevice 和 QPaintEngine 都比 QPainter 更底层,一般只需要了解即可。
flowchart TB
A["QPainter<br/>直接绘制图像"] --> B["QPainter 通过 QPaintEngine 绘制"]
B --> C["QPaintEngine<br/>提供平台相关的抽象定义"]
C --> D["QPaintDevice<br/>提供绘制对象的基类"]
D --> E["QWidget / QPixmap / QImage / QLabel ..."]
一般用于显示的类,如 QWidget、QPixmap、QImage、QLabel 等可视类控件都可以充当绘图区域的「画布」。从 QWidget 继承的类都有 virtual void paintEvent(QPaintEvent *event);,这个虚函数在 qwidget.h 的 protected: 修饰符下。
什么是绘图事件? 当界面初始化或需要刷新时才会执行的事件。也就是说:
update() 方法触发 paintEvent()。paintEvent() 是父类 QWidget 提供给子类的接口,在父类里定义为空,所以它就是专门给子类画图用的。
重写的基本结构:
void Widget::paintEvent(QPaintEvent *)
{
/* 指定画图的对象,this 代表是本 Widget */
QPainter painter(this);
/* 使用 painter 在对象上绘图... */
}
⚠️ 注意:
paintEvent()是protected的虚函数,重写时应写在子类的protected:区域。不要在paintEvent()外部创建QPainter绘制到窗口,也不要在paintEvent()里调用update()(会造成无限重绘)。
QPainter 需要两个「工具」才能画出好看的东西:
| 工具 | 类 | 作用 |
| ---- | -- | ---- |
| 画笔 | QPen | 决定线条(轮廓)的颜色、宽度、线型、端点样式、连接样式 |
| 画刷 | QBrush | 决定封闭图形内部填充的颜色和样式 |
| 颜色 | QColor | 被 QPen、QBrush 使用,表示颜色 |
| 构造 / 方法 | 说明 |
|---|---|
QColor(Qt::GlobalColor) |
用预定义颜色构造,如 Qt::red、Qt::blue、Qt::gray |
QColor(int r, int g, int b, int a = 255) |
用 RGB(A) 构造,分量 0~255 |
QColor(const QString &name) |
用颜色名构造,如 "#RRGGBB"、"red" |
red()/green()/blue()/alpha() |
取各分量 |
setRgb(...) |
设置 RGB(A) |
QColor c1(Qt::red); /* 预定义 */
QColor c2(255, 128, 0); /* 橙色 */
QColor c3("#00A0FF"); /* 十六进制 */
QColor c4 = QColor::fromHsv(200, 255, 200); /* HSV */
| API | 说明 |
|---|---|
QPen(const QColor &color) |
用颜色构造 |
QPen(const QBrush &brush, qreal width) |
用画刷和线宽构造 |
setColor(QColor) |
设置颜色 |
setWidth(int) |
设置线宽(像素) |
setStyle(Qt::PenStyle) |
线型:SolidLine、DashLine、DotLine、DashDotLine、DashDotDotLine、NoPen |
setCapStyle(Qt::PenCapStyle) |
端点样式:FlatCap、SquareCap、RoundCap |
setJoinStyle(Qt::PenJoinStyle) |
连接样式:MiterJoin、BevelJoin、RoundJoin |
QPen pen;
pen.setColor(Qt::blue);
pen.setWidth(5);
pen.setStyle(Qt::DashLine);
pen.setCapStyle(Qt::RoundCap);
painter.setPen(pen);
| API | 说明 |
|---|---|
QBrush(Qt::GlobalColor) |
用预定义颜色构造 |
QBrush(const QColor &color, Qt::BrushStyle) |
用颜色和样式构造 |
setColor(QColor) |
设置颜色 |
setStyle(Qt::BrushStyle) |
填充样式:SolidPattern、NoBrush、CrossPattern、DiagCrossPattern、HorPattern、VerPattern、Dense1Pattern~Dense7Pattern 等 |
setTexture(QPixmap) |
用图片纹理填充 |
setTextureImage(QImage) |
用图片填充 |
QBrush brush;
brush.setColor(Qt::yellow);
brush.setStyle(Qt::SolidPattern);
painter.setBrush(brush);
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书第九章只以旋转 CD 为例讲解
QPainter,QPen/QBrush/QColor 属于 Qt 官方文档中QPainter的标准用法,此处补充以便实际开发使用。
| API | 说明 |
|---|---|
drawPoint(const QPointF &position) |
画点 |
drawLine(const QLineF &line) |
画直线 |
drawRect(const QRectF &rectangle) |
画矩形(轮廓 + 填充) |
drawRoundedRect(rect, xRadius, yRadius) |
画圆角矩形 |
drawEllipse(const QRectF &rectangle) |
在矩形内画椭圆(内切) |
drawArc(rect, startAngle, spanAngle) |
画圆弧,角度单位为 1/16 度 |
drawPie(rect, startAngle, spanAngle) |
画扇形 |
drawPolygon(const QPolygonF &points) |
画多边形 |
drawPolyline(const QPolygonF &points) |
画折线 |
drawConvexPolygon(...) |
画凸多边形 |
drawText(const QPointF &position, const QString &text) |
在指定位置画文字 |
drawText(const QRectF &rectangle, int flags, const QString &text) |
在矩形内按对齐标志画文字,如 Qt::AlignCenter |
drawImage(const QRectF &target, const QImage &image, const QRectF &source) |
画图片(指定源与目标矩形) |
drawPixmap(const QRectF &target, const QPixmap &pixmap, const QRectF &source) |
画 Pixmap |
fillRect(const QRectF &rectangle, const QColor &color) |
用颜色填充矩形 |
eraseRect(const QRectF &rectangle) |
擦除矩形区域 |
setPen(const QPen &pen) / pen() |
设置 / 获取画笔 |
setBrush(const QBrush &brush) / brush() |
设置 / 获取画刷 |
setFont(const QFont &font) |
设置文字字体 |
setRenderHint(RenderHint, bool on = true) |
设置渲染提示,如抗锯齿 |
translate(qreal dx, qreal dy) |
平移坐标原点 |
rotate(qreal angle) |
绕原点旋转(单位:度,顺时针为正) |
scale(qreal sx, qreal sy) |
缩放坐标系统 |
shear(qreal sh, qreal sv) |
错切 |
save() / restore() |
保存 / 恢复画笔状态与坐标变换状态 |
QPainter 的默认坐标系是:原点 (0,0) 在绘图设备左上角,x 向右增大,y 向下增大,单位是像素。
坐标变换的核心思路:
translate(dx, dy) 移动坐标原点;rotate(angle) 绕当前原点旋转,默认顺时针;scale(sx, sy) 缩放;旋转图片时通常这样做:先把原点平移到图片中心,旋转,再把原点平移回去,最后以中心为基准绘制。也可以用 save() / restore() 成对包裹,避免手工计算反向平移:
painter.save();
painter.translate(rect.center()); /* 原点到图形中心 */
painter.rotate(45); /* 旋转 45 度 */
painter.drawImage(QRectF(-rect.width()/2, -rect.height()/2,
rect.width(), rect.height()), image);
painter.restore(); /* 恢复坐标系统与画笔 */
默认绘制的线条和图形边缘有明显锯齿。开启抗锯齿让曲线、斜线更平滑:
| 渲染提示 | 作用 |
|---|---|
QPainter::Antialiasing |
图形边缘抗锯齿 |
QPainter::TextAntialiasing |
文字抗锯齿 |
QPainter::SmoothPixmapTransform |
图片平滑缩放 |
painter.setRenderHints(QPainter::Antialiasing
| QPainter::SmoothPixmapTransform);
QChartView 上也提供了 setRenderHint(QPainter::Antialiasing)。
例程路径 Qt/02/03_qpainter。功能:用一张 CD 图片,在 paintEvent() 里把 CD 画在窗口中心,并且每 100ms 旋转 1 度,看上去 CD 在旋转。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QPainter>
#include <QPaintEvent>
#include <QTimer>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
/* 重写父类下的 protected 方法 */
protected:
void paintEvent(QPaintEvent *);
private:
/* 定时器,用于定时更新界面 */
QTimer *timer;
/* 角度 */
int angle;
private slots:
/* 槽函数 */
void timerTimeOut();
};
#endif // MAINWINDOW_H
paintEvent() 是父类 QWidget 的 protected 虚方法,所以建议重写时也写到子类的 protected 修饰符下。
#include "mainwindow.h"
#include "QDebug"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置主窗口位置及颜色 */
this->setGeometry(0, 0, 800, 480);
setPalette(QPalette(Qt::gray));
setAutoFillBackground(true);
/* 定时器实例化 */
timer = new QTimer(this);
/* 默认角度为 0 */
angle = 0;
/* 定时 100ms */
timer->start(100);
/* 信号槽连接 */
connect(timer, SIGNAL(timeout()), this, SLOT(timerTimeOut()));
}
MainWindow::~MainWindow()
{
}
void MainWindow::timerTimeOut()
{
/* 需要更新界面,不设置不更新 */
this->update();
}
void MainWindow::paintEvent(QPaintEvent *)
{
/* 指定父对象,this 指本窗口 */
QPainter painter(this);
/* 设置抗锯齿,流畅转换 */
painter.setRenderHints(QPainter::Antialiasing
| QPainter::SmoothPixmapTransform);
/* 计算旋转角度 */
if (angle++ == 360)
angle = 0;
/* QPixmap 类型对象 */
QPixmap image;
/* 加载 */
image.load(":/image/cd.png");
/* QRectF 即继承 QRect(Qt 的矩形类),F 代表精确到浮点类型 */
QRectF rect((this->width() - image.width()) / 2,
(this->height() - image.height()) / 2,
image.width(),
image.height());
/* 默认参考点为左上角原点(0,0),因为旋转需要以图形的中心为参考点,
* 我们使用 translate 把参考点设置为 CD 图形的中心点坐标 */
painter.translate(0 + rect.x() + rect.width() / 2,
0 + rect.y() + rect.height() / 2);
/* 旋转角度 */
painter.rotate(angle);
/* 现在参考点为 CD 图形的中心,我们需要把它设置回原点的位置,
* 所以需要减去上面加上的数 */
painter.translate(0 - (rect.x() + rect.width() / 2),
0 - (rect.y() + rect.height() / 2));
/* 画图,QPainter 提供了许多 drawX 的方法 */
painter.drawImage(rect, image.toImage(), image.rect());
/* 再画一个矩形 */
painter.drawRect(rect.toRect());
}
setPalette(QPalette(Qt::gray)) + setAutoFillBackground(true):给窗口设置灰色背景。只有 autoFillBackground 为 true 时,调色板的背景色才会生效。timer->start(100):每 100ms 触发一次 timeout(),槽函数里调用 update()。update() 会安排一次重绘,最终调用 paintEvent()。如果这里用 repaint() 会立即同步重绘,update() 则更高效(可合并多次请求)。update():paintEvent() 在构造函数执行时只会执行一次,不主动刷新就看不到旋转动画。angle++ == 360:先取当前值再自增;当旧值为 360 时把它重置为 0,形成 0~360 的循环。QRectF:继承 QRect,后缀 F 表示坐标精确到浮点(qreal)。图片居中靠 (窗口宽 - 图片宽)/2、(窗口高 - 图片高)/2 计算。translate:默认参考点是左上角 (0,0),而旋转必须绕图形中心。第一次 translate 把原点移到 CD 中心,rotate(angle) 绕新原点旋转,第二次 translate 把原点平移回去,保证后续 drawImage / drawRect 使用的 rect 坐标仍然以窗口左上角为基准。painter.drawImage(rect, image.toImage(), image.rect()):目标矩形 rect、源图片 image.toImage()、源矩形 image.rect()(整张图)。painter.drawRect(rect.toRect()):在图片外框画一个矩形,让旋转效果更明显。res.qrc 把 image/cd.png 注册为 :/image/cd.png,随可执行文件打包。编译运行后,窗口中央的 CD 每 100ms 转 1 度,产生连续的旋转动画;CD 外框还画了一个矩形,使旋转更明显。
用 paintEvent 可以实现一些需要绘图的情景,有时比 Qt 动画类更容易实现;结合 Qt 的画笔,也可以设计一个绘图软件,这都得益于 paintEvent() 与 QPainter。界面重绘时使用 paintEvent() 的场合也比较多。
在 Qt 5.7 之前,开源社区版本里没有 Qt Charts(自带绘图组件库),这让 QWT、QCustomPlot 等第三方库有了生存空间。Qt 5.7 之后,社区版才免费提供 Qt Charts(此前只有商业版才有)。要使用 Qt Charts,Qt 版本必须是 5.7 之后的版本。
Qt Charts 可以方便地绘制常见的曲线图、折线图、柱状图和饼状图等,无需了解第三方组件。
在项目里使用 Qt Charts 模块,需要在 .pro 文件里添加:
QT += charts
除了包含相应头文件,还需要使用命名空间。一般在头文件处加:
QT_CHARTS_USE_NAMESPACE
或者在类外写:
using namespace QtCharts;
理解继承关系有助于知道这个类「怎么来的」:
flowchart BT
QObject --> QGraphicsObject
QGraphicsItem --> QGraphicsObject
QGraphicsItem --> QGraphicsLayoutItem
QGraphicsObject --> QGraphicsWidget
QGraphicsLayoutItem --> QGraphicsWidget
QGraphicsWidget --> QChart
也就是说 QChart 本质是一个 QGraphicsWidget(它同时继承 QGraphicsObject 与 QGraphicsLayoutItem),可以像图形项一样被放到 QGraphicsScene 里;而 QChartView 是承载 QChart 的视图控件(继承自 QGraphicsView)。在 Qt Creator 里可用 Ctrl + Shift + T 查看某个类的继承关系。
| 类 | 作用 |
| -- | ---- |
| QChart | 图表对象,管理系列(Series)、坐标轴(Axis)、图例(Legend)、标题 |
| QChartView | 图表视图,承载并显示 QChart |
| QAbstractSeries | 所有系列的基类 |
| QLineSeries | 折线图系列 |
| QSplineSeries | 平滑曲线系列 |
| QScatterSeries | 散点图系列 |
| QBarSeries / QHorizontalBarSeries | 柱状图系列 |
| QStackedBarSeries | 堆叠柱状图系列 |
| QBarSet | 柱状图的一组数据 |
| QPieSeries / QPieSlice | 饼图系列 / 饼图扇区 |
| QAreaSeries | 面积图系列 |
| QValueAxis | 数值坐标轴 |
| QBarCategoryAxis | 分类坐标轴(柱状图横轴) |
| QDateTimeAxis | 时间坐标轴 |
| QLogValueAxis | 对数坐标轴 |
| QLegend | 图例,用颜色区分不同系列 |
| 类 | API | 说明 |
| -- | --- | ---- |
| QChart | addSeries(QAbstractSeries *series) | 添加系列 |
| QChart | removeSeries(...) / removeAllSeries() | 移除系列 |
| QChart | addAxis(QAbstractAxis *axis, Qt::Alignment) | 添加坐标轴并指定方位 |
| QChart | setTitle(const QString &) | 设置标题 |
| QChart | legend() | 返回 QLegend *,hide() / show() / setVisible() |
| QChart | setTheme(QChart::ChartTheme) | 设置主题 |
| QChart | createDefaultAxes() | 根据系列自动创建默认坐标轴 |
| QChartView | setChart(QChart *chart) | 设置要显示的图表 |
| QChartView | setRenderHint(QPainter::RenderHint) | 设置渲染提示,如抗锯齿 |
| QAbstractSeries | attachAxis(QAbstractAxis *axis) | 把系列绑定到某条坐标轴 |
| QValueAxis | setRange(qreal min, qreal max) | 设置轴范围 |
| QValueAxis | setTitleText(const QString &) | 设置轴标题 |
| QValueAxis | setLabelFormat(const QString &) | 设置标签格式,如 "%i"、"%g" |
| QValueAxis | setTickCount(int count) | 设置刻度数量 |
| QLineSeries | append(qreal x, qreal y) | 追加数据点 |
| QLineSeries | clear() | 清空数据点 |
| QBarSeries | append(QBarSet *set) | 追加一组数据 |
| QBarSet | operator<<(qreal value) | 追加一个数据 |
| QBarCategoryAxis | append(const QStringList &categories) | 设置分类标签 |
| QPieSeries | append(const QString &label, qreal value) | 追加一个扇区 |
| QPieSlice | setExploded(bool) / setLabelVisible(bool) | 扇区分离 / 显示标签 |
例程路径 Qt/02/04_qtchart。本例基本流程:
QSplineSeries(曲线)、QChart(图表)、QChartView(图表视图);axisX 与 axisY,添加到 chart;splineSeries 与坐标轴联系起来(attachAxis);chart 设置到 chartView;这个例子除了可以绘制静态曲线,也可以绘制动态曲线,稍加改造即可用于实际项目中读取数据绘制实时曲线图。
QT += core gui charts
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
DEFINES += QT_DEPRECATED_WARNINGS
SOURCES += \
main.cpp \
mainwindow.cpp
HEADERS += \
mainwindow.h
关键就是第一行的 charts 模块。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QChartView>
#include <QSplineSeries>
#include <QScatterSeries>
#include <QDebug>
#include <QValueAxis>
#include <QTimer>
#include <QMainWindow>
/* 必须添加命名空间 */
QT_CHARTS_USE_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* 接收数据接口 */
void receivedData(int);
/* 数据最大个数 */
int maxSize;
/* x 轴上的最大值 */
int maxX;
/* y 轴上的最大值 */
int maxY;
/* y 轴 */
QValueAxis *axisY;
/* x 轴 */
QValueAxis *axisX;
/* QList int 类型容器 */
QList<int> data;
/* QSplineSeries 对象(曲线)*/
QSplineSeries *splineSeries;
/* QChart 图表 */
QChart *chart;
/* 图表视图 */
QChartView *chartView;
/* 定时器 */
QTimer *timer;
private slots:
void timerTimeOut();
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include <QDateTime>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置最显示位置与大小 */
this->setGeometry(0, 0, 800, 480);
/* 最大储存 maxSize - 1 个数据 */
maxSize = 51;
/* x 轴上的最大值 */
maxX = 5000;
/* y 轴最大值 */
maxY = 40;
/* splineSeries 曲线实例化(折线用 QLineSeries) */
splineSeries = new QSplineSeries();
/* 图表实例化 */
chart = new QChart();
/* 图表视图实例化 */
chartView = new QChartView();
/* 坐标轴 */
axisY = new QValueAxis();
axisX = new QValueAxis();
/* 定时器 */
timer = new QTimer(this);
/* legend 译图例类型,以绘图的颜色区分,本例设置为隐藏 */
chart->legend()->hide();
/* chart 设置标题 */
chart->setTitle("实时动态曲线示例");
/* 添加一条曲线 splineSeries */
chart->addSeries(splineSeries);
/* 设置显示格式 */
axisY->setLabelFormat("%i");
/* y 轴标题 */
axisY->setTitleText("温度/℃");
/* y 轴标题位置(设置坐标轴的方向) */
chart->addAxis(axisY, Qt::AlignLeft);
/* 设置 y 轴范围 */
axisY->setRange(0, maxY);
/* 将 splineSeries 附加于 y 轴上 */
splineSeries->attachAxis(axisY);
/* 设置显示格式 */
axisX->setLabelFormat("%i");
/* x 轴标题 */
axisX->setTitleText("时间/ms");
/* x 轴标题位置(设置坐标轴的方向) */
chart->addAxis(axisX, Qt::AlignBottom);
/* 设置 x 轴范围 */
axisX->setRange(0, maxX);
/* 将 splineSeries 附加于 x 轴上 */
splineSeries->attachAxis(axisX);
/* 将图表的内容设置在图表视图上 */
chartView->setChart(chart);
/* 设置抗锯齿 */
chartView->setRenderHint(QPainter::Antialiasing);
/* 设置为图表视图为中心部件 */
setCentralWidget(chartView);
/* 定时 200ms */
timer->start(200);
/* 信号槽连接 */
connect(timer, SIGNAL(timeout()), this, SLOT(timerTimeOut()));
/* 设置随机种子,随机数初始化 */
qsrand(time(NULL));
}
MainWindow::~MainWindow()
{
}
void MainWindow::timerTimeOut()
{
/* 产生随机 0~maxY 之间的数据 */
receivedData(qrand() % maxY);
}
void MainWindow::receivedData(int value)
{
/* 将数据添加到 data 中 */
data.append(value);
/* 当储存数据的个数大于最大值时,把第一个数据删除 */
while (data.size() > maxSize) {
/* 移除 data 中第一个数据 */
data.removeFirst();
}
/* 先清空 */
splineSeries->clear();
/* 计算 x 轴上的点与点之间显示的间距 */
int xSpace = maxX / (maxSize - 1);
/* 添加点,xSpace * i 表示第 i 个点的 x 轴的位置 */
for (int i = 0; i < data.size(); ++i) {
splineSeries->append(xSpace * i, data.at(i));
}
}
splineSeries 存数据(点),chart 组织图形(系列 + 坐标轴 + 标题 + 图例),chartView 负责显示。三者必须按「系列 → chart → chartView」的顺序组装。maxSize = 51:因为 xSpace = maxX / (maxSize - 1),所以注释说「最大储存 maxSize - 1 个数据」。实际上 while (data.size() > maxSize) 允许存 maxSize 个,多出的一个会被移除,窗口里稳定显示 51 个点。chart->legend()->hide():图例(legend)用颜色区分不同系列。本例只有一条曲线,隐藏图例让画面更干净。chart->addAxis(axisY, Qt::AlignLeft):把轴加到图表并指定方位(左、右、上、下);axisY->setRange(0, maxY):设置范围;splineSeries->attachAxis(axisY):把系列绑到轴上。缺少第 3 步曲线不会显示,这是最常见的错误。attachAxis:一个系列要显示在二维平面上,必须同时绑定 x 轴和 y 轴。chartView->setRenderHint(QPainter::Antialiasing):曲线抗锯齿,看起来更平滑。setCentralWidget(chartView):把图表视图设为 QMainWindow 的中央部件。qsrand(time(NULL)) + qrand() % maxY:设置随机种子并产生 0~maxY-1 的整数,模拟实时采集到的温度数据。receivedData() 的滑动窗口算法(第 84~105 行,本例核心):
data.append(value) 把新数据加到末尾;while (data.size() > maxSize) data.removeFirst() 在数据超过上限后删除最老的一个数据;splineSeries->clear() 清空旧点;xSpace * i 重新铺 x 坐标,data.at(i) 作为 y 值,把窗口内的数据重新画一遍。
由于每次 x 位置固定、y 值滚动,曲线看起来就在「向左移动」,实现了数据移动的过程。改造为真实数据源:把
qrand() % maxY换成串口/CAN/传感器的读数,把timerTimeOut()换成数据到达信号触发的槽函数,即可用于实际项目的实时曲线。
运行后,窗口中显示标题为「实时动态曲线示例」的曲线图,横轴为「时间/ms」(0~5000),纵轴为「温度/℃」(0~40)。每 200ms 曲线向左滚动一格并出现一个新的随机值,形成实时动态曲线的效果。
⚠️ 来源说明:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书第九章只演示了曲线图,以下为 Qt Charts 官方文档中的标准用法,补充柱状图与饼图以便完整掌握 Qt Charts。
QT_CHARTS_USE_NAMESPACE
/* 一组数据 */
QBarSet *set = new QBarSet("温度");
*set << 12 << 22 << 30 << 25 << 18 << 10;
/* 柱状图系列 */
QBarSeries *series = new QBarSeries();
series->append(set);
QChart *chart = new QChart();
chart->addSeries(series);
chart->setTitle("一天温度变化");
/* 分类横轴 */
QStringList categories;
categories << "6时" << "9时" << "12时" << "15时" << "18时" << "21时";
QBarCategoryAxis *axisX = new QBarCategoryAxis();
axisX->append(categories);
chart->addAxis(axisX, Qt::AlignBottom);
series->attachAxis(axisX);
/* 数值纵轴 */
QValueAxis *axisY = new QValueAxis();
axisY->setRange(0, 35);
axisY->setTitleText("温度/℃");
chart->addAxis(axisY, Qt::AlignLeft);
series->attachAxis(axisY);
QChartView *view = new QChartView(chart);
view->setRenderHint(QPainter::Antialiasing);
QPieSeries *series = new QPieSeries();
series->append("C 语言", 40);
series->append("C++", 30);
series->append("Qt", 20);
series->append("Linux 驱动", 10);
/* 让第二块分离并显示标签 */
QPieSlice *slice = series->slices().at(1);
slice->setExploded(true);
slice->setLabelVisible(true);
QChart *chart = new QChart();
chart->addSeries(series);
chart->setTitle("技能占比");
chart->legend()->setVisible(true);
chart->legend()->setAlignment(Qt::AlignRight);
QChartView *view = new QChartView(chart);
view->setRenderHint(QPainter::Antialiasing);
要点:饼图不需要手动创建坐标轴;柱状图的横轴用 QBarCategoryAxis(分类轴)而不是 QValueAxis。
| 例程 | 额外模块 | .pro 修改 |
|---|---|---|
| 01_qfile | 无(core gui widgets) | QT += core gui |
| 02_qtextstream | 无 | QT += core gui |
| 03_qpainter | 无(用资源文件) | QT += core gui,并加 RESOURCES += res.qrc |
| 04_qtchart | charts | QT += core gui charts |
通用做法:打开 .pro 文件,把第一行改成对应内容;03_qpainter、04_qtchart 的图片/资源需要随工程一起编译。
# 用 Qt Creator 打开 Qt/02/01_qfile/01_qfile.pro,选择 Desktop Qt 套件
# 或命令行编译:
qmake 01_qfile.pro
make -j4
./01_qfile
运行 03_qpainter 前确认 res.qrc 里已包含 image/cd.png;运行 04_qtchart 前确认 .pro 里有 QT += charts。
交叉编译流程与普通 Qt 程序一致(详见交叉编译章节):
arm-linux-gnueabihf)编译;要点:
charts 模块;若交叉编译的 Qt 未编译 Qt Charts,04_qtchart 会链接失败;paintEvent 绘图直接使用 framebuffer / EGLFS,无需额外配置;res.qrc 打包进可执行文件,避免开发板上路径问题。WriteOnly 和 Append 有什么区别?WriteOnly 单独使用时相当于「只写 + 截断」,会清空文件原有内容再从开头写;Append 则把新数据追加到文件末尾,不破坏原有内容。要做「追加日志」必须用 QIODevice::WriteOnly | QIODevice::Append。二者都可以按位或 QIODevice::Text 以文本模式打开。
QFile 与 QTextStream 分别负责什么?为什么常一起用?QFile 继承自 QIODevice,负责打开/关闭/定位文件和读写字节流,返回 QByteArray;QTextStream 负责把字节流当成文本处理,负责编码转换、按行/按词读取、格式化输出,返回 QString。两者一起用时,QFile 管设备、QTextStream 管文本语义,既能处理编码又能格式化和对齐,因此是处理文本文件的常见组合。
paintEvent() 什么时候被调用?为什么旋转动画必须在定时器里调用 update()?paintEvent() 是 QWidget 的 protected 虚函数,在控件首次显示/实例化以及需要重绘时被调用。程序运行过程中如果数据或角度变了但不通知系统,界面不会自己刷新,所以要在定时器槽函数里调用 update(),由 Qt 安排一次重绘事件,从而再次进入 paintEvent()。注意不能在 paintEvent() 里调用 update(),否则会无限重绘。
QPainter 旋转图片时为什么要 translate 两次?QPainter 默认坐标原点是绘图设备左上角 (0,0),而 rotate() 是绕当前原点旋转的。若直接旋转,图片会绕左上角转,位置跑偏。所以先把原点平移到图片中心,再 rotate(),最后把原点平移回原位,这样旋转就围绕图片中心进行。两次 translate 也可以用 save() / restore() 包裹来简化。
最可能是漏了 series->attachAxis(axis)。把坐标轴 addAxis() 到 chart 只是把轴放进图表,系列还必须通过 attachAxis() 分别绑定到 x 轴和 y 轴,且要保证 x、y 两条轴都绑定。其次是:.pro 里没加 QT += charts、头文件没写 QT_CHARTS_USE_NAMESPACE、或者 setRange() 的范围不包含数据点导致曲线画在可见区域外。
内容来源:正点原子《I.MX6U嵌入式Qt开发指南V1.2》第八章 文本读写(8.1 QFile 读写文本、8.2 QTextStream 读写文本)、第九章 绘图与图表(9.1 QPainter 绘图、9.2 QChart 图表);例程 Qt/02/01_qfile、02_qtextstream、03_qpainter、04_qtchart。