06-文本绘图与图表.md 61 KB


title: 文本读写、绘图与图表 tags: [

Qt,
QFile,
QTextStream,
QPainter,
QChart,
QtCharts,
文本读写,
绘图,
图表,
QPen,
QBrush,
QColor,
QIODevice,
IMX6ULL,
嵌入式Qt,

] created: 2026-09-18 updated: 2026-09-18

pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第八章 文本读写、第九章 绘图与图表"

文本读写、绘图与图表

💡 关联知识:[[05-Qt应用开发/04-Qt控件详解]]、[[05-Qt应用开发/05-布局容器与模型视图]]、[[05-Qt应用开发/07-多线程网络多媒体与数据库]];延伸阅读:[[编程学习项目/使用Qt和C++构造UI界面/10 绘图和绘图设备]]、[[编程学习项目/使用Qt和C++构造UI界面/11 文件系统]]

本篇把 Qt 里三类「偏底层但天天要用」的能力放在一起讲:

  1. 文本读写——用 QFile 直接读写文件,或者用 QTextStream 以流的方式读写,这是歌词显示、配置文件、日志、字符设备读写的基础;
  2. 绘图——用 QPainter 在窗口(画布)上画点、线、矩形、圆、文字、图片,并做旋转、缩放等坐标变换,这是自定义控件和界面重绘的基础;
  3. 图表——用 Qt Charts 模块(QChart / QChartView / 各种 Series)画出折线图、曲线图、柱状图、饼图,这是数据可视化的基础。

三者都是 Qt GUI 应用的能力,和具体硬件无关,因此在 Ubuntu 与 I.MX6ULL 开发板上运行效果一致。本篇示例对应 PDF 第二篇提高篇第八章、第九章,例程编号 Qt/02/01_qfile ~ Qt/02/04_qtchart


1. 文本读写概述

程序运行时经常需要读写文本文件,例如:

  • 音乐播放器要读取 Mp3 的歌词文本;
  • 记事本类软件打开 txt、修改后再保存;
  • 保存/恢复用户配置;
  • 在 Linux 下把字符设备当作文件读写(Linux「一切皆文件」),例如第 14.2、14.3 节用 QFile 往字符设备写 '0' / '1' 控制硬件。

Qt 提供了两条主要路线:

路线 核心类 特点 适用场景
直接 I/O QFile(继承 QIODevice 一次读全部 / 按行读 / 直接写字节 小文件、整体读写、字符设备
流式 I/O QTextStream + QFile 支持 << >> 运算符、格式化、编码转换 需要按字段/格式化读写文本

两者的关系是:QFile 负责「打开/关闭/定位/读写字节流」,QTextStream 负责「把字节流解释成文本并按格式处理」。QTextStream 不能独立存在,它必须绑定到一个 QIODevice(如 QFile)、QByteArrayQString 上。

flowchart LR
    subgraph 应用层
        A["QTextStream<br/>格式化 / 编码 / 行读取"]
    end
    subgraph 设备层
        B["QFile<br/>打开 / 关闭 / 定位 / 字节读写"]
    end
    C["磁盘文件 / 字符设备 / 资源文件"]
    A -->|"绑定 QIODevice"| B
    B -->|"read()/write()"| C

本章只讲「简单的文本读写」,目的是掌握基本操作。更复杂的文件系统操作(文件信息、目录遍历)由 QFileInfoQDir 提供。


2. QFile 读写文本

2.1 QFile 类原理

QFile 是一个读写文本文件、二进制文件和 Qt 资源文件的 I/O 设备类。它的核心定位是:把文件抽象成一个可从其中读、可向其写的字节流

要点:

  • QFile 可以单独使用,也可以更方便地和 QTextStreamQDataStream 组合使用;
  • 文件名通常在构造函数里传入,也可以之后用 setFileName() 设置;
  • 路径分隔符只能用 /,不支持 \。无论在 Windows、Linux 还是 Mac,QFile 的文件分隔符都是 /。看到 Windows 用 \,也不能在 QFile 路径里写 \
  • exists() 判断文件是否存在,用 remove() 删除文件(更高级的文件系统操作由 QFileInfoQDir 提供);
  • open() 打开,用 close() 关闭,用 flush() 刷新;
  • 通常用 QDataStreamQTextStream 读写,但也可以调用从 QIODevice 继承来的 read()readLine()readAll()write()
  • QFile 还继承了 getChar()putChar()ungetChar(),一次只处理一个字符;
  • 文件大小由 size() 返回;用 pos() 获取当前文件位置,用 seek() 移动到新位置;到达末尾时 atEnd() 返回 true

2.2 QIODevice::OpenModeFlag 打开模式

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

2.3 QFile 常用 API

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 是否到达末尾

2.4 QFile 操作流程

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();                                 /* 关闭 */

2.5 完整实例 01_qfile:读写文本(难度:简单)

例程路径 Qt/02/01_qfile。功能:点击「打开」按钮弹出系统文件选择框,选中文本文件后把内容读入 QTextEdit;修改内容后点击「关闭」按钮,把 QTextEdit 的内容写回原文件并清空显示。

2.5.1 mainwindow.h

#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

2.5.2 mainwindow.cpp

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

2.5.3 逐段解释

  • 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 版记事本。

2.5.4 程序运行效果

  1. 点击「打开」,弹出系统文件选择窗口,选择项目路径下的 测试.txt
  2. 文件内容显示在 QTextEdit 中,可以直接修改;
  3. 点击「关闭」,修改后的内容写回文件,QTextEdit 被清空;
  4. 再次打开该文件,可以看到修改已经保存。

其他常用读写方式(扩展):

/* 按行读取,适合日志、歌词等大文件 */
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();

3. QTextStream 读写文本

3.1 QTextStream 类原理

QTextStream 类为读写文本提供了一个方便的接口,常与 QFile 结合使用。它可以在 QIODeviceQByteArrayQString 上操作。

它的好处:

  • 使用流操作符 <<(流插入运算符)和 >>(流提取运算符)就能方便地读写单词、行和数字;
  • 支持字段填充和对齐的格式化选项,以及数字进制、精度等格式化;
  • 支持编码设置,可指定读写时使用的文本编码,避免中文乱码。

QTextStream 内部与 C++ 的 iostream 思路一致:把一个「设备」包成一个流,然后用运算符逐项读写。

flowchart LR
    A["QString / QByteArray / QIODevice"] --> B["QTextStream"]
    B -->|"operator<<"| C["写入文本"]
    B -->|"operator>>"| D["读取文本"]
    B -.-> E["编码 / 填充 / 对齐 / 数字格式"]

3.2 编码处理

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 环境。

3.3 格式化选项

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) ShowBaseUppercaseBaseForceSign

