Content Table

Qt 生成报表

开发中经常会需要输出报表,方法很多,这里就不一一去列举对比各种方法的优缺点,只介绍使用字符串模板输出 PDF 报表的方式,能够满足绝大多数时候的需求:

  1. 字符串模板 + 数据输出 HTML
  2. HTML 输出 PDF

输出 HTML

为了生成 HTML,可以在程序中使用字符串拼接的方式,但这种方式对于复杂点的 HTML 就会非常麻烦,不推荐使用,我们这里将使用字符串模板引擎 Inja 来生成 HTML,使用步骤如下:

  1. 下载 Inja

  2. 解压,得到的目录结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    inja-2.1.0
    ...
    ├── single_include
    │   └── inja
    │   └── inja.hpp
    └── third_party
    └── include
    └── nlohmann
    ├── LICENSE.MIT
    └── json.hpp
  3. 复制 single_include 下的 inja 目录和 third_party/include 下的 nlohmann 目录到项目的 lib 目录下:

    1
    2
    3
    4
    5
    6
    lib
    ├── inja
    │   └── inja.hpp
    └── nlohmann
    ├── LICENSE.MIT
    └── json.hpp
  4. Qt 工程的 pro 文件中添加包含路径: INCLUDEPATH += lib

  5. 最简单的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <inja/inja.hpp>

    using namespace inja;
    using json = nlohmann::json;

    int main(int argc, char *argv[]) {
    json data;
    data["name"] = "world";

    render("Hello {{ name }}", data); // Returns std::string "Hello world"

    return 0;
    }

    Inja 的模板使用了 Mustache 风格的语法,上面例子中 { { name } }data["name"] 的值 world 替换掉,生成字符串 Hello world,其中 Inja 使用 JSON 库 nlohmann json 传递数据给模板。

到这里我们能够使用 Inja 把字符串模板+数据生成最终的字符串了,实际项目中的报表一般都比较大,还有复杂的样式,如果直接在程序里使用字符串拼接模板,如要生成下面这样的报表想想会有多不容易:

我们推荐使用下面的方式来完成:
  1. 使用 HTML 进行设计,调整效果直到满意
  2. 把变化的部分按照 Inja 的语法替换为 { { variable } },得到模板,保存到可执行文件 (exe) 所在目录的 template 目录下的 report.html
  3. 程序中准备模板需要的数据
  4. 使用 Inja 生成最终的 HTML

模板文件 template/report.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>检测报告</title>
<style>
body {
color: #333;
background-color: white;
padding: 20px;
font-size: 12px;
font-family: Tahoma, 微软雅黑;
}

h3 {

text-align: center;
margin: 0;
padding: 5px;
}

table {
width: 100%;
border-collapse: collapse;
}

td {
padding: 8px;
vertical-align: top;
text-align: center;
}

td.text-left {
text-align: left;
}

.fitb1 {
padding: 0 5px;
}

.fitb2 {
min-width: 100px;
display: inline-block;
text-align: center;
}

.gap {
display: inline-block;
width: 60px;
}

table, th, td, .wrapper {
border: 1px solid #AAA;
}

h3, .fitb1, .fitb2 {
border-bottom: 1px solid #AAA;
}
</style>
</head>
<body>
<div class="wrapper">
<h3>检测原始数据</h3>
<div style="margin-left: 10px; padding: 10px;">(3)检测数据</div>

<div style="padding: 10px; padding-top: 0">
<table>
<tr>
<td colspan="11">
设定温度: <span class="fitb1">aaa.aa</span> °C,
设定压力: <span class="fitb1">bbb.bb</span> kPa,
设定时间: <span class="fitb1">ccc</span> s,
设定间隔: <span class="fitb1">dd</span> s
</td>
</tr>
<tr>
<td></td>
<td>时刻</td>
<td>温度点<br>1 测量<br>值/°C</td>
<td>温度点<br>2 测量<br>值/°C</td>
<td>温度点<br>3 测量<br>值/°C</td>
<td>温度点<br>4 测量<br>值/°C</td>
<td>温度点<br>5 测量<br>值/°C</td>
<td>温度点<br>6 测量<br>值/°C</td>
<td>压力测<br>量值<br>/kPa</td>
<td>最大<br><br>/°C</td>
<td>最小<br><br>/°C</td>
</tr>

<!-- 下面注释掉的代码在设计时用来看效果,不需要后注释掉即可 -->
<!--
<tr>
<td>1</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>
</tr>
<tr>
<td>2</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>
</tr>
<tr>
<td>3</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>
</tr>
<tr>
<td>4</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>
</tr>
-->

<!-- 模板语法 -->
{% for row in rows %}
<tr>
{% for col in row %}
<td>{{ col }}</td>
{% endfor %}
</tr>
{% endfor %}

<tr>
<td colspan="11" class="text-left">
实测灭菌温度平均值<span class="fitb2">{{ value1 }}</span>°C
</td>
</tr>
<tr>
<td colspan="11" class="text-left">
温度均匀性<span class="fitb2">{{ value2 }}</span>°C
</td>
</tr>
<tr>
<td colspan="11" class="text-left">
灭菌温度带<span class="fitb2">{{ value3 }}</span>°C
</td>
</tr>
<tr>
<td colspan="11" class="text-left">
实测灭菌压力平均值<span class="fitb2">{{ value4 }}</span>kPa
</td>
</tr>
<tr>
<td colspan="11" class="text-left">
实测灭菌时间<span class="fitb2">{{ value5 }}</span>s
</td>
</tr>
</table>
</div>
</div>
</body>
</html>

