minidocx 是一个跨平台且易于使用的 C++ 库,用于从零开始创建 Microsoft Word 文档。因为DOCX本质上是一个包含多个XML文件的ZIP包。可以使用特定的库和工具来完成这种转换,从而生成符合要求的docx文档。因此这个项目当中就是采用pugixml来进行XML的处理。
minidoc项目中创建的docx文档是minidocx项目中提供的Document类的实例,而Document类的内部属性信息存储在impl_指针指向的内存中,这种设计方式通过一个指向实现细节的指针来隐藏类的实现,从而减少类接口与实现之间的耦合。其指向的其实际上是一个结构体,结构如下,可以看出Document类的内部数据是通过xml数据格式来进行存储,所以这里实际上是通过pugixml来进行XML数据的处理。
struct Document::Impl
{
pugi::xml_document doc_; // 定义了一个pugi::xml_document类型的成员变量doc_,用于存储整个文档的XML数据,包含了文档的所有节点、属性等。
pugi::xml_node w_body_; // 定义了一个pugi::xml_node类型的成员变量w_body_,用于存储文档中w:body节点,这是文档的主要内容部分,包含了所有的段落、表格等内容。
pugi::xml_node w_sectPr_; // 定义了一个pugi::xml_node类型的成员变量w_sectPr_,用于存储文档中w:sectPr节点,这是文档段落或部分的属性节点,包含了节属性信息。
pugi::xml_document settings_; // 定义了一个pugi::xml_document类型的成员变量settings_,用于存储文档的设置XML数据。
pugi::xml_node w_settings_; // 定义了一个pugi::xml_node类型的成员变量w_settings_,用于存储文档设置中的w:settings节点。
unsigned int nextBookmarkId_; // 定义了一个无符号整数类型的成员变量nextBookmarkId_,用于存储下一个书签ID的值。
std::vector<Bookmark> bookmarks_; // 定义了一个std::vector类型的成员变量bookmarks_,用于存储文档中的书签集合。
};
pugixml以类似于DOM的方式存储XML数据:整个XML文档(文档结构和元素数据)都以树的形式存储在内存中。树的根是文档本身,它对应于C ++ type xml_document
。文档具有一个或多个子节点,它们对应于C ++ type xml_node
。节点具有不同的类型;根据类型,节点可以具有子节点的集合,与C ++类型相对应的属性的集合xml_attribute
以及一些其他数据(即名称)。
在项目当中通过Document doc;
创建doc对象,doc对象的构造函数如下,主要是对impl_指针指向的数据内容进行初始化,
Document::Document()
{
impl_ = new Impl;
impl_->doc_.load_buffer(DOCUMENT_XML, std::strlen(DOCUMENT_XML), pugi::parse_declaration); // 这行代码将DOCUMENT_XML字符串加载到impl_对象的doc_成员变量中,doc_是一个pugi::xml_document对象。pugi::parse_declaration表示在解析时包含XML声明。
impl_->w_body_ = impl_->doc_.child("w:document").child("w:body"); // 这行代码从解析的XML文档中找到名为w:document的根节点,然后找到它的子节点w:body并将其赋值给impl_对象的w_body_成员变量。
impl_->w_sectPr_ = impl_->w_body_.child("w:sectPr"); // 这行代码从w:body节点中找到名为w:sectPr的子节点并将其赋值给impl_对象的w_sectPr_成员变量。
impl_->settings_.load_buffer(SETTINGS_XML, std::strlen(SETTINGS_XML), pugi::parse_declaration); // 这行代码将SETTINGS_XML字符串加载到impl_对象的settings_成员变量中,settings_是一个pugi::xml_document对象。
impl_->w_settings_ = impl_->settings_.child("w:settings"); // 这行代码从解析的settings_文档中找到名为w:settings的根节点,并将其赋值给impl_对象的w_settings_成员变量。
impl_->nextBookmarkId_ = 0; // 这行代码初始化impl_对象的nextBookmarkId_成员变量,并将其设置为0
}
而mpl_->doc_.load_buffer(DOCUMENT_XML, std::strlen(DOCUMENT_XML), pugi::parse_declaration);
参数中的DOCUMENT_XML
记录了一个Word文档的基础结构,包含了文档的页面设置和一些元数据,内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex"
xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex"
xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex"
xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex"
xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex"
xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex"
xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex"
xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex"
xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink"
xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:oel="http://schemas.microsoft.com/office/2019/extlst"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex"
xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid"
xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml"
xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash"
xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex"
xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh wp14">
<w:body>
<w:sectPr>
<w:pgSz w:w="11906" w:h="16838" />
<w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="851" w:footer="992" w:gutter="0" />
<w:cols w:space="425" />
<w:docGrid w:type="lines" w:linePitch="312" />
</w:sectPr>
</w:body>
</w:document>
注解:
文档主体 <w:body>:
节属性 <w:sectPr>:
页面大小 <w:pgSz>:
页面边距 <w:pgMar>
页面边距 <w:pgMar>
文档网格 <w:docGrid>
xmlns
:xmlns
是 “XML Namespace” 的缩写,ns 代表 “namespace”。命名空间在 XML 中用于确保元素和属性名称的唯一性,避免冲突,并提供明确的语义。
比如"xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
:是WordprocessingML的命名空间,定义了文档中使用的所有Word相关元素。而其他命名空间(如xmlns:wpc
、xmlns:cx
等)是为了支持特定的Word功能,如绘图、图表、兼容性等。另外由于xml具有独立性,因此每个XML文档都需要声明它所使用的命名空间,即使这些命名空间在多个文档中是重复的。这样做可以确保每个文档都能独立地解析和理解。
因此当对XML解析之后的操作:
1、impl_->w_body_ = impl_->doc_.child("w:document").child("w:body");
这行代码从解析的XML文档中找到名为w:document
的根节点,然后找到它的子节点w:body并将其赋值给impl_对象的w_body_
成员变量。
2、impl_->w_sectPr_ = impl_->w_body_.child("w:sectPr");
这行代码从w:body
节点中找到名为w:sectPr
的子节点并将其赋值给impl_
对象的w_sectPr_
成员变量。也就是impl_->w_sectPr_
中存储着文档的节属性信息sectPr
。
impl_->settings_.load_buffer(SETTINGS_XML, std::strlen(SETTINGS_XML), pugi::parse_declaration);
这行代码将SETTINGS_XML字符串加载到impl_对象的settings_成员变量中,settings_是一个pugi::xml_document对象。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:settings xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex"
xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid"
xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml"
xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash"
xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex"
xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh">
</w:settings>
这段XML片段定义了Word文档的设置部分,并声明了许多命名空间,以支持不同版本和扩展的Word功能。这些命名空间确保了文档可以在不同的Word版本中正确解析和显示。
AppendParagraph()接口的细节:
在创建了Document对象后,可以通过auto p1 = doc.AppendParagraph("Hello, World!", 12, "Times New Roman");
来向文档添加段落,Document
的AppendParagraph
方法内部实现如下:
Paragraph Document::AppendParagraph(const std::string& text, // 文本
const double fontSize, // 字体大小
const std::string& fontAscii, //
const std::string& fontEastAsia)
{
Paragraph p = AppendParagraph();
p.AppendRun(text, fontSize, fontAscii, fontEastAsia);
return p;
}
因此文档内部添加段落是通过Document::AppendParagraph()
实现的,实现细节:
Paragraph Document::AppendParagraph() // 定义了Document类的成员函数AppendParagraph,返回类型是Paragraph。
{
if (!impl_) return Paragraph(); // 检查impl_指针是否为空。如果为空,返回一个默认构造的Paragraph对象,表示无法添加段落。
pugi::xml_node w_p = impl_->w_body_.insert_child_before("w:p", impl_->w_sectPr_); // 在impl_->w_body_节点中,在impl_->w_sectPr_节点之前插入一个新的子节点w:p,表示一个段落,并将其赋值给变量w_p。
pugi::xml_node w_pPr = w_p.append_child("w:pPr"); // 在新创建的段落节点w_p中添加一个子节点w:pPr,表示段落属性,并将其赋值给变量w_pPr
Paragraph::Impl* impl = new Paragraph::Impl; // 动态分配内存并创建一个新的Paragraph::Impl对象,并将其指针赋值给变量impl
impl->w_body_ = impl_->w_body_; // 将Document对象的impl_成员中的w_body_节点指针赋值给新创建的Paragraph::Impl对象的w_body_成员。
impl->w_p_ = w_p; // 将新创建的段落节点w_p赋值给新创建的Paragraph::Impl对象的w_p_成员
impl->w_pPr_ = w_pPr; // 将新创建的段落属性节点w_pPr赋值给新创建的Paragraph::Impl对象的w_pPr_成员。
return Paragraph(impl);
}
解析:
impl_->w_body_
当中存储的是
<w:body>
<w:sectPr>
<w:pgSz w:w="11906" w:h="16838" />
<w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="851" w:footer="992" w:gutter="0" />
<w:cols w:space="425" />
<w:docGrid w:type="lines" w:linePitch="312" />
</w:sectPr>
</w:body>
因此pugi::xml_node w_p = impl_->w_body_.insert_child_before("w:p", impl_->w_sectPr_);
是先在w:sectPr
节点前插入新的子节点w:p
,表示一个段落,并将其赋值给变量w_p。插入新的子节点后,
<w:body>
<w:p />
<w:sectPr>
<w:pgSz w:w="11906" w:h="16838" />
<w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="851" w:footer="992" w:gutter="0" />
<w:cols w:space="425" />
<w:docGrid w:type="lines" w:linePitch="312" />
</w:sectPr>
</w:body>
实际就是在<w:sectPr>...</w:sectPr>
前添加了<w:p />
节点,要注意一下段落对象的impl_
指针指向的结构体中存储的内容如下,也就是
struct Paragraph::Impl
{
pugi::xml_node w_body_; // 定义了一个pugi::xml_node类型的成员变量w_body_,用于存储段落所在文档的w:body节点
pugi::xml_node w_p_; // 定义了一个pugi::xml_node类型的成员变量w_p_,用于存储当前段落的w:p节点
pugi::xml_node w_pPr_; // 定义了一个pugi::xml_node类型的成员变量w_pPr_,用于存储当前段落的属性w:pPr节点
};
因此新建段落,也就是调用Paragraph Document::AppendParagraph()
,之后,实际上就是创建了一个Paragraph对象,而该对象中的impl_
指针指向的就是上诉结构体。
向段落中添加文字:Run Paragraph::AppendRun()
接口如下:
Run Paragraph::AppendRun()
{
if (!impl_) return Run(); // 检查impl_指针是否为空。如果为空,返回一个默认构造的Run对象,表示无法添加运行节点。
// impl_->w_p_.print(std::cout);
// std::cout.flush();
pugi::xml_node w_r = impl_->w_p_.append_child("w:r"); // 在当前段落节点impl_->w_p_中添加一个子节点w:r,表示一个新的运行节点,并将其赋值给变量w_r。
pugi::xml_node w_rPr = w_r.append_child("w:rPr"); // 在新创建的运行节点w_r中添加一个子节点w:rPr,表示运行属性,并将其赋值给变量w_rPr。
Run::Impl* impl = new Run::Impl;
impl->w_p_ = impl_->w_p_; // 将当前段落节点impl_->w_p_的指针赋值给新创建的Run::Impl对象的w_p_成员。
impl->w_r_ = w_r; // 将新创建的运行节点w_r赋值给新创建的Run::Impl对象的w_r_成员。
impl->w_rPr_ = w_rPr; // 将新创建的运行属性节点w_rPr赋值给新创建的Run::Impl对象的w_rPr_成员。
return Run(impl);
}
从上面代码可以看出,向段落添加文本主要分成几步:
1、pugi::xml_node w_r = impl_->w_p_.append_child("w:r");
:向impl_->w_p_
中添加子节点,这里的imp_
是 Paragraph::imp_
,也就是段落的成员变量,impl_->w_p_
在添加子节点之前的内容是:
<w:p>
<w:pPr />
</w:p>
而在执行pugi::xml_node w_r = impl_->w_p_.append_child("w:r");
和pugi::xml_node w_rPr = w_r.append_child("w:rPr");
之后,impl_->w_p_
的内容变为:
<w:p>
<w:pPr />
<w:r>
<w:rPr />
</w:r>
</w:p>
可以看出其实添加文本实际上也就是新建了一个Run
对象,impl->w_p_ = impl_->w_p_;
表示创建的Run::Impl
对象的w_p_
成员存储着当前段落节点impl_->w_p_
,除此之外对象成员还存储着当前运行节点w_r
和运行节点属性w_rPr
,这也对应着Run::Impl
对应的成员变量:
struct Run::Impl
{
pugi::xml_node w_p_;
pugi::xml_node w_r_;
pugi::xml_node w_rPr_;
};
设置文本字体接口:void Run::SetFontSize(const double fontSize)
void Run::SetFontSize(const double fontSize)
{
if (!impl_) return;
pugi::xml_node sz = impl_->w_rPr_.child("w:sz");
if (!sz) {
sz = impl_->w_rPr_.append_child("w:sz");
}
pugi::xml_attribute szVal = sz.attribute("w:val");
if (!szVal) {
szVal = sz.append_attribute("w:val");
}
// font size in half-points (1/144 of an inch)
szVal.set_value(fontSize * 2);
}
设置字体格式接口:void Run::SetFont( const std::string& fontAscii, const std::string& fontEastAsia)
void Run::SetFont(
const std::string& fontAscii,
const std::string& fontEastAsia)
{
if (!impl_) return;
pugi::xml_node rFonts = impl_->w_rPr_.child("w:rFonts");
if (!rFonts) {
rFonts = impl_->w_rPr_.append_child("w:rFonts");
}
pugi::xml_attribute rFontsAscii = rFonts.attribute("w:ascii");
if (!rFontsAscii) {
rFontsAscii = rFonts.append_attribute("w:ascii");
}
pugi::xml_attribute rFontsEastAsia = rFonts.attribute("w:eastAsia");
if (!rFontsEastAsia) {
rFontsEastAsia = rFonts.append_attribute("w:eastAsia");
}
rFontsAscii.set_value(fontAscii.c_str());
rFontsEastAsia.set_value(fontEastAsia.empty()
? fontAscii.c_str()
: fontEastAsia.c_str());
}```