带对齐的表格化输出示例:

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 当前位置

3.4 完整实例 02_qtextstream:文本流读写(难度:简单)

例程路径 Qt/02/02_qtextstream。界面与 01_qfile 完全一样,唯一的区别是在 QFile 的基础上叠了一层 QTextStream。下面给出差异代码,其余代码与 01_qfile 相同。

3.4.1 mainwindow.cpp 差异部分

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.h01_qfile 完全一致。

3.4.2 逐段解释

  • 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();
    

3.4.3 程序运行效果

与 8.1.2 小节(即 01_qfile 的运行效果)完全一样:点击打开读入文本、修改后点击关闭写回文件。QFileQTextStream 在简单场景下看起来没有区别。


4. QPainter 绘图

4.1 Qt 绘图体系

Qt 里的所有绘图——包括一个按钮、一个 Label 的显示——都由绘图系统执行。绘图系统基于三个类:

角色 说明
QPainter 画家 / 绘图操作类 提供 drawLine()drawRect()drawText() 等绘图接口
QPaintDevice 画布 / 绘图设备基类 提供绘制对象的基类,代表「能画在哪」
QPaintEngine 绘图引擎 提供一个平台上设备的抽象定义,起「抽象平台」的作用

它们的关系是:QPainter 是直接用来操作绘图的类;QPaintDeviceQPaintEngine 都比 QPainter 更底层,一般只需要了解即可。

flowchart TB
    A["QPainter<br/>直接绘制图像"] --> B["QPainter 通过 QPaintEngine 绘制"]
    B --> C["QPaintEngine<br/>提供平台相关的抽象定义"]
    C --> D["QPaintDevice<br/>提供绘制对象的基类"]
    D --> E["QWidget / QPixmap / QImage / QLabel ..."]

一般用于显示的类,如 QWidgetQPixmapQImageQLabel 等可视类控件都可以充当绘图区域的「画布」。从 QWidget 继承的类都有 virtual void paintEvent(QPaintEvent *event);,这个虚函数在 qwidget.hprotected: 修饰符下。

4.2 paintEvent 机制

什么是绘图事件? 当界面初始化或需要刷新时才会执行的事件。也就是说:

  • 绘图事件在构造对象实例化时会执行一次;
  • 需要刷新界面时,调用 update() 方法触发 paintEvent()

