Day4 教学示例补三个:QDialog 综合 / QLineEdit 小眼睛 / 文件 IO(+顶层构建注册)
- qdialog_showcase:wiki「6 对话框 QDialog」6.2–6.5 综合演示。含两套实现:
· Designer 表单版(dialog.{h,cpp,ui} + resources.qrc + source.gif,QMovie 播 GIF)
· 纯代码版(qt_dialog_showcase.cpp,单文件,QWidget 布满按钮逐个演示 QDialog 行为)
- lineedit_eye_toggle:子类化 QLineEdit,paintEvent 自绘小眼睛、mouseEvent 命中切换
echoMode,并与内置 clear button 动态避让(读 objectName=="clearButton" 子 widget 几何)。
含独立子目录 CMakeLists.txt 供单独打开(顶层 add_subdirectory 不递归)。
- file_io_demo:QFile/QTextStream/QDataStream/QFileInfo/QDir/QTextCodec/QJson 五主题
综合示例,每个按钮产出一个文件落到 demo_output/ 便于课堂手动查看。
构建注册:examples/CMakeLists.txt 用 add_teaching_example 注册前两个纯代码示例,
mandelbrot 多源文件单独 add_executable;calculator_mainwindow_qss 加 themes.qrc 与
AUTORCC;all_teaching_examples 依赖列表同步补齐。
附 lineedit_eye_toggle 设计文档(docs/superpowers/specs)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# =============================================================================
|
||||
# Qt5 文件操作教学示例 —— 独立 CMake 工程
|
||||
#
|
||||
# 本文件供「单独打开 / 独立构建」使用:
|
||||
# cmake -S . -B build -DCMAKE_PREFIX_PATH=<Qt5.14.2> -DCMAKE_CXX_COMPILER=<g++>
|
||||
# cmake --build build
|
||||
#
|
||||
# 仓库统一构建【不走】这里:顶层 CMakeLists.txt 只
|
||||
# add_subdirectory(docs/teaching/examples) 且不递归,统一构建走
|
||||
# examples/CMakeLists.txt 的 add_teaching_example,源文件同为
|
||||
# qt_file_io_demo.cpp(零重复)。本目录不计入仓库「173 目标」计数。
|
||||
#
|
||||
# 说明:本工程为手写规范版(C++17 / cmake 3.16 / 无 ANDROID 噪音),
|
||||
# 区别于其它子目录里 Qt Creator 自动导出的低质量模板。
|
||||
# =============================================================================
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(qt_file_io_demo LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED)
|
||||
message(STATUS "Found Qt5 ${Qt5Core_VERSION_STRING} at ${Qt5Core_DIR}")
|
||||
|
||||
add_executable(qt_file_io_demo qt_file_io_demo.cpp)
|
||||
target_link_libraries(qt_file_io_demo PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)
|
||||
target_compile_options(qt_file_io_demo PRIVATE -Wall -Wextra)
|
||||
@@ -0,0 +1,108 @@
|
||||
# qt_file_io_demo —— Qt5 文件操作教学示例
|
||||
|
||||
一个 `QWidget` 布满按钮,分 5 个主题演示 Qt5 文件操作。每个按钮产生一个文件,落到
|
||||
**可执行文件同目录的 `demo_output/`** 子目录下,界面顶部 `statusLabel` 始终显示绝对路径,
|
||||
方便课堂逐个手动打开查看文件内容。
|
||||
|
||||
> 这是对仓库 `json_io`(控制台、仅内存序列化)的 GUI 补充:把 **QJson + 真实文件 I/O**
|
||||
> 与其它文件操作主题一起,放进一个可点击演示的界面。
|
||||
|
||||
## 构建
|
||||
|
||||
仓库统一构建(走 `examples/CMakeLists.txt` 的 `add_teaching_example`):
|
||||
|
||||
```bash
|
||||
cmake -S . -B build -G Ninja -DBUILD_QT_PART=ON \
|
||||
-DCMAKE_PREFIX_PATH=$PWD/.qt514/5.14.2/gcc_64 -DCMAKE_BUILD_TYPE=Debug
|
||||
cmake --build build --target qt_file_io_demo
|
||||
tools/run_qt.sh qt_file_io_demo
|
||||
```
|
||||
|
||||
单独打开(用本目录的独立 `CMakeLists.txt`):
|
||||
|
||||
```bash
|
||||
cmake -S . -B build -DCMAKE_PREFIX_PATH=<Qt5.14.2> -DCMAKE_CXX_COMPILER=<g++>
|
||||
cmake --build build
|
||||
```
|
||||
|
||||
离屏自动化验证(仿 p03 风格,200ms 自动退出、退出码 0):
|
||||
|
||||
```bash
|
||||
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_file_io_demo
|
||||
```
|
||||
|
||||
## 界面布局
|
||||
|
||||
```
|
||||
┌─ statusLabel(显示绝对路径 + 每个演示的结果摘要)─────────────┐
|
||||
├─ ① 基础文件操作 QFile 写/读/追加、QFileInfo、QDir │
|
||||
├─ ② 字符集编码 UTF-8 / Latin-1 / GBK / UTF-16LE / 乱码对比 │
|
||||
├─ ③ 文本vs二进制 QTextStream / QDataStream / QIODevice::Text │
|
||||
├─ ④ 异常处理 Qt 返回值式 | C++ try/catch(对照) │
|
||||
├─ ⑤ QJson 文件 写 / 读 / 改写 │
|
||||
├─ [在文件管理器中打开 demo_output] [清空 demo_output] │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 各主题要点与产出文件
|
||||
|
||||
### ① 基础文件操作
|
||||
| 按钮 | 产出文件 | 要点 |
|
||||
|---|---|---|
|
||||
| QFile 写文本 | `01_qfile_write.txt` | `QTextStream` 写;**坑点:Qt5 默认 codec 是系统区域编码(中文 Windows 常为 GBK),写中文必须显式 `setCodec("UTF-8")`** |
|
||||
| QFile 读文本 | (读上一个) | 读端同样要 `setCodec("UTF-8")`,否则跨平台乱码 |
|
||||
| 追加 vs 覆盖 | `01_append.txt` | `WriteOnly` 每次清空;`Append` 在末尾追加 |
|
||||
| QFileInfo 信息 | `01_fileinfo.txt` | `exists / size / absoluteFilePath / absolutePath` |
|
||||
| QDir 列目录 | — | `entryList(QDir::Files)` 列出 `demo_output` 内文件 |
|
||||
|
||||
### ② 字符集编码
|
||||
| 按钮 | 产出文件 | 要点 |
|
||||
|---|---|---|
|
||||
| 写 UTF-8 | `02_utf8.txt` | `QString::toUtf8()`:中文每字 3 字节、ASCII 每字 1 字节 |
|
||||
| 写 UTF-8 + BOM | `02_utf8_bom.txt` | 头部 `EF BB BF`(十六进制可见) |
|
||||
| 写 Latin-1 | `02_latin1.txt` | `toLatin1()` 只能表 `U+0000~U+00FF`,中文超出变 `?` |
|
||||
| 写 GBK | `02_gbk.txt` | `QTextCodec::codecForName("GBK")`;**未装 CJK codec 插件时返回 `nullptr`,界面列出当前可用编码并给出提示**。本仓库 MinGW Qt 未含 `plugins/codecs/`,故此按钮走降级提示(这本身就是教学点);如需真正写 GBK,用 Qt Maintenance Tool 勾选安装 codecs 插件,或把 `plugins/codecs/` 拷到 Qt 的 plugins 目录 |
|
||||
| 写 UTF-16LE | `02_utf16le.txt` | 手动写 `FF FE` BOM + 每字符固定 2 字节(低字节在前) |
|
||||
| 乱码:错码读回 | (读 `02_utf8.txt`) | UTF-8 字节用 Latin-1 强解 → mojibake,说明「读写编码须一致」 |
|
||||
|
||||
> 用编辑器(VS Code / Notepad++)切换编码打开这些文件,可直观看到「同一段中文在不同编码下的字节差异」与乱码成因。
|
||||
|
||||
### ③ 文本方式 vs 二进制方式
|
||||
| 按钮 | 产出文件 | 要点 |
|
||||
|---|---|---|
|
||||
| 文本写结构 | `03_struct_text.txt` | `QTextStream` 写 `name=张三 / age=20 / score=95.5`:可读但格式松散 |
|
||||
| 二进制写结构 | `03_struct_bin.dat` | `QDataStream` 写 `qint32 + double + QString`:紧凑、带类型、可精确还原(程序内立即读回验证) |
|
||||
| 对比大小 | — | `QFileInfo` 报告两文件字节数;用十六进制工具看 `.dat` 内部布局 |
|
||||
| QIODevice::Text 换行 | `03_nl_text.txt` / `03_nl_bin.txt` | Windows 下 `Text` 标志把 `\n` 转 `\r\n`,不加则原样 `\n`;Linux 下两者相同 |
|
||||
|
||||
> `QDataStream` **务必 `setVersion(Qt_5_14)`**,否则 Qt 升级后二进制格式可能不兼容。
|
||||
|
||||
### ④ 异常与错误处理(两范式对照)
|
||||
| 按钮 | 要点 |
|
||||
|---|---|
|
||||
| Qt: 打开不存在文件 | **Qt 范式**:`open()` 返回 `false` + `error()` / `errorString()`,不抛异常 |
|
||||
| Qt: 只读模式写 | 只读 `QFile` 调 `write()` 返回 `-1` + `errorString()` |
|
||||
| C++: ifstream 异常 | **C++ 范式**:`in.exceptions(failbit\|badbit)` 后打开失败抛 `std::ios_base::failure`,`try/catch` 捕获 |
|
||||
| C++: throw + catch | 自定义异常 + `catch` 顺序(派生类在前、基类在后) |
|
||||
| JSON: 损坏解析 | `04_broken.json`:`QJsonDocument::fromJson` 不抛异常,用 `QJsonParseError` 拿出错偏移与描述 |
|
||||
|
||||
> 核心对照:**Qt 几乎不用 C++ 异常**,错误经返回值/API 暴露;标准库流默认也不抛,需显式开启。
|
||||
> 教学 LLVM 式「异常 vs 返回值」两种错误处理哲学的绝佳切入点。
|
||||
|
||||
### ⑤ QJson 文件读写
|
||||
| 按钮 | 产出文件 | 要点 |
|
||||
|---|---|---|
|
||||
| 写 JSON 文件 | `05_student.json` | `QJsonObject` + `QJsonArray` → `QJsonDocument::toJson(Indented)` → 写文件 |
|
||||
| 读 JSON 文件 | (读上一个) | `fromJson` → `object()` → 按键取值,`toArray()` 遍历数组 |
|
||||
| 修改后回写 | (覆盖上一个) | 改 `score`、增 `grade`、删 `enrolled`,覆盖回写 |
|
||||
|
||||
## demo_output 在哪?
|
||||
|
||||
`QCoreApplication::applicationDirPath() + "/demo_output"` —— 即**可执行文件旁边**。
|
||||
`tools/run_qt.sh` 运行时通常在 `build/` 下,所以路径形如 `build/qt_file_io_demo` 同级。
|
||||
界面底部「在文件管理器中打开 demo_output」按钮可一键定位。
|
||||
|
||||
## 相关课程点
|
||||
|
||||
对应「C++ 方向 Day3」文件与数据格式章节;与 `json_io`(控制台 QJson 内存演示)、
|
||||
`p03/ch11`(wiki 的 QFile/QTextStream/QDataStream)互为补充。
|
||||
@@ -0,0 +1,500 @@
|
||||
// ============================================================================
|
||||
// qt_file_io_demo —— Qt5 文件操作教学综合示例(非 wiki 生成,手工维护)
|
||||
//
|
||||
// 覆盖 5 个主题,每组按钮对应一个场景;所有演示文件落到可执行文件同目录的
|
||||
// demo_output/ 子目录,statusLabel 始终显示绝对路径,方便课堂手动打开查看:
|
||||
// ① 基础文件操作:QFile 写/读/追加、QFileInfo、QDir 列目录
|
||||
// ② 字符集编码:UTF-8 / Latin-1 / GBK / UTF-16LE + BOM + 「错码读回」乱码对比
|
||||
// ③ 文本方式 vs 二进制方式:QTextStream 文本、QDataStream 二进制、QIODevice::Text
|
||||
// ④ 异常与错误处理:Qt 返回值式(open/errorString)+ C++ try/catch(ifstream 异常)两范式
|
||||
// ⑤ QJson 文件读写:构造→落盘 / 读盘→解析 / 改写 / 损坏解析
|
||||
//
|
||||
// 形态:一个 QWidget 布满按钮(仿 qt_dialog_showcase),纯代码、单文件。
|
||||
// 离屏验证:QT_QPA_PLATFORM=offscreen 时 200ms 自动退出(仿 p03 风格,不点按钮
|
||||
// 不触发任何文件写入)。
|
||||
//
|
||||
// 编码演示设计要点:优先用 QString::toUtf8()/toLatin1()(Qt 内置、不依赖 CJK codec
|
||||
// 插件),GBK 通过 QTextCodec 尝试,未安装 codec 则给出明确提示并优雅降级 —— 这
|
||||
// 本身就是 Linux 上精简 Qt 安装的真实坑点。
|
||||
// ============================================================================
|
||||
#include <utility>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QGridLayout>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QFrame>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QDataStream>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QList>
|
||||
#include <QDateTime>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonParseError>
|
||||
#include <QTextCodec>
|
||||
#include <QDesktopServices>
|
||||
#include <QUrl>
|
||||
#include <QTimer>
|
||||
|
||||
#include <fstream> // std::ifstream(C++ 异常演示)
|
||||
#include <ios> // std::ios_base::failure
|
||||
#include <stdexcept> // std::runtime_error
|
||||
#include <exception>
|
||||
|
||||
class FileIoDemo : public QWidget {
|
||||
public:
|
||||
explicit FileIoDemo(QWidget *parent = nullptr) : QWidget(parent) {
|
||||
// demo_output 放在可执行文件同目录,路径稳定可预测;statusLabel 始终显示绝对路径。
|
||||
m_dir = QCoreApplication::applicationDirPath() + QStringLiteral("/demo_output");
|
||||
QDir().mkpath(m_dir);
|
||||
|
||||
auto *root = new QVBoxLayout(this);
|
||||
|
||||
m_status = new QLabel(
|
||||
QStringLiteral("演示文件目录:%1\n点击下方按钮,每个按钮产生一个文件,可手动打开查看。")
|
||||
.arg(m_dir));
|
||||
m_status->setWordWrap(true);
|
||||
m_status->setMinimumHeight(54);
|
||||
m_status->setFrameShape(QFrame::Box);
|
||||
root->addWidget(m_status);
|
||||
|
||||
buildBasicGroup(root); // ①
|
||||
buildEncodingGroup(root); // ②
|
||||
buildTextBinGroup(root); // ③
|
||||
buildErrorGroup(root); // ④
|
||||
buildJsonGroup(root); // ⑤
|
||||
|
||||
// 底部:打开 / 清空 demo_output
|
||||
auto *bottom = new QHBoxLayout;
|
||||
auto *btnOpen = new QPushButton(QStringLiteral("在文件管理器中打开 demo_output"), this);
|
||||
auto *btnClear = new QPushButton(QStringLiteral("清空 demo_output"), this);
|
||||
bottom->addWidget(btnOpen);
|
||||
bottom->addWidget(btnClear);
|
||||
root->addLayout(bottom);
|
||||
connect(btnOpen, &QPushButton::clicked, this, &FileIoDemo::openDemoDir);
|
||||
connect(btnClear, &QPushButton::clicked, this, &FileIoDemo::clearDemoDir);
|
||||
|
||||
setWindowTitle(QStringLiteral("Qt5 文件操作教学示例(5 主题 × 多场景)"));
|
||||
resize(700, 760);
|
||||
}
|
||||
|
||||
private:
|
||||
QLabel *m_status = nullptr;
|
||||
QString m_dir; // demo_output 绝对路径
|
||||
|
||||
// ---- 通用 helpers --------------------------------------------------------
|
||||
template <typename F>
|
||||
void addBtn(QGridLayout *g, int row, int col, const QString &text, F &&slot) {
|
||||
auto *btn = new QPushButton(text, this);
|
||||
g->addWidget(btn, row, col);
|
||||
connect(btn, &QPushButton::clicked, this, std::forward<F>(slot));
|
||||
}
|
||||
|
||||
void setStatus(const QString &s) { m_status->setText(s); }
|
||||
|
||||
QString path(const QString &fname) const { return m_dir + QLatin1Char('/') + fname; }
|
||||
|
||||
// 写原始字节;extra 可叠加 QIODevice::Text / QIODevice::Append
|
||||
bool writeBytes(const QString &fname, const QByteArray &bytes,
|
||||
QIODevice::OpenMode extra = QIODevice::NotOpen) {
|
||||
QFile f(path(fname));
|
||||
if (!f.open(QIODevice::WriteOnly | extra)) {
|
||||
setStatus(QStringLiteral("写入失败:%1\n错误:%2").arg(path(fname), f.errorString()));
|
||||
return false;
|
||||
}
|
||||
f.write(bytes);
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 读原始字节
|
||||
bool readBytes(const QString &fname, QByteArray *out,
|
||||
QIODevice::OpenMode extra = QIODevice::NotOpen) {
|
||||
QFile f(path(fname));
|
||||
if (!f.open(QIODevice::ReadOnly | extra)) {
|
||||
setStatus(QStringLiteral("读取失败:%1\n错误:%2").arg(path(fname), f.errorString()));
|
||||
return false;
|
||||
}
|
||||
*out = f.readAll();
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
// 汇报某文件信息(名称/大小/绝对路径),可选附注,用于 statusLabel
|
||||
QString report(const QString &fname, const QString ¬e = QString()) {
|
||||
QFileInfo fi(path(fname));
|
||||
const QString base = QStringLiteral("✅ %1(%2 字节)\n绝对路径:%3")
|
||||
.arg(fname).arg(fi.size()).arg(fi.absoluteFilePath());
|
||||
return note.isEmpty() ? base : base + QStringLiteral("\n") + note;
|
||||
}
|
||||
|
||||
// ---- ① 基础文件操作 ------------------------------------------------------
|
||||
void buildBasicGroup(QVBoxLayout *root) {
|
||||
auto *g = new QGroupBox(QStringLiteral("① 基础文件操作(QFile / QTextStream / QFileInfo / QDir)"));
|
||||
auto *l = new QGridLayout(g);
|
||||
addBtn(l, 0, 0, QStringLiteral("QFile 写文本"), [this] { basicWriteText(); });
|
||||
addBtn(l, 0, 1, QStringLiteral("QFile 读文本"), [this] { basicReadText(); });
|
||||
addBtn(l, 0, 2, QStringLiteral("追加 vs 覆盖"), [this] { basicAppend(); });
|
||||
addBtn(l, 1, 0, QStringLiteral("QFileInfo 信息"), [this] { basicFileInfo(); });
|
||||
addBtn(l, 1, 1, QStringLiteral("QDir 列目录"), [this] { basicDirList(); });
|
||||
root->addWidget(g);
|
||||
}
|
||||
void basicWriteText() {
|
||||
// 坑点:Qt5 的 QTextStream 默认 codec 是「系统区域编码」(中文 Windows 常为 GBK),
|
||||
// 不是 UTF-8。写中文必须显式 setCodec("UTF-8"),否则文件编码随系统而变。
|
||||
QFile f(path("01_qfile_write.txt"));
|
||||
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
setStatus(QStringLiteral("打开失败:%1").arg(f.errorString())); return;
|
||||
}
|
||||
QTextStream out(&f);
|
||||
out.setCodec(QTextCodec::codecForName("UTF-8"));
|
||||
out << QStringLiteral("第一行:Qt5 文件操作\n");
|
||||
out << QStringLiteral("第二行:当前时间 ") << QDateTime::currentDateTime().toString(Qt::ISODate) << '\n';
|
||||
out << QStringLiteral("第三行:中文测试 —— 你好,世界\n");
|
||||
f.close();
|
||||
setStatus(report("01_qfile_write.txt", QStringLiteral("用 QTextStream 写,显式 setCodec UTF-8。")));
|
||||
}
|
||||
void basicReadText() {
|
||||
QFile f(path("01_qfile_write.txt"));
|
||||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
setStatus(QStringLiteral("读取失败(请先点「QFile 写文本」):%1").arg(f.errorString())); return;
|
||||
}
|
||||
QTextStream in(&f);
|
||||
in.setCodec(QTextCodec::codecForName("UTF-8"));
|
||||
const QString all = in.readAll();
|
||||
f.close();
|
||||
setStatus(QStringLiteral("读到内容(%1 个字符):\n%2").arg(all.length()).arg(all));
|
||||
}
|
||||
void basicAppend() {
|
||||
// WriteOnly 每次清空覆盖;Append 在末尾追加、保留原内容
|
||||
QFile f(path("01_append.txt"));
|
||||
if (!f.open(QIODevice::WriteOnly | QIODevice::Append)) { setStatus(f.errorString()); return; }
|
||||
QTextStream out(&f);
|
||||
out.setCodec(QTextCodec::codecForName("UTF-8"));
|
||||
out << QStringLiteral("append @ ")
|
||||
<< QDateTime::currentDateTime().toString(Qt::ISODateWithMs) << '\n';
|
||||
f.close();
|
||||
|
||||
QFile r(path("01_append.txt"));
|
||||
r.open(QIODevice::ReadOnly | QIODevice::Text);
|
||||
setStatus(report("01_append.txt",
|
||||
QStringLiteral("当前全部内容:\n") + QString::fromUtf8(r.readAll())));
|
||||
}
|
||||
void basicFileInfo() {
|
||||
writeBytes("01_fileinfo.txt", "hello\n");
|
||||
const QFileInfo fi(path("01_fileinfo.txt"));
|
||||
setStatus(QStringLiteral("文件名:%1\n存在:%2 大小:%3 字节\n绝对路径:%4\n所在目录:%5")
|
||||
.arg(fi.fileName())
|
||||
.arg(fi.exists() ? QStringLiteral("是") : QStringLiteral("否"))
|
||||
.arg(fi.size()).arg(fi.absoluteFilePath()).arg(fi.absolutePath()));
|
||||
}
|
||||
void basicDirList() {
|
||||
const QDir d(m_dir);
|
||||
const QStringList files = d.entryList(QDir::Files, QDir::Name);
|
||||
setStatus(QStringLiteral("demo_output 内文件数:%1\n%2")
|
||||
.arg(files.size()).arg(files.join('\n')));
|
||||
}
|
||||
|
||||
// ---- ② 字符集编码 --------------------------------------------------------
|
||||
void buildEncodingGroup(QVBoxLayout *root) {
|
||||
auto *g = new QGroupBox(QStringLiteral("② 字符集编码(UTF-8 / Latin-1 / GBK / UTF-16LE + BOM + 乱码)"));
|
||||
auto *l = new QGridLayout(g);
|
||||
addBtn(l, 0, 0, QStringLiteral("写 UTF-8"), [this] { encUtf8(); });
|
||||
addBtn(l, 0, 1, QStringLiteral("写 UTF-8 + BOM"), [this] { encUtf8Bom(); });
|
||||
addBtn(l, 0, 2, QStringLiteral("写 Latin-1"), [this] { encLatin1(); });
|
||||
addBtn(l, 1, 0, QStringLiteral("写 GBK"), [this] { encGbk(); });
|
||||
addBtn(l, 1, 1, QStringLiteral("写 UTF-16LE"), [this] { encUtf16Le(); });
|
||||
addBtn(l, 1, 2, QStringLiteral("乱码:错码读回"), [this] { encMismatch(); });
|
||||
root->addWidget(g);
|
||||
}
|
||||
void encUtf8() {
|
||||
const QString s = QStringLiteral("你好,Qt\tASCII: Hello\n"); // 含中文 + ASCII + 制表符
|
||||
writeBytes("02_utf8.txt", s.toUtf8());
|
||||
setStatus(report("02_utf8.txt", QStringLiteral("toUtf8():中文每字 3 字节,ASCII 每字 1 字节。")));
|
||||
}
|
||||
void encUtf8Bom() {
|
||||
QByteArray b;
|
||||
b.append((char)0xEF).append((char)0xBB).append((char)0xBF); // UTF-8 BOM = EF BB BF
|
||||
b.append(QStringLiteral("带 BOM 的 UTF-8\n").toUtf8());
|
||||
writeBytes("02_utf8_bom.txt", b);
|
||||
setStatus(report("02_utf8_bom.txt", QStringLiteral("头部 3 字节 EF BB BF 即 UTF-8 BOM(十六进制可见)。")));
|
||||
}
|
||||
void encLatin1() {
|
||||
// Latin-1(ISO-8859-1)只能表示 U+0000~U+00FF;中文超出范围,toLatin1() 会替换成 '?'
|
||||
const QString s = QStringLiteral("Hello 世界");
|
||||
writeBytes("02_latin1.txt", s.toLatin1());
|
||||
setStatus(report("02_latin1.txt",
|
||||
QStringLiteral("toLatin1() 把超出 0xFF 的「世界」替换成 '?',文件里只剩 ASCII 部分。")));
|
||||
}
|
||||
void encGbk() {
|
||||
// GBK 是中文 Windows 默认编码,需 Qt 的 CJK codec 插件(plugins/codecs)。
|
||||
// 精简 Qt 安装常不含该插件,codecForName 返回 nullptr —— 这里优雅降级,
|
||||
// 并列出当前实际可用的编码,让学生看清「为什么 GBK 用不了」。
|
||||
QTextCodec *gbk = QTextCodec::codecForName("GBK");
|
||||
if (!gbk) {
|
||||
const QList<QByteArray> codecs = QTextCodec::availableCodecs();
|
||||
QStringList names;
|
||||
for (const QByteArray &c : codecs) names << QString::fromLatin1(c);
|
||||
setStatus(QStringLiteral("⚠️ 当前 Qt 构建未提供 GBK codec 插件\n"
|
||||
"(QTextCodec::codecForName(\"GBK\") 返回 nullptr)。\n"
|
||||
"这是真实坑点:精简 Qt 安装常不含 CJK codec 插件。\n"
|
||||
"当前可用编码(%1 个):%2\n"
|
||||
"可改用 toUtf8()/toLatin1()(内置)演示编码原理,见其余按钮。")
|
||||
.arg(names.size()).arg(names.join(QStringLiteral(", "))));
|
||||
return;
|
||||
}
|
||||
const QString s = QStringLiteral("你好,GBK 编码\n");
|
||||
writeBytes("02_gbk.txt", gbk->fromUnicode(s));
|
||||
setStatus(report("02_gbk.txt",
|
||||
QStringLiteral("GBK:中文每字 2 字节。用编辑器以 GBK 打开才正常,以 UTF-8 打开会乱码。")));
|
||||
}
|
||||
void encUtf16Le() {
|
||||
const QString s = QStringLiteral("UTF-16LE 你好\n");
|
||||
QByteArray b;
|
||||
b.append((char)0xFF).append((char)0xFE); // UTF-16LE BOM = FF FE
|
||||
const QChar *p = s.unicode();
|
||||
for (int i = 0; i < s.length(); ++i) {
|
||||
const ushort u = p[i].unicode();
|
||||
b.append((char)(u & 0xFF)); // 低字节在前 = Little Endian
|
||||
b.append((char)((u >> 8) & 0xFF));
|
||||
}
|
||||
writeBytes("02_utf16le.txt", b);
|
||||
setStatus(report("02_utf16le.txt", QStringLiteral("FF FE 为 UTF-16LE BOM;之后每字符固定 2 字节。")));
|
||||
}
|
||||
void encMismatch() {
|
||||
if (!QFile::exists(path("02_utf8.txt"))) encUtf8();
|
||||
QByteArray raw;
|
||||
if (!readBytes("02_utf8.txt", &raw)) return;
|
||||
// 故意用 Latin-1 解码 UTF-8 字节:多字节中文被拆成多个单字节字符 = 乱码(mojibake)
|
||||
const QString mojibake = QString::fromLatin1(raw);
|
||||
setStatus(QStringLiteral("02_utf8.txt 用 Latin-1 强行解码(编码不匹配)→ 乱码:\n%1\n"
|
||||
"结论:读文件必须用与写入一致的编码。").arg(mojibake));
|
||||
}
|
||||
|
||||
// ---- ③ 文本方式 vs 二进制方式 -------------------------------------------
|
||||
void buildTextBinGroup(QVBoxLayout *root) {
|
||||
auto *g = new QGroupBox(
|
||||
QStringLiteral("③ 文本方式 vs 二进制方式(QTextStream / QIODevice::Text / QDataStream)"));
|
||||
auto *l = new QGridLayout(g);
|
||||
addBtn(l, 0, 0, QStringLiteral("文本写结构"), [this] { tbStructText(); });
|
||||
addBtn(l, 0, 1, QStringLiteral("二进制写结构"), [this] { tbStructBin(); });
|
||||
addBtn(l, 0, 2, QStringLiteral("对比大小"), [this] { tbCompare(); });
|
||||
addBtn(l, 1, 0, QStringLiteral("QIODevice::Text 换行"), [this] { tbNewline(); });
|
||||
root->addWidget(g);
|
||||
}
|
||||
void tbStructText() {
|
||||
// 文本方式:人可读,但格式松散、解析需约定分隔符、浮点精度受 << 默认值影响
|
||||
QFile f(path("03_struct_text.txt"));
|
||||
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { setStatus(f.errorString()); return; }
|
||||
QTextStream out(&f);
|
||||
out.setCodec(QTextCodec::codecForName("UTF-8"));
|
||||
out << "name=张三\nage=20\nscore=95.5\n";
|
||||
f.close();
|
||||
setStatus(report("03_struct_text.txt", QStringLiteral("文本:可读,但程序解析需自己处理格式。")));
|
||||
}
|
||||
void tbStructBin() {
|
||||
// 二进制方式(QDataStream):紧凑、带类型长度、可精确还原;是 Qt 推荐的跨平台二进制格式
|
||||
QFile f(path("03_struct_bin.dat"));
|
||||
if (!f.open(QIODevice::WriteOnly)) { setStatus(f.errorString()); return; }
|
||||
QDataStream out(&f);
|
||||
out.setVersion(QDataStream::Qt_5_14); // 固定版本,避免 Qt 升级后格式不兼容
|
||||
out.setByteOrder(QDataStream::LittleEndian);
|
||||
out << qint32(20) << double(95.5) << QStringLiteral("张三");
|
||||
f.close();
|
||||
|
||||
// 立即读回验证二进制可精确还原
|
||||
QFile r(path("03_struct_bin.dat"));
|
||||
r.open(QIODevice::ReadOnly);
|
||||
QDataStream in(&r);
|
||||
in.setVersion(QDataStream::Qt_5_14);
|
||||
in.setByteOrder(QDataStream::LittleEndian);
|
||||
qint32 age = 0; double score = 0; QString rname;
|
||||
in >> age >> score >> rname;
|
||||
r.close();
|
||||
setStatus(report("03_struct_bin.dat",
|
||||
QStringLiteral("读回验证 → age=%1 score=%2 name=%3(二进制可精确还原)")
|
||||
.arg(age).arg(score, 0, 'f', 1).arg(rname)));
|
||||
}
|
||||
void tbCompare() {
|
||||
if (!QFile::exists(path("03_struct_text.txt"))) tbStructText();
|
||||
if (!QFile::exists(path("03_struct_bin.dat"))) tbStructBin();
|
||||
const QFileInfo ft(path("03_struct_text.txt")), fb(path("03_struct_bin.dat"));
|
||||
setStatus(QStringLiteral("文本 03_struct_text.txt:%1 字节\n二进制 03_struct_bin.dat:%2 字节\n"
|
||||
"用十六进制工具看 .dat:int=4B、double=8B、QString 带长度前缀 + UTF-16。")
|
||||
.arg(ft.size()).arg(fb.size()));
|
||||
}
|
||||
void tbNewline() {
|
||||
// QIODevice::Text:Windows 上写 '\n' 会被转成 "\r\n";Linux 上不转换。
|
||||
// 不加 Text 标志(纯二进制写法)则始终原样 '\n'。这是跨平台文本文件的经典差异。
|
||||
const QByteArray body = "AAAA\nBBBB\nCCCC\n";
|
||||
writeBytes("03_nl_text.txt", body, QIODevice::Text); // 文本模式
|
||||
writeBytes("03_nl_bin.txt", body); // 二进制模式(无转换)
|
||||
setStatus(QStringLiteral("已写 03_nl_text.txt(QIODevice::Text)与 03_nl_bin.txt(无 Text)。\n"
|
||||
"Windows 下前者含 \\r\\n、后者只 \\n;Linux 下两者相同(平台差异,见注释)。"));
|
||||
}
|
||||
|
||||
// ---- ④ 异常与错误处理 ----------------------------------------------------
|
||||
void buildErrorGroup(QVBoxLayout *root) {
|
||||
auto *g = new QGroupBox(
|
||||
QStringLiteral("④ 异常与错误处理(Qt 返回值式 | C++ try/catch 两范式对照)"));
|
||||
auto *l = new QGridLayout(g);
|
||||
addBtn(l, 0, 0, QStringLiteral("Qt: 打开不存在文件"), [this] { errQtOpenMissing(); });
|
||||
addBtn(l, 0, 1, QStringLiteral("Qt: 只读模式写"), [this] { errQtReadOnlyWrite(); });
|
||||
addBtn(l, 0, 2, QStringLiteral("C++: ifstream 异常"), [this] { errCppIfstream(); });
|
||||
addBtn(l, 1, 0, QStringLiteral("C++: throw + catch"), [this] { errCppThrow(); });
|
||||
addBtn(l, 1, 1, QStringLiteral("JSON: 损坏解析"), [this] { errJsonBroken(); });
|
||||
root->addWidget(g);
|
||||
}
|
||||
void errQtOpenMissing() {
|
||||
// Qt 范式:不抛异常,靠 open() 返回值 + errorString()
|
||||
QFile f(path("这个文件不存在.txt"));
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
f.close();
|
||||
setStatus(QStringLiteral("意外:文件竟存在。"));
|
||||
} else {
|
||||
setStatus(QStringLiteral("【Qt 返回值式】open() 返回 false\n错误码:%1\n错误描述:%2\n"
|
||||
"(Qt 设计上几乎不用 C++ 异常,错误经返回值/API 暴露)")
|
||||
.arg(f.error()).arg(f.errorString()));
|
||||
}
|
||||
}
|
||||
void errQtReadOnlyWrite() {
|
||||
writeBytes("04_readonly.txt", "data\n");
|
||||
QFile f(path("04_readonly.txt"));
|
||||
f.open(QIODevice::ReadOnly); // 故意只读
|
||||
const qint64 n = f.write("try overwrite"); // 只读模式下写
|
||||
const QString es = f.errorString();
|
||||
f.close();
|
||||
setStatus(QStringLiteral("【Qt 返回值式】只读模式下 write() 返回 %1(-1 表示失败)\nerrorString():%2")
|
||||
.arg(n).arg(es));
|
||||
}
|
||||
void errCppIfstream() {
|
||||
// C++ 范式:标准库流默认不抛异常;调用 exceptions() 开启后,打开失败抛 std::ios_base::failure
|
||||
std::ifstream in;
|
||||
in.exceptions(std::ios::failbit | std::ios::badbit);
|
||||
try {
|
||||
in.open(path("这个文件不存在.txt").toStdString()); // 不存在 → failbit → 抛异常
|
||||
setStatus(QStringLiteral("意外:文件竟存在。"));
|
||||
} catch (const std::ios_base::failure &e) {
|
||||
setStatus(QStringLiteral("【C++ 异常式】捕获 std::ios_base::failure\nwhat():%1\n"
|
||||
"(对比:Qt 的 QFile 同样情况只返回 false,不抛异常)")
|
||||
.arg(QString::fromStdString(e.what())));
|
||||
} catch (const std::exception &e) {
|
||||
setStatus(QStringLiteral("捕获 std::exception:%1").arg(QString::fromStdString(e.what())));
|
||||
}
|
||||
}
|
||||
void errCppThrow() {
|
||||
// 演示自定义异常 + 多层 catch 顺序(派生类在前、基类在后)
|
||||
try {
|
||||
throw std::runtime_error("演示:文件内容校验失败,主动抛出");
|
||||
} catch (const std::runtime_error &e) {
|
||||
setStatus(QStringLiteral("【C++ 异常式】捕获 std::runtime_error:\n%1\n"
|
||||
"要点:catch 顺序从派生类到基类,否则被基类提前吞掉。")
|
||||
.arg(QString::fromStdString(e.what())));
|
||||
}
|
||||
}
|
||||
void errJsonBroken() {
|
||||
// 先写一段语法错误的 JSON(键未加引号、对象未闭合)
|
||||
writeBytes("04_broken.json", "{ name: \"张三\", score: 95 ");
|
||||
QByteArray raw;
|
||||
if (!readBytes("04_broken.json", &raw)) return;
|
||||
QJsonParseError err;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
|
||||
if (doc.isNull()) {
|
||||
setStatus(QStringLiteral("【JSON 解析】失败(QJsonDocument 不抛异常)。\n"
|
||||
"错误偏移:%1\n错误描述:%2\n"
|
||||
"(JSON 解析用 QJsonParseError 拿精确出错位置,而非 try/catch)")
|
||||
.arg(err.offset).arg(err.errorString()));
|
||||
} else {
|
||||
setStatus(QStringLiteral("意外:JSON 竟合法。"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- ⑤ QJson 文件读写 ----------------------------------------------------
|
||||
void buildJsonGroup(QVBoxLayout *root) {
|
||||
auto *g = new QGroupBox(QStringLiteral("⑤ QJson 文件读写(QJsonDocument / Object / Array)"));
|
||||
auto *l = new QGridLayout(g);
|
||||
addBtn(l, 0, 0, QStringLiteral("写 JSON 文件"), [this] { jsonWrite(); });
|
||||
addBtn(l, 0, 1, QStringLiteral("读 JSON 文件"), [this] { jsonRead(); });
|
||||
addBtn(l, 1, 0, QStringLiteral("修改后回写"), [this] { jsonModify(); });
|
||||
root->addWidget(g);
|
||||
}
|
||||
void jsonWrite() {
|
||||
QJsonObject student;
|
||||
student["name"] = QStringLiteral("张三");
|
||||
student["score"] = 92;
|
||||
student["enrolled"] = true;
|
||||
QJsonArray tags;
|
||||
tags.append(QStringLiteral("Qt"));
|
||||
tags.append(QStringLiteral("C++"));
|
||||
tags.append(QStringLiteral("多线程"));
|
||||
student["tags"] = tags;
|
||||
const QJsonDocument doc(student);
|
||||
writeBytes("05_student.json", doc.toJson(QJsonDocument::Indented));
|
||||
setStatus(report("05_student.json",
|
||||
QStringLiteral("QJsonObject → QJsonDocument → toJson(Indented) 缩进美化。")));
|
||||
}
|
||||
void jsonRead() {
|
||||
if (!QFile::exists(path("05_student.json"))) jsonWrite();
|
||||
QByteArray raw;
|
||||
if (!readBytes("05_student.json", &raw)) return;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(raw);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
setStatus(QStringLiteral("解析失败,请先点「写 JSON 文件」。")); return;
|
||||
}
|
||||
const QJsonObject obj = doc.object();
|
||||
const QJsonArray tags = obj["tags"].toArray();
|
||||
QStringList tagList;
|
||||
for (const QJsonValue &v : tags) tagList << v.toString();
|
||||
setStatus(QStringLiteral("读 05_student.json:\nname = %1\nscore = %2\nenrolled = %3\ntags = [%4]")
|
||||
.arg(obj["name"].toString())
|
||||
.arg(obj["score"].toInt())
|
||||
.arg(obj["enrolled"].toBool() ? QStringLiteral("true") : QStringLiteral("false"))
|
||||
.arg(tagList.join(", ")));
|
||||
}
|
||||
void jsonModify() {
|
||||
if (!QFile::exists(path("05_student.json"))) jsonWrite();
|
||||
QByteArray raw;
|
||||
if (!readBytes("05_student.json", &raw)) return;
|
||||
QJsonObject obj = QJsonDocument::fromJson(raw).object();
|
||||
obj["score"] = 100; // 改
|
||||
obj["grade"] = QStringLiteral("A+"); // 增
|
||||
obj.remove("enrolled"); // 删
|
||||
writeBytes("05_student.json", QJsonDocument(obj).toJson(QJsonDocument::Indented));
|
||||
setStatus(report("05_student.json",
|
||||
QStringLiteral("已改 score=100、新增 grade=A+、删除 enrolled,覆盖回写。")));
|
||||
}
|
||||
|
||||
// ---- 底部按钮 ------------------------------------------------------------
|
||||
void openDemoDir() {
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(m_dir));
|
||||
}
|
||||
void clearDemoDir() {
|
||||
const QDir d(m_dir);
|
||||
int n = 0;
|
||||
for (const QFileInfo &fi : d.entryInfoList(QDir::Files)) {
|
||||
if (QFile::remove(fi.absoluteFilePath())) ++n;
|
||||
}
|
||||
setStatus(QStringLiteral("已清空 demo_output,删除 %1 个文件。").arg(n));
|
||||
}
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
FileIoDemo w;
|
||||
w.show();
|
||||
// 离屏自动化验证时 200ms 后自动退出(仿 p03 风格);
|
||||
// 正常运行窗口保持、不自动退出;不点按钮则不触发任何文件写入。
|
||||
if (qgetenv("QT_QPA_PLATFORM") == "offscreen") {
|
||||
QTimer::singleShot(200, &app, &QApplication::quit);
|
||||
}
|
||||
return app.exec();
|
||||
}
|
||||
Reference in New Issue
Block a user