菜单

FileUtil.h说明

下载

FileUtil / PDFUtil 使用手册

本文档依据当前工程中的以下实现编写:

  • src/util/FileUtil.h

文档以 .cpp 的实际行为为准,而不是只复述头文件注释。FileUtil.h 中还声明了独立的 PDFUtil 类,因此本文在完整介绍 FileUtil 后,也提供 PDFUtil 的全部公开接口说明和调用示例。

1. 使用前准备

1.1 引入头文件

cpp 复制代码
#include "util/FileUtil.h"

#include <iostream>
#include <string>
#include <vector>

using stt::KV_Str_Str;
using stt::data::TTable;
using stt::util::FileUtil;
using stt::util::PDFUtil;

FileUtil 的方法全部是静态方法,无需创建对象:

cpp 复制代码
bool exists = FileUtil::fileExists("./data/config.txt");

如果不使用 using,也可以写完整名称:

cpp 复制代码
bool exists = stt::util::FileUtil::fileExists("./data/config.txt");

1.2 路径约定

  • 为兼容模拟器与 ARM,示例统一使用 / 作为路径分隔符,例如 D:/data/a.txt。
  • 多数写文件接口不会自动创建父目录,应先调用 pathExists 和 createPath。
  • createPath 只创建一层目录,不等价于 mkdir -p。
  • 相对路径以程序当前工作目录为基准,不一定是工程目录。
  • ARM 上的 U 盘目录通常形如 /vendor/udisk_sdxy/,应优先通过 getExportDir 获取。

1.3 返回值与错误信息

布尔接口通常用 true 表示成功、false 表示失败;整数接口的含义以各节中的返回值表为准。

部分失败会写入全局错误字符串,可这样读取:

cpp 复制代码
#include "util/Util.h"

if (!FileUtil::readTxtFile("./data/config.txt", content)) {
    std::cerr << stt::util::DEBUG::getLastErrorString() << std::endl;
}

注意:并非所有接口都会设置错误字符串,而且成功调用不会统一清空旧错误。因此判断结果时必须首先检查当前方法的返回值。

1.4 破坏性操作

removeFile、removeFiles、removePath 和 clearPath 会直接删除数据,没有回收站或撤销机制。调用前应校验路径,禁止把用户输入未经检查直接传给这些方法。

2. FileUtil 接口速查

方法 作用 成功结果
exportDB2CSV(3 个重载) SQLite 表或查询结果导出为 CSV true
getExportDir 获取平台允许的导出目录 非空路径
exportTableToFile TTable 导出为 CSV 0
importTableFromFile CSV 导入 TTable 0
fileExists 判断文件能否作为输入流打开 true
pathExists 判断目录是否存在 true
createTxtFile 覆盖或追加文本文件 true
readTxtFile 读取文本文件 true
createPath 创建单层目录 true
isPathOrFile 判断路径对象类型 0 文件,1 目录
getPathContent 递归枚举目录全部内容 true
getPathFile 枚举当前目录内容 true
removeFile 删除单个文件 true
removeFiles 按正则删除当前目录中的匹配项 无返回值
removePath 递归删除目录及目录本身 true
clearPath 清空目录并重新创建该目录 true
copyFile 把文件复制到目录,保留原文件名 true
copyFile2 把文件复制到指定目标文件 true
copyPath 递归复制目录内容 true
getPathSpace 获取所在磁盘的总空间和可用空间 0

3. SQLite 导出为 CSV

3.1 导出整张表

cpp 复制代码
static bool exportDB2CSV(
    const string& pathdb,
    const string& tbname,
    const string& pathcsv,
    string& err);

参数:

  • pathdb:SQLite 数据库文件路径。
  • tbname:表名或视图名。
  • pathcsv:目标 CSV 文件路径,父目录必须已存在。
  • err:失败时接收错误信息。

返回值:成功返回 true;数据库文件不存在、CSV 无法打开、数据库无法连接、表/视图不存在、没有数据或写入失败时返回 false。

实际导出行为:

  • CSV 开头写入 UTF-8 BOM,便于 Excel 识别中文。
  • 第一行是字段名。
  • 每次从 SQLite 读取 500 行。
  • 字段中有逗号、双引号、回车或换行时会按 CSV 规则加引号,字段内双引号会写成两个双引号。
  • 表中没有任何数据时视为失败,不会只导出表头。
  • 目标文件在数据库检查前已经打开,因此后续失败时可能留下空文件或只含 BOM 的文件。