paintEvent() 是父类 QWidget 提供给子类的接口,在父类里定义为空,所以它就是专门给子类画图用的。

重写的基本结构:

void Widget::paintEvent(QPaintEvent *)
{
    /* 指定画图的对象,this 代表是本 Widget */
    QPainter painter(this);

    /* 使用 painter 在对象上绘图... */
}

⚠️ 注意paintEvent()protected 的虚函数,重写时应写在子类的 protected: 区域。不要在 paintEvent() 外部创建 QPainter 绘制到窗口,也不要在 paintEvent() 里调用 update()(会造成无限重绘)。

4.3 画笔、画刷与颜色

QPainter 需要两个「工具」才能画出好看的东西:

工具 作用
画笔 QPen 决定线条(轮廓)的颜色、宽度、线型、端点样式、连接样式
画刷 QBrush 决定封闭图形内部填充的颜色和样式
颜色 QColor QPenQBrush 使用,表示颜色

4.3.1 QColor

构造 / 方法 说明
QColor(Qt::GlobalColor) 用预定义颜色构造,如 Qt::redQt::blueQt::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 */

4.3.2 QPen

API 说明
QPen(const QColor &color) 用颜色构造
QPen(const QBrush &brush, qreal width) 用画刷和线宽构造
setColor(QColor) 设置颜色
setWidth(int) 设置线宽(像素)
setStyle(Qt::PenStyle) 线型:SolidLineDashLineDotLineDashDotLineDashDotDotLineNoPen
setCapStyle(Qt::PenCapStyle) 端点样式:FlatCapSquareCapRoundCap
setJoinStyle(Qt::PenJoinStyle) 连接样式:MiterJoinBevelJoinRoundJoin
QPen pen;
pen.setColor(Qt::blue);
pen.setWidth(5);
pen.setStyle(Qt::DashLine);
pen.setCapStyle(Qt::RoundCap);
painter.setPen(pen);

4.3.3 QBrush

API 说明
QBrush(Qt::GlobalColor) 用预定义颜色构造
QBrush(const QColor &color, Qt::BrushStyle) 用颜色和样式构造
setColor(QColor) 设置颜色
setStyle(Qt::BrushStyle) 填充样式:SolidPatternNoBrushCrossPatternDiagCrossPatternHorPatternVerPatternDense1Pattern~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 的标准用法,此处补充以便实际开发使用。

4.4 QPainter 常用绘制 API

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() 保存 / 恢复画笔状态与坐标变换状态

4.5 坐标变换与状态保存

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();                  /* 恢复坐标系统与画笔 */

4.6 抗锯齿

默认绘制的线条和图形边缘有明显锯齿。开启抗锯齿让曲线、斜线更平滑:

渲染提示 作用
QPainter::Antialiasing 图形边缘抗锯齿
QPainter::TextAntialiasing 文字抗锯齿
QPainter::SmoothPixmapTransform 图片平滑缩放
painter.setRenderHints(QPainter::Antialiasing
                       | QPainter::SmoothPixmapTransform);

QChartView 上也提供了 setRenderHint(QPainter::Antialiasing)

4.7 完整实例 03_qpainter:旋转的 CD(难度:一般)

例程路径 Qt/02/03_qpainter。功能:用一张 CD 图片,在 paintEvent() 里把 CD 画在窗口中心,并且每 100ms 旋转 1 度,看上去 CD 在旋转。

4.7.1 mainwindow.h

#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() 是父类 QWidgetprotected 虚方法,所以建议重写时也写到子类的 protected 修饰符下。

4.7.2 mainwindow.cpp

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

4.7.3 逐段解释

  • setPalette(QPalette(Qt::gray)) + setAutoFillBackground(true):给窗口设置灰色背景。只有 autoFillBackgroundtrue 时,调色板的背景色才会生效。
  • 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.qrcimage/cd.png 注册为 :/image/cd.png,随可执行文件打包。

4.7.4 程序运行效果

编译运行后,窗口中央的 CD 每 100ms 转 1 度,产生连续的旋转动画;CD 外框还画了一个矩形,使旋转更明显。

paintEvent 可以实现一些需要绘图的情景,有时比 Qt 动画类更容易实现;结合 Qt 的画笔,也可以设计一个绘图软件,这都得益于 paintEvent()QPainter。界面重绘时使用 paintEvent() 的场合也比较多。


5. QChart 图表

5.1 Qt Charts 模块

在 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;

5.2 QChart 继承关系

理解继承关系有助于知道这个类「怎么来的」:

