基于 RTF 的报表生成函数(C++)

笔者尝试生成任意格式的报表,期望功能强和技术门槛低。

发现除了基于 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值