示例:

cpp 复制代码
std::string err;
bool ok = FileUtil::exportDB2CSV(
    "./data/oplog.db",
    "S_OPLOG",
    "./data/oplog.csv",
    err);

if (!ok) {
    std::cerr << "导出失败: " << err << std::endl;
} else {
    std::cout << "已导出 ./data/oplog.csv" << std::endl;
}

3.2 按 SQL 查询条件导出

cpp 复制代码
static bool exportDB2CSV(
    const string& pathdb,
    const string& tbname,
    const string& sql,
    const string& pathcsv,
    string& err);

sql 为空时等价于导出整张表;非空时导出该查询的结果。虽然查询可以只选择部分字段,但 tbname 指定的表或视图仍必须存在,因为实现会先单独检查它。

示例:只导出指定时间以后的记录。

cpp 复制代码
std::string err;
const std::string sql =
    "SELECT STIME, TAGNAME, VALUE "
    "FROM SAMPLE_5_V "
    "WHERE STIME >= '2026-01-01' "
    "ORDER BY STIME";

bool ok = FileUtil::exportDB2CSV(
    "./data/SAMPLE_5.db",
    "SAMPLE_5_V",
    sql,
    "./data/sample_2026.csv",
    err);

if (!ok) {
    std::cerr << err << std::endl;
}

不要把未经转义的外部输入直接拼接进 sql。该接口不会绑定 SQL 参数,也不会替调用者防止 SQL 注入或引号错误。

3.3 同步或异步导出

cpp 复制代码
static bool exportDB2CSV(
    const string& pathdb,
    const string& tbname,
    const string& sql,
    const string& pathcsv,
    string& err,
    bool isAsync,
    const string& strOK = "导出成功!",
    const string& strFail = "导出失败");
  • isAsync == false:在当前线程执行,返回真实导出结果。
  • isAsync == true:启动 AsynWorker 后立即返回 true;完成后通过消息框显示 strOK 或 strFail。
  • 异步模式下,返回的 true 只表示已发起任务,不代表 CSV 导出成功。

同步调用示例:

cpp 复制代码
std::string err;
bool ok = FileUtil::exportDB2CSV(
    "./data/SAMPLE_5.db",
    "SAMPLE_5_V",
    "SELECT * FROM SAMPLE_5_V",
    "./data/sample.csv",
    err,
    false);

异步调用示例(展示现有 API 的调用形式):

cpp 复制代码
std::string err;
FileUtil::exportDB2CSV(
    "./data/SAMPLE_5.db",
    "SAMPLE_5_V",
    "SELECT * FROM SAMPLE_5_V WHERE STIME > '2026-01-01'",
    "./data/sample.csv",
    err,
    true,
    "样本导出成功",
    "样本导出失败");

重要限制:当前异步实现把内部局部变量 status 按引用捕获到后台任务中,函数返回后该引用失效,存在未定义行为;err 也按引用捕获,调用方必须保证它在任务结束前仍然有效。在修复实现前,生产代码应优先在调用方自己的后台线程中调用同步重载,而不要依赖此异步重载。

4. 获取导出目录

cpp 复制代码
static string getExportDir(const string& subdir);

此方法在头文件中标记为 @HIDECA,但它是公开静态方法。

平台行为:

  • ARM:选择检测到的第一个 U 盘,返回 U盘路径/subdir/;没有 U 盘或目录创建失败时返回空字符串。
  • 模拟器/Windows:固定返回 ./data/,忽略 subdir,也不会负责创建 ./data。
  • ARM 上只适合单层 subdir;多层子目录的父级必须事先存在。

示例:

cpp 复制代码
std::string exportDir = FileUtil::getExportDir("sample");
if (exportDir.empty()) {
    std::cerr << "没有可用的导出目录" << std::endl;
    return;
}

std::string csvPath = exportDir + "sample.csv";
std::string err;
FileUtil::exportDB2CSV(
    "./data/SAMPLE_5.db",
    "SAMPLE_5_V",
    csvPath,
    err);

5. TTable 与 CSV

5.1 exportTableToFile:导出表格