flowchart BT
    QObject --> QGraphicsObject
    QGraphicsItem --> QGraphicsObject
    QGraphicsItem --> QGraphicsLayoutItem
    QGraphicsObject --> QGraphicsWidget
    QGraphicsLayoutItem --> QGraphicsWidget
    QGraphicsWidget --> QChart

也就是说 QChart 本质是一个 QGraphicsWidget(它同时继承 QGraphicsObjectQGraphicsLayoutItem),可以像图形项一样被放到 QGraphicsScene 里;而 QChartView 是承载 QChart 的视图控件(继承自 QGraphicsView)。在 Qt Creator 里可用 Ctrl + Shift + T 查看某个类的继承关系。

5.3 Qt Charts 主要类

作用
QChart 图表对象,管理系列(Series)、坐标轴(Axis)、图例(Legend)、标题
QChartView 图表视图,承载并显示 QChart
QAbstractSeries 所有系列的基类
QLineSeries 折线图系列
QSplineSeries 平滑曲线系列
QScatterSeries 散点图系列
QBarSeries / QHorizontalBarSeries 柱状图系列
QStackedBarSeries 堆叠柱状图系列
QBarSet 柱状图的一组数据
QPieSeries / QPieSlice 饼图系列 / 饼图扇区
QAreaSeries 面积图系列
QValueAxis 数值坐标轴
QBarCategoryAxis 分类坐标轴(柱状图横轴)
QDateTimeAxis 时间坐标轴
QLogValueAxis 对数坐标轴
QLegend 图例,用颜色区分不同系列

5.4 QChart 常用 API

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) 扇区分离 / 显示标签

5.5 完整实例 04_qtchart:实时动态曲线(难度:一般)

例程路径 Qt/02/04_qtchart。本例基本流程:

  1. 创建 QSplineSeries(曲线)、QChart(图表)、QChartView(图表视图);
  2. 创建两条坐标轴 axisXaxisY,添加到 chart
  3. splineSeries 与坐标轴联系起来(attachAxis);
  4. chart 设置到 chartView
  5. 数据由系统产生随机数,用定时器定期更新。

这个例子除了可以绘制静态曲线,也可以绘制动态曲线,稍加改造即可用于实际项目中读取数据绘制实时曲线图。

5.5.1 04_qtchart.pro

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 模块。

5.5.2 mainwindow.h

#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

5.5.3 mainwindow.cpp

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

5.5.4 逐段解释

  • 对象分工splineSeries 存数据(点),chart 组织图形(系列 + 坐标轴 + 标题 + 图例),chartView 负责显示。三者必须按「系列 → chart → chartView」的顺序组装。
  • maxSize = 51:因为 xSpace = maxX / (maxSize - 1),所以注释说「最大储存 maxSize - 1 个数据」。实际上 while (data.size() > maxSize) 允许存 maxSize 个,多出的一个会被移除,窗口里稳定显示 51 个点。
  • chart->legend()->hide():图例(legend)用颜色区分不同系列。本例只有一条曲线,隐藏图例让画面更干净。
  • 坐标轴绑定三步
    1. chart->addAxis(axisY, Qt::AlignLeft):把轴加到图表并指定方位(左、右、上、下);
    2. axisY->setRange(0, maxY):设置范围;
    3. splineSeries->attachAxis(axisY):把系列绑到轴上。缺少第 3 步曲线不会显示,这是最常见的错误。
  • x 轴与 y 轴都要各自 attachAxis:一个系列要显示在二维平面上,必须同时绑定 x 轴和 y 轴。
  • chartView->setRenderHint(QPainter::Antialiasing):曲线抗锯齿,看起来更平滑。
  • setCentralWidget(chartView):把图表视图设为 QMainWindow 的中央部件。
  • qsrand(time(NULL)) + qrand() % maxY:设置随机种子并产生 0~maxY-1 的整数,模拟实时采集到的温度数据。
  • receivedData() 的滑动窗口算法(第 84~105 行,本例核心):
    1. data.append(value) 把新数据加到末尾;
    2. while (data.size() > maxSize) data.removeFirst() 在数据超过上限后删除最老的一个数据;
    3. splineSeries->clear() 清空旧点;
    4. xSpace * i 重新铺 x 坐标,data.at(i) 作为 y 值,把窗口内的数据重新画一遍。 由于每次 x 位置固定、y 值滚动,曲线看起来就在「向左移动」,实现了数据移动的过程。

改造为真实数据源:把 qrand() % maxY 换成串口/CAN/传感器的读数,把 timerTimeOut() 换成数据到达信号触发的槽函数,即可用于实际项目的实时曲线。

5.5.5 程序运行效果

