笔者尝试生成任意格式的报表,期望功能强和技术门槛低。
发现除了基于 HTML 外,剩下可能的最佳方案就是基于 RTF。
基本做法是:用 Word 编辑报表模板,保存为可保留绝大多数 Word 格式的 RTF 文件,然后根据内部的关键字生成报表。
该方案实现需要对 RTF 规范了解,但一旦实现,其通用性、实用性极强,理论上可以解决所有 C/S类软件的报表问题。
已实际应用的函数代码如下:
///<summary>
/// 根据原始 RTF 代码和 "关键词-值" 映射生成用于打印的 RTF 代码,平台无关,其中关键词的格式为 [关键词]。
/// 其中预定义关键词如下:
/// [PAGEBREAK] 分页标记
/// [PAGECOUNT] 总页数
/// [PAGENUM] 当前页
/// [ID] 动态行及序号, mVal['[ID]'] 的值是动态行数量
/// [IMG:关键词.高度] 图片(图片数据输入的相关代码需自行修改完成)
///</summary>
///<include>string、map、filesystem、assert.h</include>
///<param name="mVal">欲替换文本内容 [...] 和替换文本内容的映射</param>
///<param name="sRtf">原始 RTF 文件内容,内部有欲替换标记 [...],从已编辑完成的 RTF 文件直接读取</param>
///<param name="nMode">
/// 0-正常
/// 1-套打,隐藏原模板内容,显示已被替换的内容
/// 2-模板,隐藏被替换内容,显示未被替换的内容
/// 3-原始
/// </param>
///<param name="bMark">是否以不同的背景色标记替换内容,替换成功时淡绿或失败时淡红</param>
///<param name="nMaxPagings">最多动态行数,原 RTF 内容中 [ID] 所在的表格行将复制出 mVal['[ID]'] 数量的多行,其中 [ID] 替换成序号,行数超过 nMaxPagings 时插入 [PAGEBREAK] 分页标记。</param>
///<param name="bFullPage">动态生成表格行时是否自动填充全页,例如:最多动态行数=10,mVal['[ID]']=5,生成的 RTF 第二页表格行数量为 5,将补足至 10</param>
std::string GetPrintRtf(std::map<std::wstring, std::wstring> mVal, std::string sRtf, unsigned int nMode, bool bMark, int nMaxPagings, bool bFullPage)
{
if (sRtf.find("{\\*\\generator") == std::string::npos || sRtf.find("\\viewkind4\\uc1") == std::string::npos) // 检查 RTF 文件格式
{
assert(false);
return "";
}
int nClrCount = 0; // 已定义颜色数量
for (size_t pos = sRtf.find("{\\colortbl"); (pos = sRtf.find("\\red", pos + 1)) != std::string::npos && pos < sRtf.find("{\\*\\generator"); )
nClrCount++;
// 无颜色定义表时加入
if (sRtf.find("{\\colortbl") == std::string::npos)
sRtf.insert(sRtf.find("{\\*\\generator"), "{\\colortbl ;}");
// 加入颜色定义(白色、淡红(错误)、淡绿(警告))
sRtf.insert(sRtf.find('}', sRtf.find("{\\colortbl")), "\\red255\\green255\\blue255;\\red255\\green160\\blue160;\\red160\\green255\\blue160;");
std::string sClrWhite = "\\cf" + std::to_string(nClrCount + 1); // 隐藏文本色(白色)
std::string sClrBkErr = "\\highlight" + std::to_string(nClrCount + 2); // 错误背景色(淡红)
std::string sClrBkInf = "\\highlight" + std::to_string(nClrCount + 3); // 警告背景色(淡绿)
char sTmp[128] = { 0 }; // 临时变量
const size_t DOCBGN = sRtf.find("\\viewkind4\\uc1"); // RTF 文档内容开始位置
// 1 像素透明空白图片 RTF 代码
const char* NULLIMG = "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000a49444154785e63000100000500019b3b067a0000000049454e44ae426082";
// 删除关键词内部全部样式定义
for (size_t b = 0, nBlankLV = 0, p = DOCBGN; p < sRtf.size(); p++)
{
if (sRtf[p] == '[')
{
if ((nBlankLV++) == 0)
b = p;
}
else if (sRtf[p] == ']')
{
if ((--nBlankLV) == 0)
{
for (size_t s = b; s < p; s++)
{
if (sRtf[s] == '\\' && sRtf[s + 1] != '\'')
{
size_t e = s + 1;
while (e < sRtf.size() && sRtf[e] != ' ' && sRtf[e] != '\\')
e++;
if (sRtf[e] == ' ')
e++;
sRtf.replace(s, e - s, "");
p -= e - s;
s--;
}
}
}
}
}
if (nMode != 3) // 0-正常 1-套打(隐藏原模板内容) 2-仅模板(隐藏被替换内容) 3-原始
{
if (nMode == 1) // 套打时隐藏表格边框、图片
{
// 隐藏表格边框
for (size_t posB = DOCBGN; (posB = sRtf.find("\\brdrcf", posB)) != std::string::npos; ) // 删除已有边框颜色定义
{
size_t posE = posB + ::strlen("\\brdrcf");
while (::isdigit(sRtf[posE])) posE++;
sRtf.replace(posB, posE - posB, "");
}
for (size_t pos = 0; (pos = sRtf.find("\\brdrs", pos + 1)) != std::string::npos; ) // 设置所有边框为白色
sRtf.insert(pos + ::strlen("\\brdrs"), "\\brdrcf" + std::to_string(nClrCount + 1));
// 隐藏图片(用占位白色图片替换)
for (size_t posB = DOCBGN, posW = 0, posH = 0, posE = 0;
(posB = sRtf.find("{\\pict\\", posB + 1)) != std::string::npos &&
(posW = sRtf.find("picwgoal", posB + 1)) != std::string::npos &&
(posH = sRtf.find("pichgoal", posB + 1)) != std::string::npos &&
(posE = sRtf.find('}', posH)) != std::string::npos; )
{
int nTwW = ::atoi(sRtf.substr(posW + ::strlen("picwgoal")).c_str()); // 图片占位宽度(缇,96dpi 下缇=像素×22)
int nTwH = ::atoi(sRtf.substr(posH + ::strlen("pichgoal")).c_str()); // 图片占位高度(缇,96dpi 下缇=像素×22)
::sprintf_s(sTmp, "{\\pict\\pngblip\\picwgoal%d\\pichgoal%d ", nTwW, nTwH); // 按图片占位尺寸生成图片 RTF 代码
sRtf.replace(posB, posE - posB + 1, std::string(sTmp) + NULLIMG + "}");
}
}
// 图片关键词(以 [IMG: 开头)替换成图片 RTF 代码
for (size_t posB = DOCBGN, posP = 0, posE = 0;
(posB = sRtf.find("[IMG:", posB + 1)) != std::string::npos && // 图片关键词固定以 [IMG: 开头
(posP = sRtf.find('.', posB + 1)) != std::string::npos &&
(posE = sRtf.find(']', posB + 1)) != std::string::npos; )
{
std::string ss = sRtf.substr(posB, 20);
std::string sImgKey = sRtf.substr(posB + 1, posP - posB - 1); // 图片关键词
std::string sHeight = sRtf.substr(posP + 1, posE - posP);
int nImgMaxH = std::min<int>(std::max<int>(::atoi(sHeight.c_str()), 16), 0xFFF); // 图片占位高度(像素)
// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
// 需自行改写的业务关联代码:根据图片关键词获取图片数据及图片信息 ...
std::string imgData; // 图片二进制数据
if (sImgKey == "IMG:LOGO") imgData = GetCurBin(m_xStdLogo);
else if (sImgKey == "IMG:BIGLOGO") imgData = GetCurBin(m_xBigLogo);
else if (sImgKey == "IMG:HOMEPAGE") imgData = CXImage::CreateQrcode(GetCurStF(GetHomePage).c_str(), nImgMaxH);
else if (sImgKey == "IMG:WXOPEN") imgData = CXImage::CreateQrcode(GetCurStF(GetWxOpenUrl).c_str(), nImgMaxH);
else if (sImgKey == "IMG:CONTACT") imgData = CXImage::CreateQrcode(GetCurStr(m_qContactUrl).c_str(), nImgMaxH);
else assert(false);
bool bAlpha = CXImage(imgData).IsAlpha(); // 图片是否透明
double fRadio = CXImage(imgData).GetWHRadio(); // 图片宽高比
// ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
std::string sRtfCodeImg;
if (nMode != 2) // 0-正常 1-套打(隐藏原模板内容) 2-仅模板(隐藏被替换内容) 3-原始
{
for (size_t pos = 0; pos < imgData.size(); pos++)
{
::sprintf_s(sTmp, "%02x", (unsigned char)imgData[pos]);
sRtfCodeImg += sTmp;
}
}
else
{
sRtfCodeImg = NULLIMG;
}
::sprintf_s(sTmp, "{\\pict\\%sblip\\picwgoal%d\\pichgoal%d ", nMode == 2 || bAlpha ? "png" : "jpeg", int(nImgMaxH * 22 * fRadio), nImgMaxH * 22); // 图片 RTF 代码头部分
sRtf.replace(posB, posE - posB + 1, sTmp + sRtfCodeImg + "}");
}
// 根据 [ID] 动态生成表格行:[ID] 所在表格行将复制出 ID 数量的多行,[ID] 替换成序号,行数超过设定值时插入 [PAGEBREAK] 分页标记。
if (mVal.find(L"[ID]") != mVal.end() && sRtf.find("[ID]") != std::string::npos)
{
int nID = ::_wtoi(mVal.find(L"[ID]")->second.c_str()); // 动态行数
size_t nIDP = sRtf.find("[ID]"); // 第一个 [ID] 位置
size_t nDocE = sRtf.find_last_of('}'); // 文档内容部分结束位置 }
size_t nRowB = sRtf.rfind("\\trowd", nIDP); // 从 [ID] 向前查找,找到表格行开始位置 \trowd
size_t nRowE = sRtf.find("\\row", nIDP); // 从 [ID] 向后查找,找到表格行结束位置 \row
assert(nID > 0 && DOCBGN < nRowB && nRowB < nRowE && nRowE < nDocE);
if (nID > 0 && DOCBGN < nRowB && nRowB < nRowE && nRowE < nDocE)
{
std::string sDocPre = sRtf.substr(0, DOCBGN); // 文档固定开始部分 【{\rtf1 ... \viewkind4\uc1】 ...
std::string sRtfPre = sRtf.substr(DOCBGN, nRowB - DOCBGN); // 动态行外文档内容开始部分 ...\viewkind4\uc1【...】\\trowd ... [ID] ...
std::string sRow = sRtf.substr(nRowB, nRowE - nRowB + ::strlen("\\row")); // [ID] 所在行的 RTF 代码 ...【\trowd ... [ID] ... \row】...
std::string sRtfSuf = sRtf.substr(nRowE + ::strlen("\\row"), nDocE - nRowE - ::strlen("\\row")); // 动态行外内容结束部分 ... [ID] ... \row【 ... \par】}
sRtf = sDocPre; // RTF 文档开始部分 【{\rtf1 ... \viewkind4\uc1】 ...
int nPageCount = nID / nMaxPagings + ((nID % nMaxPagings) == 0 ? 0 : 1); // 动态分页数量
// 根据实际的 ID 数展开表格行并分页
for (int nRowID = 1, nPageNum = 1; nPageNum <= nPageCount; nPageNum++)
{
if (nPageNum > 1)
sRtf += "[PAGEBREAK]"; // 插入分页标记
sRtf += sRtfPre; // 每页加入表格行(动态行)外 RTF 文档内容开始部分 ...\viewkind4\uc1【...】\trowd ... [ID] ...
// 复制 RTF 表格部分
while ((bFullPage || nRowID <= nID) && nRowID <= nPageNum * nMaxPagings)
{
std::string sRowTmp = sRow;
// [ID] 所在行的 RTF 代码 ...【\trowd ... [ID] ... \row】...
for (size_t pos = 0; (pos = sRowTmp.find("[ID]")) != std::string::npos; )
sRowTmp.replace(pos, 4, std::to_string(nRowID));
sRtf += sRowTmp; // 序号
nRowID++;
}
sRtf += sRtfSuf; // 表格行(动态行)外 RTF 文档内容结束部分 ... [ID] ... \row【 ... \par】}
}
sRtf += "}"; // RTF 文档固定以 } 结束
}
}
// 总页数
int nPageCount = 1;
for (size_t pos = DOCBGN; (pos = sRtf.find("[PAGEBREAK]", pos + 1)) != std::string::npos; ) nPageCount++;
mVal[L"[PAGECOUNT]"] = std::to_wstring(nPageCount);
// 页码
for (size_t pos = DOCBGN; (pos = sRtf.find("[PAGENUM]", pos)) != std::wstring::npos; )
{
int nPageNum = 1; // 当前位置的页码
for (size_t posP = 0; (posP = sRtf.find("[PAGEBREAK]", posP + 1)) != std::string::npos && posP <= pos; ) nPageNum++;
sRtf.replace(pos, ::strlen("[PAGENUM]"), "[PAGE" + std::to_string(nPageNum) + "]"); // 这里替换 [PAGENUM] 为 [PAGE(n)]
mVal[L"[PAGE" + std::to_wstring(nPageNum) + L"]"] = std::to_wstring(nPageNum);
}
}
// 添加必要的文本色和背景色以避免受后面的替换影响,但未实际改变替换前的样式
for (size_t pos = DOCBGN; pos < sRtf.size() - 3; pos++)
{
if (sRtf[pos] == '{')
{
pos = sRtf.find('}', pos);
}
else if (sRtf.substr(pos, 3) == " \r\n")
{
pos += 3;
}
else if (((sRtf[pos] == ' ' || sRtf[pos] == '[') && (sRtf[pos + 1] != '\\' || sRtf[pos + 2] == '\'')) || (sRtf[pos] == '\\' && sRtf[pos + 1] == '\'')) // 文本开始位置
{
// 向后找到文本结束位置
size_t e = pos + 1;
while (e < sRtf.size() - 3 && (sRtf[e] != '\\' || sRtf[e + 1] == '\''))
e++;
// 向前找到定义开始位置
size_t d = pos - 1;
while (d > 0 && sRtf[d] != '\\' && sRtf[d - 1] != ' ' && sRtf[d - 1] != '\r' && sRtf[d - 1] != '\n')
d--;
std::string sTxt = sRtf.substr(pos, e - pos); // 文字内容
std::string sDef = sRtf.substr(d, pos - d); // 定义内容
pos -= sDef.size();
std::string sClrTxtDefault = nMode != 1 ? "\\cf0" : sClrWhite; // 文字的默认色
std::string sClrKeyDefault = nMode != 2 ? "\\cf0" : sClrWhite; // 关键词的默认色
std::string sClrTbkDefault = "\\highlight0"; // 文字的默认背景色
std::string sClrKbkDefault = !bMark ? "\\highlight0" : sClrBkInf; // 关键词的默认背景色
if (sDef.find("\\cf") != std::string::npos) // 在定义中找到原有文字颜色
{
size_t posB = sDef.find("\\cf");
size_t posE = posB + ::strlen("\\cf");
while (sDef[posE] >= '0' && sDef[posE] <= '9') posE++; // 跳过数字部分
if (nMode != 1) sClrTxtDefault = sDef.substr(posB, posE - posB); // 文字的默认色
if (nMode != 2) sClrKeyDefault = sDef.substr(posB, posE - posB); // 关键词的默认色
sDef.replace(posB, posE - posB, ""); // 去除原文字颜色
}
if (sDef.find("\\highlight") != std::string::npos) // 在定义中找到原有背景颜色
{
size_t posB = sDef.find("\\highlight");
size_t posE = posB + ::strlen("\\highlight");
while (sDef[posE] >= '0' && sDef[posE] <= '9') posE++; // 跳过数字部分
if (nMode != 1) sClrTbkDefault = sDef.substr(posB, posE - posB); // 文字的默认背景色
if (!bMark) sClrKbkDefault = sDef.substr(posB, posE - posB); // 关键词的默认背景色
sDef.replace(posB, posE - posB, ""); // 去除原背景颜色
}
sDef += sClrTxtDefault + sClrTbkDefault; // 统一设置默认文字色和背景色
// 为将要被替换的关键词部分设置文字色和背景色
for (size_t posB = 0, posE = 0; (posB = sTxt.find('[', posE)) != std::string::npos && (posE = sTxt.find(']', posE)) != std::string::npos; )
{
sTxt.insert(posE + 1, sClrTxtDefault + sClrTbkDefault + " "); // 设置关键词后面默认文字色和背景色
sTxt.insert(posB, sClrKeyDefault + sClrKbkDefault + " "); // 设置关键词文字色和背景色
posE += sClrTxtDefault.size() + sClrTbkDefault.size() + sClrKeyDefault.size() + sClrKbkDefault.size() + 2;
}
sRtf.replace(pos, e - pos, sDef + sTxt);
pos = d + sDef.size() + sTxt.size();
}
}
// 统一替换关键词
if (nMode != 3) // 0-正常 1-套打(隐藏原模板内容) 2-仅模板(隐藏被替换内容) 3-原始
{
for (auto& it : mVal)
{
std::string sKey, sVal;
try { sKey = std::filesystem::path(it.first).string(); } catch (const std::exception&) { }; // Uncode 转 ANSI 字符串
try { sVal = std::filesystem::path(it.second).string(); } catch (const std::exception&) { }; // Uncode 转 ANSI 字符串
std::string sEncodeKey, sEncodeVal; // 关键词/替换值 RTF 编码
for (size_t pos = 0; pos < sKey.size(); pos++)
{
if (!isascii(sKey[pos])) ::sprintf_s(sTmp, "\\'%02x", (unsigned char)sKey[pos]);
sEncodeKey += !isascii(sKey[pos]) ? sTmp : sKey.substr(pos, 1);
}
for (size_t pos = 0; pos < sVal.size(); pos++)
{
if (!isascii(sVal[pos])) ::sprintf_s(sTmp, "\\'%02x", (unsigned char)sVal[pos]);
sEncodeVal += !isascii(sVal[pos]) ? sTmp : sVal.substr(pos, 1);
}
for (size_t pos = 0; (pos = sRtf.find(sEncodeKey, pos + 1)) != std::wstring::npos; )
sRtf.replace(pos, sEncodeKey.size(), sEncodeVal);
}
}
if (bMark) // 是否以不同的背景色标记替换内容,替换成功时淡绿或失败时淡红
{
for (size_t pos = 0; (pos = sRtf.find(sClrBkInf + " [", pos)) != std::string::npos; )
sRtf.replace(pos, sClrBkInf.size() + 2, sClrBkErr + " [");
}
return sRtf;
}