cpp 复制代码
static int exportTableToFile(
    TTable& table,
    const string& path,
    bool overwrite = true);

参数:

  • table:要导出的 TTable。
  • path:目标 CSV 路径,父目录必须已存在。
  • overwrite:目标已存在时是否覆盖。

返回值:

返回值 含义
0 已成功写出,或者 overwrite == false 且目标文件已存在而跳过
1 目标文件无法打开

CSV 同样带 UTF-8 BOM,并使用标准 CSV 引号转义。当前实现没有在每次写入后检查流状态,因此磁盘中途写满等错误仍可能返回 0。

示例:

cpp 复制代码
TTable table("users");
table.addColumn("ID");
table.addColumn("NAME");
table.addColumn("REMARK");

std::vector<std::string> row1 = {"1", "张三", "一线,二线"};
std::vector<std::string> row2 = {"2", "李四", "他说\"已完成\""};
table.addRow(row1);
table.addRow(row2);

int rc = FileUtil::exportTableToFile(
    table,
    "./data/users.csv",
    true);

if (rc != 0) {
    std::cerr << "CSV 文件无法打开" << std::endl;
}

不覆盖已有文件:

cpp 复制代码
int rc = FileUtil::exportTableToFile(
    table,
    "./data/users.csv",
    false);

// rc == 0 也可能表示文件已存在,因此本次被跳过。

5.2 importTableFromFile:导入表格

cpp 复制代码
static int importTableFromFile(TTable& table, const string& path);

返回值:

返回值 含义
0 成功
1 路径不是普通文件或无法获取状态
2 文本读取失败
3 文件内容为空

导入成功时会调用 table.set(content),CSV 第一行成为列名,后续行成为数据。原表的列和行会被替换。

示例:

cpp 复制代码
TTable imported("imported_users");
int rc = FileUtil::importTableFromFile(
    imported,
    "./data/users.csv");

if (rc == 0) {
    std::cout << "行数: " << imported.size() << std::endl;
    std::cout << "第一行姓名: " << imported.getValue(0, 1) << std::endl;
} else {
    std::cerr << "导入失败,错误码: " << rc << std::endl;
}

当前限制:

  • TTable::set 的 CSV 解析能力有限,不适合字段内部包含真实换行的复杂 CSV。
  • 解析代码没有明确去除 UTF-8 BOM。导入带 BOM 的 CSV 时,应检查第一列列名是否包含 BOM 字节。
  • CSV 中双引号的还原能力应结合实际数据测试,不应把它当作完整 RFC 4180 解析器。

6. 文件与目录判断

6.1 fileExists

cpp 复制代码
static bool fileExists(const string& filePath);

该方法通过 ifstream 尝试打开路径。返回 true 表示文件可以作为输入流打开;不存在、没有读取权限或打开失败都返回 false。

cpp 复制代码
if (FileUtil::fileExists("./data/config.ini")) {
    std::cout << "配置文件可读" << std::endl;
}

如果需要严格区分文件、目录和其他对象,请使用 isPathOrFile。

6.2 pathExists

cpp 复制代码
static bool pathExists(const string& path);

只有路径存在且类型是目录时才返回 true。

cpp 复制代码
if (!FileUtil::pathExists("./data/export")) {
    if (!FileUtil::createPath("./data/export")) {
        std::cerr << "目录创建失败" << std::endl;
    }
}

6.3 isPathOrFile

cpp 复制代码
static int isPathOrFile(const string& path);
返回值 类型
-1 获取状态失败,通常表示不存在或无权访问
0 普通文件
1 目录
2 其他类型,例如某些设备或特殊文件
cpp 复制代码
switch (FileUtil::isPathOrFile("./data/item")) {
case 0:
    std::cout << "普通文件" << std::endl;
    break;
case 1:
    std::cout << "目录" << std::endl;
    break;
case 2:
    std::cout << "其他文件系统对象" << std::endl;
    break;
default:
    std::cout << "路径不存在或无法访问" << std::endl;
    break;
}

7. 文本文件读写

7.1 createTxtFile

cpp 复制代码
static bool createTxtFile(
    const string& filePath,
    const string& content,
    int mode = 0);

mode:

  • 0:覆盖已有文件。
  • 1:追加到已有文件末尾。
  • 其他值:立即失败,并设置错误信息 the param mode can be 0 or 1.。