运行后,窗口中显示标题为「实时动态曲线示例」的曲线图,横轴为「时间/ms」(0~5000),纵轴为「温度/℃」(0~40)。每 200ms 曲线向左滚动一格并出现一个新的随机值,形成实时动态曲线的效果。

5.6 扩展:柱状图与饼图

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书第九章只演示了曲线图,以下为 Qt Charts 官方文档中的标准用法,补充柱状图与饼图以便完整掌握 Qt Charts。

5.6.1 柱状图 QBarSeries

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

5.6.2 饼图 QPieSeries

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


6. 构建与运行

6.1 所需 Qt 模块

例程 额外模块 .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_qpainter04_qtchart 的图片/资源需要随工程一起编译。

6.2 Ubuntu 上编译运行

# 用 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

6.3 I.MX6ULL 开发板上运行

交叉编译流程与普通 Qt 程序一致(详见交叉编译章节):

  1. 用开发板对应的交叉编译 Qt 套件(如 arm-linux-gnueabihf)编译;
  2. 把生成的可执行文件拷贝到开发板;
  3. 在开发板 Qt 运行环境(含 Qt Charts 库)下执行。

要点:

  • 开发板上的 Qt 必须带 charts 模块;若交叉编译的 Qt 未编译 Qt Charts,04_qtchart 会链接失败;
  • paintEvent 绘图直接使用 framebuffer / EGLFS,无需额外配置;
  • 图片资源建议通过 res.qrc 打包进可执行文件,避免开发板上路径问题。

7. 面试精选

7.1 QFile 的 WriteOnlyAppend 有什么区别?

WriteOnly 单独使用时相当于「只写 + 截断」,会清空文件原有内容再从开头写;Append 则把新数据追加到文件末尾,不破坏原有内容。要做「追加日志」必须用 QIODevice::WriteOnly | QIODevice::Append。二者都可以按位或 QIODevice::Text 以文本模式打开。

7.2 QFileQTextStream 分别负责什么?为什么常一起用?

QFile 继承自 QIODevice,负责打开/关闭/定位文件和读写字节流,返回 QByteArrayQTextStream 负责把字节流当成文本处理,负责编码转换、按行/按词读取、格式化输出,返回 QString。两者一起用时,QFile 管设备、QTextStream 管文本语义,既能处理编码又能格式化和对齐,因此是处理文本文件的常见组合。

7.3 paintEvent() 什么时候被调用?为什么旋转动画必须在定时器里调用 update()

paintEvent()QWidgetprotected 虚函数,在控件首次显示/实例化以及需要重绘时被调用。程序运行过程中如果数据或角度变了但不通知系统,界面不会自己刷新,所以要在定时器槽函数里调用 update(),由 Qt 安排一次重绘事件,从而再次进入 paintEvent()。注意不能在 paintEvent() 里调用 update(),否则会无限重绘。

7.4 QPainter 旋转图片时为什么要 translate 两次?

QPainter 默认坐标原点是绘图设备左上角 (0,0),而 rotate() 是绕当前原点旋转的。若直接旋转,图片会绕左上角转,位置跑偏。所以先把原点平移到图片中心,再 rotate(),最后把原点平移回原位,这样旋转就围绕图片中心进行。两次 translate 也可以用 save() / restore() 包裹来简化。

7.5 Qt Charts 里一条曲线显示不出来,最可能漏了哪一步?

最可能是漏了 series->attachAxis(axis)。把坐标轴 addAxis()chart 只是把轴放进图表,系列还必须通过 attachAxis() 分别绑定到 x 轴和 y 轴,且要保证 x、y 两条轴都绑定。其次是:.pro 里没加 QT += charts、头文件没写 QT_CHARTS_USE_NAMESPACE、或者 setRange() 的范围不包含数据点导致曲线画在可见区域外。


延伸阅读

  • Qt 绘图与绘图设备深入:[[编程学习项目/使用Qt和C++构造UI界面/10 绘图和绘图设备]]
  • Qt 文件系统:[[编程学习项目/使用Qt和C++构造UI界面/11 文件系统]]
  • 控件基础:[[05-Qt应用开发/04-Qt控件详解]]
  • 布局与模型视图:[[05-Qt应用开发/05-布局容器与模型视图]]

内容来源:正点原子《I.MX6U嵌入式Qt开发指南V1.2》第八章 文本读写(8.1 QFile 读写文本、8.2 QTextStream 读写文本)、第九章 绘图与图表(9.1 QPainter 绘图、9.2 QChart 图表);例程 Qt/02/01_qfile02_qtextstream03_qpainter04_qtchart