用XLNT库读写Excel文件

经常会有生成Excel文件的需求,用C++生成Excel报表比较简单的方法是用MSOFFICE的OLE实现,这种兼容性最好,而且网上容易找到解决方案。但是这种方案要求电脑上装有MS Office软件,运行速度不高。

另一种方案就是直接读写EXCEL文件,感谢开源,我们可以找到不少读写EXCEL的库。XLNT是其中一个功能比较完整的C++开源库,基地在此:https://github.com/tfussell/xlnt

西加加斯基(www.cpp-prog.com) 原创,欢迎转载。

安装

再次感觉开源(和微软),我们可以用vcpkg这样方便的包管理工具,编译安装C++库和Linux下一样方便。直接输入下面的命令就可以装好xlnt了。

1
vcpkg install xlnt

等有空了专门写一下vcpkg,它的基地在此:https://github.com/microsoft/vcpkg, 后面的代码为了方便演示类型,使用了明确的名空间和类型名,实际写代码时推荐用auto会方便很多。

操作Workbook

记得包含头文件

1
#include <xlnt/xlnt.hpp>

首先从Workbook开始。
只需声明xlnt::workbook就可以新建一个Workbook,一番猛如虎的操作后调用void save(const std::string &filename)方法保存出Excel文件。

1
2
3
4
5
xlnt::workbook wb;
xlnt::worksheet ws = wb.active_sheet();
ws.cell("A1").value(5);
ws.cell("B2").value("string data");
wb.save("example.xlsx");

打开一个Excel文件同样简单

1
2
3
4
xlnt::workbook wb;
wb.load("example.xlsx");
xlnt::worksheet ws = wb.active_sheet();
std::cout << ws.cell("A1").value<int>() << std::endl;

操作Worksheet

在XLNT里,我们可以把xlnt::workbook看成是xlnt::worksheet的容器:

1
2
3
4
5
6
xlnt::workbook wb;
wb.load("example.xlsx");
for(auto& sheet : wb)
{
std::cout << sheet.title() << std::endl;
}

而且xlnt::workbook提供了很多与Worksheet相关的方法

  • create_sheetcopy_sheet方法来生成新表
  • remove_sheet 方法删除表
  • active_sheet 得到当前活动表
  • sheet_by_title,sheet_by_index 用于得到指定名字或序号的表,

操作Cell

Cell就是Excel里的单元格,XLNT中类型是xlnt::cell,前面的例子已经出场过了。

定位

单元格可以通过”A1”, “C5”这样的EXCEL地址来定位, 如:

1
ws.cell("A1").value(5);

也可以用行列序号来定位,左上角是(1,1),如:

1
ws.cell(2,1).value(5); // 第二列, 第一行(即B1)数值设置成5

XLNT库提供了xlnt::cell_reference类来表示位置信息,用它可以方便的在行列与EXCEL地址之间转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xlnt::cell_reference cr("C5");
std::cout
<< "C5"
<< " Row:"
<< cr.row()
<< " Column:"
<< cr.column_index()
<< std::endl;

cr.row(10);
cr.column_index(10);
std::cout
<< "[C10R10] address is "
<< cr.to_string()
<< std::endl;

// 可以用cell_reference作为参数得到cell
// cell对象也可以取得对应的cell_reference
xlnt::cell cell = ws.cell(cr);
xlnt::cell_reference cr2 = cell.reference();
//...

遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xlnt::worksheet ws = wb.active_sheet();
for(xlnt::cell_vector cell_vec : ws)
{
for(xlnt::cell cell: cell_vec)
{
if(cell.has_value())
{
std::cout
<< cell.reference().to_string()
<< ":"
<< cell.to_string()
<< std::endl;
}
}
}

如果把一张表(Worksheet)看成是一个二维单元格数组的话,那么xlnt::cell_vector就是一维单元格数组,这样看上面的聚合关系就明确了。

我们还可以用下面的方法得到现有数据在表中的最大使用范围:

1
2
3
4
row_t worksheet::highest_row()
row_t worksheet::lowest_row()
column_t worksheet::lowest_column()
column_t worksheet::highest_column()

数值读写

1
2
3
4
5
6
bool cell::has_value() const;
void cell::clear_value();
void cell::value(T data);
T cell::value<T>() const;
type cell::data_type() const;
std::string cell::to_string() const;