如果文件不存在,mode 为 0 或 1 都会创建新文件。方法不会自动加换行,也不会创建父目录。

覆盖写入示例:

cpp 复制代码
bool ok = FileUtil::createTxtFile(
    "./data/config.txt",
    "host=127.0.0.1\nport=8080\n",
    0);

追加写入示例:

cpp 复制代码
bool ok = FileUtil::createTxtFile(
    "./data/app.log",
    "启动完成\n",
    1);

当前实现不会可靠检查所有 open/write 返回值。对配置、升级包等关键文件,建议写入后再调用 fileExists 或 readTxtFile 校验。

7.2 readTxtFile

cpp 复制代码
static bool readTxtFile(const string& filePath, string& res);

行为:

  • 按行读取,并用 \n 重新连接。
  • 结果末尾不会保留最后一个换行符。
  • 方法不会先清空 res,而是追加到原内容后面。

推荐调用方式:

cpp 复制代码
std::string content;
content.clear();

if (FileUtil::readTxtFile("./data/config.txt", content)) {
    std::cout << content << std::endl;
} else {
    std::cerr << "读取失败" << std::endl;
}

展示追加语义:

cpp 复制代码
std::string content = "prefix:";
FileUtil::readTxtFile("./data/config.txt", content);
// content 以 "prefix:" 开头,随后直接接文件内容。

该方法是文本读取接口,不适合包含 \0 或需要保持原始换行字节的二进制文件。

8. 创建和枚举目录

8.1 createPath

cpp 复制代码
static bool createPath(const string& path);

成功创建目录返回 true。以下情况返回 false:

  • 父目录不存在。
  • 目标目录已经存在。
  • 权限不足或路径非法。

创建多层目录时要逐层创建:

cpp 复制代码
if (!FileUtil::pathExists("./data")) {
    FileUtil::createPath("./data");
}

if (!FileUtil::pathExists("./data/export")) {
    if (!FileUtil::createPath("./data/export")) {
        std::cerr << "创建 ./data/export 失败" << std::endl;
    }
}

8.2 getPathContent:递归枚举

cpp 复制代码
static bool getPathContent(
    const string& path,
    vector<KV_Str_Str>& res);

返回的每个 KV_Str_Str:

  • k:文件名或目录名,不含父路径。
  • v:其父目录路径。

方法会把目录本身加入结果,然后继续递归其内容。结果顺序由文件系统枚举顺序决定,不保证排序。方法不会清空 res。

cpp 复制代码
std::vector<KV_Str_Str> entries;
entries.clear();

if (FileUtil::getPathContent("./data", entries)) {
    for (const KV_Str_Str& entry : entries) {
        std::string fullPath = entry.v + "/" + entry.k;
        int type = FileUtil::isPathOrFile(fullPath);
        std::cout
            << (type == 1 ? "[DIR]  " : "[FILE] ")
            << fullPath << std::endl;
    }
}

如果任何一项的 stat 或子目录递归失败,方法会返回 false;此时 res 可能已经包含部分结果。

8.3 getPathFile:只枚举当前层

cpp 复制代码
static bool getPathFile(
    const string& path,
    vector<KV_Str_Str>& res);

尽管名字是 getPathFile,它实际返回当前目录中的文件和子目录,只是不进入子目录。k、v、顺序和追加行为与 getPathContent 相同。

cpp 复制代码
std::vector<KV_Str_Str> entries;
if (!FileUtil::getPathFile("./data", entries)) {
    std::cerr << "目录无法打开" << std::endl;
    return;
}

for (const KV_Str_Str& entry : entries) {
    std::string fullPath = entry.v + "/" + entry.k;
    std::cout << fullPath << std::endl;
}

如果只想要普通文件,需要再用 isPathOrFile(fullPath) == 0 过滤。

9. 删除文件和目录

9.1 removeFile

cpp 复制代码
static bool removeFile(const string& filePath);

只适合删除普通文件。文件不存在或删除失败时返回 false。

cpp 复制代码
const std::string file = "./data/old.log";
if (FileUtil::fileExists(file)) {
    if (!FileUtil::removeFile(file)) {
        std::cerr << "删除失败: " << file << std::endl;
    }
}

9.2 removeFiles