map.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <QTextCodec>
#include <QFile>
#include <QTextStream>

#include <inja/inja.hpp>

using namespace inja;
using json = nlohmann::json;

int main(int argc, char *argv[]) {
Q_UNUSED(argc)
Q_UNUSED(argv)

// [1] 准备模板需要的数据
json rows = json::array();

// 表格中需要 10 行 11 列的数据
for (int i = 1; i <= 10; i++) {
json row = json::array();

row.push_back(i);
for (int j = 1; j <= 10; j++) {
QString col = "列 - " + QString::number(j);
row.push_back(col.toStdString());
}

rows.push_back(row);
}

json data;
data["rows"] = rows;
data["value1"] = 10;
data["value2"] = 20;
data["value3"] = 30;
data["value4"] = 40;
data["value5"] = 50;

// [2] 模板 + 数据生成 HTML
inja::Environment env;
Template temp = env.parse_template("./template/report.html");
std::string result = env.render(temp, data);
QString html = QString::fromStdString(result);

// [3] 输出到文件,浏览器打开查看效果
QFile file("/Users/Biao/Desktop/output.html");
file.open(QIODevice::WriteOnly | QIODevice::Text);
QTextStream out(&file);
out.setCodec(QTextCodec::codecForName("UTF-8"));
out << html;
file.close();

return 0;
}

输出 PDF

Qt 里输出 HTML 为 PDF 可以借助 QWebEngineView & QWebEnginePage:

1
ui->webView->page()->printToPdf(filePath);

但是 Windows 中只有 VS 的 Qt 支持 Qt Web Engine,MinGW 的 Qt 不支持,甚至 Qt 5.5 后 Qt Web Kit 模块也被删除了,再加上 QTextDocument 输出 PDF 不支持 class 样式,只支持内联的 style 样式,导致开发 HTML 模板困难,鉴于这些原因,我们这里使用开源库 wkhtmltopdf 来把 HTML 转为 PDF:

  1. 下载 7z 压缩版,选择 win32 的,因为 MinGW 目前只有 32 位的:

  2. 解压,得到的目录结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    wkhtmltox
    ├── bin
    │   ├── libwkhtmltox.a
    │   ├── wkhtmltoimage.exe
    │   ├── wkhtmltopdf.exe
    │   └── wkhtmltox.dll
    └── include
    └── wkhtmltox
    ├── dllbegin.inc
    ├── dllend.inc
    ├── image.h
    └── pdf.h
  3. 复制 include 下的 wkhtmltox 目录到项目的 lib 目录下:

    1
    2
    3
    4
    lib
    ├── wkhtmltox
    │ └── pdf.h
    │   ├── ...
  4. 复制 wkhtmltox.dll 到可执行文件 (exe) 目录

  5. Qt 工程的 pro 文件中添加包含路径: INCLUDEPATH += lib

  6. Qt 工程的 pro 文件中添加 DLL 路径: LIBS += -L$$OUT_PWD/debug -lwkhtmltox (根据具体的情况而定)

  7. 使用下面的代码把 HMTL 内容转为 PDF:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    #include <QApplication>
    #include <QTextCodec>
    #include <QDebug>
    #include <QString>

    #include <wkhtmltox/pdf.h>

    int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));

    wkhtmltopdf_global_settings * gs;
    wkhtmltopdf_object_settings * os;
    wkhtmltopdf_converter * c;

    wkhtmltopdf_init(false);
    gs = wkhtmltopdf_create_global_settings();
    os = wkhtmltopdf_create_object_settings();
    c = wkhtmltopdf_create_converter(gs);

    // 设置输入输出
    wkhtmltopdf_set_object_setting(os, "page", "input.html");
    wkhtmltopdf_set_global_setting(gs, "out", "output.pdf");
    wkhtmltopdf_add_object(c, os, nullptr);

    /* Perform the actual conversion */
    if (!wkhtmltopdf_convert(c)) {
    fprintf(stderr, "Conversion failed!");
    } else {
    fprintf(stdout, "Conversion finished!");
    }

    wkhtmltopdf_destroy_converter(c);
    wkhtmltopdf_deinit();

    return 0;
    }

    第 22 行设置 HTML 的文件路径,第 23 行设置输出的 PDF 路径。如果想直接把 HTML 的字符串转为 PDF,把第 22 行 HTML 文件路径换为前缀 data:text/html, + html content:

    1
    2
    QString html = "data:text/html,<html><head><meta charset=\"UTF-8\"></head><body>Hello</body><html>";
    wkhtmltopdf_set_object_setting(os, "page", html.toUtf8().constData());

    关于上面代码详细的注释请阅读 examples/pdf_c_api.c

提示:

  • 默认输出 PDF 为竖版的 (即 Portrail),如果要输出为横版的 (Landscape),需要设置 wkhtmltopdf_set_global_setting(gs, "orientation", "Landscape");
  • 要支持 CSS3 的 transition 如 rotate 的话,需要 webkit 前缀: -webkit-transform: rotate(30deg);
  • 本地图片可以使用 Base64 格式,或者本地图片的绝对路径
  • 更多配置请参考
  • 生成的 PDF 中可以使用 CSS 强制插入页:
    • .pdf-new-page { page-break-before: always !important; }
    • <h2 class="pdf-new-page">灭菌图像</h2>
    • 这样 灭菌图像 就会从新的一页开始

本文介绍了使用 Inja + wkhtmltopdf,使用模板 + 数据的方式生成 PDF 报表,只要提高一下我们写 HTML 的技术,就能够很容易的生成漂亮的报表了。