value是一个模板方法,可以接受或返回多种数据类型。data_type指明了当前数据类型。

公式读写

1
2
3
4
bool cell::has_formula() const;
void cell::clear_formula();
void cell::formula(const std::string &formula);
std::string cell::formula() const;

公式以等号=开头

注释读写

1
2
3
4
bool cell::has_comment();
void cell::clear_comment();
class comment cell::comment();
void cell::comment(const class comment &new_comment);

Range

一个矩形区域内的单元格可以组成一个xlnt::range

1
2
3
4
xlnt::workbook wb;
xlnt::worksheet ws = wb.active_sheet();
ws.range("B2:D5").fill(xlnt::fill::solid(xlnt::color::yellow()));
wb.save("example.xlsx");

同样的, Range也是一个二维表格, 所以它也是xlnt::cell_vector的容器。遍历方法与前面Worksheet的相同。

定位

Range的定位要左上角和右下角,除了刚才演示的EXCEL地址写法,也可以用两个xlnt::cell_reference或四个数字指定。
与Cell一样,XLNT库也提供了xlnt::range_reference类来表示位置信息,用它可以方便的在行列与EXCEL地址之间转化。

1
xlnt::range rg = ws.range(xlnt::range_reference{2, 2, 5, 4});
1
2
3
xlnt::range rg = ws.range(xlnt::range_reference{
xlnt::cell_reference(2, 2),
xlnt::cell_reference(5, 4)});

另外,上面这个也可以简写成这样:

1
2
3
xlnt::range rg = ws.range({
xlnt::cell_reference(2, 2),
xlnt::cell_reference(5, 4)});

字体, 边框, 对齐, 填充

xlnt::cell有一个format方法,可以设置和取得格式。不过更简单的设置方法是这样的:

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
xlnt::workbook wb;
xlnt::worksheet ws = wb.active_sheet();
// 字体演示
xlnt::cell a1 = ws.cell("A1");
a1.value("Hi");
a1.font(
xlnt::font()
.size(20)
.bold(true)
.name("Arial"));
// 对齐演示
xlnt::cell a2 = ws.cell("A2");
a2.value("Hello\nWorld");
a2.alignment(
xlnt::alignment()
.wrap(true)
.rotation(45)
.horizontal(xlnt::horizontal_alignment::center)
.vertical(xlnt::vertical_alignment::center));
// 填充演示
xlnt::cell a3 = ws.cell("A3");
a3.value("Rainbow");
a3.fill(
xlnt::gradient_fill()
.right(1).bottom(1)
.add_stop(0, xlnt::color::red())
.add_stop(0.5, xlnt::color::green())
.add_stop(1, xlnt::color::blue()));
// 边框演示
xlnt::border bd;
xlnt::border::border_property bp;
bp.color(xlnt::color::black());
bp.style(xlnt::border_style::dotted);
for (auto side : {
xlnt::border_side::bottom,
xlnt::border_side::end,
xlnt::border_side::horizontal,
xlnt::border_side::start,
xlnt::border_side::top,
xlnt::border_side::vertical
})
bd.side(side, bp);
xlnt::range A1A3 = ws.range({a1.reference(), a3.reference()});
A1A3.border(bd);

wb.save("example.xlsx");

xlnt::range也有对应的方法

其它

上面介绍的功能只是XLNT库的一小部分,还有更多功能不能涉及,官方文档也不丰富。这里必须再再次感谢开源,我们可以直接看代码知道内部实现,了解功能。

问题和坑

用第三方库来读写EXCEL有一个很讨厌的问题就是兼容性,虽然xlsx也算是一种公开的格式,但谁也不知道微软在EXCEL里做了什么特殊的小动作。
所以我不推荐用XLNT来读取EXCEL保存的文件,至少目前会遇到各种问题。请坚持只用XLNT读写自己写的文件。
下面是我在用的过程中遇到的一些问题,(XLNT 1.4.0)

  1. 删除行和删除列操作不会处理合并单元格,如“A1:A5”合并,这时删除第一行后会发现第一格并没有变成”A1:A4”,而且原来A6的数据不见了。
  2. 遍历时单元格用formula写的公式不会被has_valuehas_formula探测到。
  3. XLNT打开Excel写的文件时抛异常。