cpp 复制代码
static void removeFiles(const string& dir, const string& reg);

该方法只扫描 dir 当前层,并对每个名称执行 POSIX extended 风格的完整正则匹配。匹配后调用 std::remove。它不递归,没有返回值,打开目录或删除失败时调用方无法直接得知。

删除所有 .tmp 文件:

cpp 复制代码
FileUtil::removeFiles("./data/cache", ".*\\.tmp");

删除名称形如 page-000001.png 的图片:

cpp 复制代码
FileUtil::removeFiles(
    "./data/pdf",
    "page\\-[0-9]{6}\\.png");

正则是对完整文件名匹配,不是子串搜索。需要匹配任意前缀时要显式写 .*。

9.3 removePath

cpp 复制代码
static bool removePath(const string& path);

递归删除目录下的所有文件和子目录,最后删除 path 本身。路径不存在、不是可打开目录或任一步失败时返回 false。

cpp 复制代码
const std::string tempDir = "./data/tmp_export";
if (FileUtil::pathExists(tempDir)) {
    if (!FileUtil::removePath(tempDir)) {
        std::cerr << "递归删除失败" << std::endl;
    }
}

安全要求:不要对根目录、挂载点、工作目录或未经规范化的用户输入调用本方法。实现使用 stat 跟随符号链接,在包含符号链接的目录树上存在越过预期边界的风险。

9.4 clearPath

cpp 复制代码
static bool clearPath(const string& path);

实现步骤是:

  1. 调用 removePath(path) 删除目录及全部内容。
  2. 调用 createPath(path) 重新创建空目录。

因此目录必须事先存在;不存在时会返回 false,不会直接创建。

cpp 复制代码
const std::string cacheDir = "./data/cache";
if (FileUtil::pathExists(cacheDir)) {
    if (!FileUtil::clearPath(cacheDir)) {
        std::cerr << "清空缓存目录失败" << std::endl;
    }
}

10. 复制文件和目录

10.1 copyFile:复制到目录

cpp 复制代码
static bool copyFile(
    const string& srcfile,
    const string& dstdir);

当前实际行为是把源文件复制到目标目录,并保留原文件名:

text 复制代码
./data/a.txt + ./backup  ->  ./backup/a.txt

头文件注释中“新文件名为时间戳后 8 位加原文件名”的描述与当前实现不一致,当前实现不会添加时间戳。

cpp 复制代码
if (!FileUtil::pathExists("./backup")) {
    FileUtil::createPath("./backup");
}

bool ok = FileUtil::copyFile(
    "./data/report.csv",
    "./backup");

if (!ok) {
    std::cerr << "复制失败" << std::endl;
}

注意事项:

  • 目标目录必须已经存在。
  • 同名文件会被截断覆盖。
  • 源文件名通过最后一个 / 提取,因此即使在 Windows 模拟器上也应使用 /,不要传 D:\\data\\a.txt。
  • ARM 上会先比较文件大小与目标磁盘可用空间。

10.2 copyFile2:复制为指定文件

cpp 复制代码
static bool copyFile2(
    const string& srcfile,
    const string& dstfile);

与 copyFile 不同,第二个参数是完整目标文件路径,可同时改名。

cpp 复制代码
bool ok = FileUtil::copyFile2(
    "./data/report.csv",
    "./backup/report-previous.csv");

if (!ok) {
    std::cerr
        << stt::util::DEBUG::getLastErrorString()
        << std::endl;
}

父目录必须存在;目标文件已存在时会覆盖。该接口不会执行 ARM 可用空间预检查。

10.3 copyPath:递归复制目录内容

cpp 复制代码
static bool copyPath(
    const string& src,
    const string& target);

复制的是 src 目录中的内容,不是在 target 下再创建一层 src 目录。目标根目录必须已存在;源目录里的子目录会按需创建。

cpp 复制代码
const std::string src = "./data/project";
const std::string dst = "./backup/project";

if (!FileUtil::pathExists(dst)) {
    FileUtil::createPath(dst);  // 父目录 ./backup 必须已存在
}

if (!FileUtil::copyPath(src, dst)) {
    std::cerr << "目录复制失败" << std::endl;
}

注意事项:

  • 同名目标文件会被覆盖。
  • 当前实现没有检查 opendir(src) 是否成功;源目录无效时可能发生崩溃。因此调用前必须先确认 pathExists(src)。
  • 不要把目标目录放在源目录内部,否则递归枚举可能不断复制新创建的内容。
  • 目录树中存在符号链接时应额外谨慎。

更稳妥的调用:

cpp 复制代码
bool safeCopyPath(const std::string& src, const std::string& dst) {
    if (!FileUtil::pathExists(src)) {
        return false;
    }
    if (!FileUtil::pathExists(dst)) {
        return false;
    }
    return FileUtil::copyPath(src, dst);
}

11. 查询磁盘空间

cpp 复制代码
static int getPathSpace(
    const string& path,
    float& total,
    float& free);

输出参数单位均为 MB:

  • total:路径所在文件系统/磁盘的总空间。
  • free:普通调用方可用的空间。

成功返回 0,失败返回 1。失败时两个输出参数均保持为 0。

cpp 复制代码
float totalMB = 0.0F;
float freeMB = 0.0F;

int rc = FileUtil::getPathSpace(
    "./data",
    totalMB,
    freeMB);

if (rc == 0) {
    std::cout << "总空间: " << totalMB << " MB\n";
    std::cout << "可用空间: " << freeMB << " MB\n";
} else {
    std::cerr << "无法获取磁盘空间" << std::endl;
}

接口使用 float,大容量磁盘的数值精度有限。path 应是已经存在且可访问的目录。

12. 常用组合示例

12.1 安全写入并回读配置

cpp 复制代码
bool writeAndVerifyConfig(const std::string& path) {
    const std::string content =
        "server=192.168.1.10\n"
        "port=9000\n";

    if (!FileUtil::createTxtFile(path, content, 0)) {
        return false;
    }

    std::string actual;
    if (!FileUtil::readTxtFile(path, actual)) {
        return false;
    }

    // readTxtFile 不保留文件末尾的换行,因此比较时去掉预期末尾换行。
    return actual == "server=192.168.1.10\nport=9000";
}

12.2 导出前检查目录和空间

cpp 复制代码
bool exportWithPrecheck(TTable& table, const std::string& path) {
    std::size_t slash = path.find_last_of('/');
    if (slash == std::string::npos) {
        return false;
    }

    std::string dir = path.substr(0, slash);
    if (!FileUtil::pathExists(dir)) {
        return false;
    }

    float totalMB = 0.0F;
    float freeMB = 0.0F;
    if (FileUtil::getPathSpace(dir, totalMB, freeMB) != 0) {
        return false;
    }
    if (freeMB < 10.0F) {
        return false;
    }

    return FileUtil::exportTableToFile(table, path, true) == 0;
}

12.3 枚举当前目录并按类型处理

cpp 复制代码
void printDirectory(const std::string& dir) {
    std::vector<KV_Str_Str> entries;
    if (!FileUtil::getPathFile(dir, entries)) {
        std::cerr << "无法打开目录: " << dir << std::endl;
        return;
    }

    for (const KV_Str_Str& entry : entries) {
        const std::string fullPath = entry.v + "/" + entry.k;
        const int type = FileUtil::isPathOrFile(fullPath);

        if (type == 0) {
            std::cout << "文件: " << fullPath << std::endl;
        } else if (type == 1) {
            std::cout << "目录: " << fullPath << std::endl;
        } else {
            std::cout << "其他: " << fullPath << std::endl;
        }
    }
}

13. PDFUtil 概述

PDFUtil 使用 Xpdf 命令行工具把 PDF 指定页转换成 PNG,再将 PNG 设置为 STTPanel 的背景图。

需要的外部文件至少包括:

  • Windows:pdftopng.exe、pdffonts.exe、pdfinfo.exe。
  • ARM:对应的不带 .exe 的可执行文件。
  • xpdfrc 配置文件(updateXpdfCfg 会覆盖生成)。
  • xpdf-t1fonts 和 chinese-simplified 字体相关目录。

工具目录、PDF 路径和 PNG 前缀最好使用绝对路径、/ 分隔符且不包含空格。当前实现直接拼接外部命令,没有对路径加引号;带空格路径会执行失败,不可信路径还可能带来命令注入风险。

14. PDFUtil 对象接口

14.1 构造函数与析构函数

cpp 复制代码
PDFUtil(
    const string& xpdfdir,
    const string& filepath,
    const string& prefix,
    STTPanel* panel);

~PDFUtil();

参数:

  • xpdfdir:Xpdf 工具目录。
  • filepath:要显示的 PDF 文件。
  • prefix:临时 PNG 的路径及文件名前缀,例如 D:/pdfcache/page。
  • panel:用于显示页面的 STTPanel*,必须非空并且生命周期长于 PDFUtil。

构造时会调用 getPageCount 获取总页数,并把当前页初始化为第 1 页。构造函数不会自动显示第一页,通常要紧接着调用 first()。

cpp 复制代码
void showManualPdf(STTPanel* panel) {
    static PDFUtil pdf(
        "D:/pdftool/xpdf",
        "D:/manual/user-guide.pdf",
        "D:/pdfcache/page",
        panel);

    pdf.first();
}

示例使用 static 只是为了确保示例函数返回后对象仍存在。实际工程中更适合把 PDFUtil 保存为页面或控制器成员。析构函数当前不删除临时 PNG,也不拥有/销毁 panel。

14.2 gotoPage

cpp 复制代码
void gotoPage(int ipage);

执行流程:获取该页字体、重写 xpdfrc、删除同前缀旧 PNG、转换指定页、把生成路径设置为面板背景。

cpp 复制代码
PDFUtil pdf(toolDir, pdfPath, pngPrefix, panel);
pdf.gotoPage(12);

当前实现不会检查 ipage 是否位于 [1, 总页数],调用方应自行校验。prefix 必须包含 /,以便实现拆出临时文件所在目录。

14.3 first 与 last

cpp 复制代码
void first();
void last();
cpp 复制代码
pdf.first();  // 显示第 1 页
pdf.last();   // 显示构造时检测到的最后一页

若 getPageCount 返回 0,last() 会尝试跳转到第 0 页,因此初始化后应先通过静态 getPageCount 验证 PDF。

14.4 back 与 next

cpp 复制代码
void back();
void next();
cpp 复制代码
pdf.back();  // 当前页大于 1 时显示上一页,否则不操作
pdf.next();  // 当前页小于总页数时显示下一页,否则不操作

14.5 rotate

cpp 复制代码
void rotate();

每次调用顺时针增加 90 度,在 0、90、180、270 之间循环;同时交换面板宽高并重新生成当前页。

cpp 复制代码
pdf.rotate();  // 0 -> 90 度
pdf.rotate();  // 90 -> 180 度

14.6 zoom

cpp 复制代码
void zoom(float scale);

scale 是相对当前面板尺寸的倍率,不是绝对倍率。例如连续调用两次 1.5F,面板边长累计变为原来的 2.25 倍。

cpp 复制代码
pdf.zoom(1.25F);  // 放大 25%
pdf.zoom(0.8F);   // 在当前尺寸基础上缩小到 80%

放大后按 宽 * 高 * 4 估算的内存达到或超过 4 MiB 时,本次缩放被拒绝。当前实现没有验证 scale > 0,因此调用方必须保证倍率为正数。

14.7 完整导航示例

cpp 复制代码
class PdfController {
public:
    explicit PdfController(STTPanel* panel)
        : m_pdf(
            "D:/pdftool/xpdf",
            "D:/manual/user-guide.pdf",
            "D:/pdfcache/manual-page",
            panel) {
    }

    void open()       { m_pdf.first(); }
    void previous()   { m_pdf.back(); }
    void following()  { m_pdf.next(); }
    void lastPage()   { m_pdf.last(); }
    void rotate90()   { m_pdf.rotate(); }
    void zoomIn()     { m_pdf.zoom(1.25F); }
    void zoomOut()    { m_pdf.zoom(0.8F); }

private:
    PDFUtil m_pdf;
};

15. PDFUtil 静态接口

这些接口在头文件中标记为 @HideCA,仍然可以从 C++ 代码直接调用。

15.1 toPng

cpp 复制代码
static string toPng(
    const string& tooldir,
    int ipage,
    const string& pdfpath,
    const string& prefix,
    int rot = 0);

把 PDF 的指定页转换为 PNG。成功时的目标名称格式为:

text 复制代码
<prefix>-<6 位页码>.png

示例:

cpp 复制代码
std::string png = PDFUtil::toPng(
    "D:/pdftool/xpdf",
    12,
    "D:/manual/user-guide.pdf",
    "D:/pdfcache/page",
    90);

// 预期路径:D:/pdfcache/page-000012.png
if (png.empty() || !FileUtil::fileExists(png)) {
    std::cerr << "PDF 转 PNG 失败" << std::endl;
}

仅当 PDF 或 pdftopng 不存在时方法明确返回空字符串。执行外部命令后,它不会检查退出码,而是直接返回预期文件名,所以调用方必须再用 fileExists 验证输出。

15.2 getFontNameAndType

cpp 复制代码
static vector<KV_Str_Str> getFontNameAndType(
    const string& tooldir,
    int ipage,
    const string& pdfpath);

调用 pdffonts 获取某页字体。每项的 k 是字体名称,v 是字体类型。

cpp 复制代码
std::vector<KV_Str_Str> fonts = PDFUtil::getFontNameAndType(
    "D:/pdftool/xpdf",
    1,
    "D:/manual/user-guide.pdf");

for (const KV_Str_Str& font : fonts) {
    std::cout << font.k << " -> " << font.v << std::endl;
}

PDF 或工具不存在、命令输出为空或格式无法识别时返回空列表。当前解析依赖 pdffonts 的固定列宽输出格式。

15.3 updateXpdfCfg

cpp 复制代码
static bool updateXpdfCfg(
    const string& tooldir,
    vector<KV_Str_Str> vfont);

根据字体列表生成并覆盖 <tooldir>/xpdfrc。其中 TrueType 字体会映射到系统指定的文泉驿微米黑字体。

cpp 复制代码
std::vector<KV_Str_Str> fonts = PDFUtil::getFontNameAndType(
    toolDir,
    1,
    pdfPath);

if (!PDFUtil::updateXpdfCfg(toolDir, fonts)) {
    std::cerr << "xpdfrc 更新失败" << std::endl;
}

这是覆盖写操作,会丢弃 xpdfrc 原有自定义配置。需要保留自定义内容时,应在调用前备份。

15.4 getInfo

cpp 复制代码
static vector<KV_Str_Str> getInfo(
    const string& tooldir,
    const string& pdfpath);

调用 pdfinfo 并把每项解析为键值对。常见键包括 Title、Author、Pages、Page size、File size 和 PDF version。

cpp 复制代码
std::vector<KV_Str_Str> info = PDFUtil::getInfo(
    "D:/pdftool/xpdf",
    "D:/manual/user-guide.pdf");

for (const KV_Str_Str& item : info) {
    std::cout << item.k << ": " << item.v << std::endl;
}

PDF 或工具不存在时返回空列表。解析依赖 pdfinfo 默认输出格式以及键名区域约 16 个字符的布局。

15.5 getPageCount

cpp 复制代码
static int getPageCount(
    const string& tooldir,
    const string& pdfpath);

内部调用 getInfo,查找键 Pages 并转换为整数。失败时返回 0。

cpp 复制代码
int pages = PDFUtil::getPageCount(
    "D:/pdftool/xpdf",
    "D:/manual/user-guide.pdf");

if (pages <= 0) {
    std::cerr << "无法获取 PDF 页数" << std::endl;
} else {
    std::cout << "总页数: " << pages << std::endl;
}

16. 已知实现差异与使用建议

  1. copyFile 的头文件注释提到时间戳改名,但实现保留原文件名。
  2. removeFiles 的头文件注释写有“返回是否成功”,但真实签名为 void。
  3. getPathFile 不只返回文件,也返回当前层子目录。
  4. createPath 不能递归创建目录,且目录已存在时返回 false。
  5. readTxtFile 追加到输出字符串,不会先清空。
  6. 异步 exportDB2CSV 存在引用生命周期风险,修复前应避免用于生产流程。
  7. copyPath 未检查源目录打开结果,必须先调用 pathExists(src)。
  8. PDF 外部命令没有给路径加引号,也不校验进程退出码;应只使用可信且不含空格的路径,并验证输出文件。
  9. 多个接口在 I/O 中途失败时仍可能返回成功;关键数据操作应做结果文件存在性、大小或内容校验。
  10. 文件与目录遍历接口不处理循环、挂载边界或符号链接隔离,不应直接用于不可信目录树。
最近修改: 2026-10-20Powered by