类HTML语法显示格式化文本
介绍
项目需要,在自定义控件中显示格式化文本。
支持格式化的文本语法,接触过的有HTML、RTF等。
由于HTML使用广泛,决定采用类似HTML的语法。
该语法按树状结构组织,需要支持以下格式:
- 对齐:垂直居中对齐;水平居左居中居右对齐
- 换行:\n
- 颜色:<color="...">...</color>
- 图标:<icon="..."/>
*: 对齐在显示整个文本时统一指定。
假设有以下文本:
普通文本<color="#FF0000">红色文本<icon="icon.ico"/></color><color="#0000FF">蓝色文本\n跨行文本</color>剩余文本
可以解析为如下的语法树:
root┬普通文本 ├color┬红色文本 │ └icon ├color─蓝色文本\n跨行文本 └剩余文本
*: 整个文本包含在隐含的 root 节点中。
代码
首先从显示整个root节点的内容开始:
LPCTSTR DrawContent(HDC hdc, LPCTSTR szText, int xStart, LPRECT pRect, long *pcxMax, long *pcyLine, UINT fAlign, long lRowHeight) { while (LPCTSTR szStartOfNode = _tcschr(szText, _T('<'))) { // 显示前置文本 if (szStartOfNode > szText) szText = DrawText(hdc, szText, szStartOfNode-szText, xStart, pRect, pcxMax, pcyLine, fAlign); // 判断新节点还是关闭节点 szText = szStartOfNode + 1; if ('/' != *szText) { if (0 == _tcsnicmp(szText, _T("color=\""), 7)) { szText += 7; int r = 0, g = 0, b = 0; if (3 == _stscanf_s(szText, _T("#%2x%2x%2x\">"), &r, &g, &b)) { COLORREF dwColor = ::SetTextColor(hdc, RGB(r, g, b)); szText = DrawContent(hdc, szText+9, xStart, pRect, pcxMax, pcyLine, fAlign, lRowHeight); ::SetTextColor(hdc, dwColor); if (NULL != szText) { if (0 == _tcsnicmp(szText, _T("color>"), 6)) szText += 6; else return NULL; } else return NULL; } else return NULL; } else if (0 == _tcsnicmp(szText, _T("icon=\""), 6)) { szText += 6; TCHAR szIcon[MAX_PATH+1] = _T(""); if (1 == _stscanf_s(szText, _T("%[^\"]\"/>"), szIcon, MAX_PATH+1)) { if (*pcyLine < 16) *pcyLine = 16; // TODO:...延迟显示图标 if (pRect->left + 16 <= pRect->right) { RECT rcIcon = {pRect->left, pRect->top, pRect->left+16, pRect->top+16}; FillRect(hdc, &rcIcon, (HBRUSH)GetStockObject(BLACK_BRUSH)); pRect->left += 16; } else pRect->left = pRect->right; szText += _tcslen(szIcon) + 3; } else return NULL; } else return NULL; } else return ++szText; } // 显示后置文本 return DrawText(hdc, szText, -1, xStart, pRect, pcxMax, pcyLine, fAlign); }
循环查找"<"作为节点的开始。
如果从文本开始到节点开始之间有字符串,则调用"DrawText"显示这段字符串(稍后介绍),比如上面的"普通文本"。
判断"<"后面是否紧跟"/",如果是,说明是关闭节点,直接返回("root"是隐含节点,无需返回)。如果不是,说明是开始节点,判断节点类型。
如果是"color"节点,解析并设置当前颜色,然后递归调用DrawContent显示"color"节点内部内容,比如上面的"红色文本<icon="icon.ico"/>"和"蓝色文本\n跨行文本",最后还原颜色,并检查关闭节点是否匹配。
如果是"icon"节点,解析图标名并显示,然后移动后续显示坐标。
当查找完所有的"<"后,显示剩余的字符串,比如上面的"剩余文本"。
接下来看一下显示字符串的部分:
LPCTSTR DrawText(HDC hdc, LPCTSTR szText, int nLen, int xStart, LPRECT pRect, long *pcxMax, long *pcyLine, UINT fAlign) { if (-1 == nLen) nLen = _tcslen(szText); while (LPCTSTR szEndOfLine = (LPCTSTR)wmemchr(szText, _T('\n'), nLen)) { int nSize = szEndOfLine - szText; // 显示单行文字 szText = DrawText(hdc, szText, nSize, pRect, pcyLine) + 1; nLen -= nSize + 1; // 根据对齐方式移动DC HorzScroll(hdc, xStart, pRect, *pcyLine, fAlign); // 保存所有显示行中的最大宽度 if (*pcxMax < pRect->left) *pcxMax = pRect->left; // 移动显示位置 pRect->left = xStart; pRect->top += *pcyLine; *pcyLine = 0; } // 显示剩余文字 if (nLen > 0) { szText = DrawText(hdc, szText, nLen, pRect, pcyLine); nLen -= nLen; } return szText; }
循环查找"\n",将字符串分拆为多行。
调用"DrawText"显示单行字符串。
根据对齐方式,调用"HorzScroll"水平对齐当前行(稍后介绍)。
保存所有行中,最大的显示宽度。
将显示X坐标移动到行首,并下移一行。
当查找完所有的"\n"后,显示剩余的字符串。
显示单行字符串:
LPCTSTR DrawText(HDC hdc, LPCTSTR szText, int nLen, LPRECT pRect, long *pcyLine) { if (pRect->right > pRect->left) { SIZE sz = {0}; if (GetTextExtentPoint(hdc, szText, nLen, &sz)) { // 判断是否超长 if (pRect->right >= pRect->left + sz.cx) { TextOut(hdc, pRect->left, pRect->top, szText, nLen); pRect->left += sz.cx; } else { ::DrawText(hdc, szText, nLen, pRect, DT_END_ELLIPSIS|DT_SINGLELINE); pRect->left = pRect->right; } if (*pcyLine < sz.cy) *pcyLine = sz.cy; } } return szText + nLen; }
判断当前显示位置,如果没有空间就不必要显示。
获取字符串显示宽度。
根据需要完整或裁减尾部的方式显示字符串。
最后记录下当前行的最高行高。
再来看下水平对齐:
void HorzScroll(HDC hdc, int xStart, LPRECT pRect, long cyLine, UINT fAlign) { long lOffset = pRect->right - pRect->left; if (lOffset > 0) { RECT rcScroll = {xStart, pRect->top, pRect->left, pRect->top+cyLine}; RECT rcUpdate = {0}; if (TA_CENTER == (fAlign & TA_CENTER)) ScrollDC(hdc, lOffset/2, 0, &rcScroll, NULL, NULL, &rcUpdate); else if (TA_RIGHT == (fAlign & TA_RIGHT)) ScrollDC(hdc, lOffset, 0, &rcScroll, NULL, NULL, &rcUpdate); FillRect(hdc, &rcUpdate, hBrush); // hBrush为当前背景画刷 } }
获取当前行水平空间。
根据对齐方式水平"ScrollDC",并用背景刷填充移动后产生的空缺。
当调用"DrawContent"显示完root节点的内容后,事情还没有结束
让我们看一下最外层的"Draw"函数:
// 显示格式化文本 long cxMax = 0; long cyLine = 0; if (NULL == DrawContent(hMemoryDC, szText, 0, &rc, &cxMax, &cyLine, fAlign, lRowHeight)) // fAlign为文本对齐方式 return false; // 最后一行水平对齐 if (cxMax < rc.left) cxMax = rc.left; if (cyLine > 0) { HorzScroll(hMemoryDC, 0, &rc, cyLine, fAlign); rc.top += cyLine; } // 整体垂直对齐 int x = TA_CENTER==(fAlign&TA_CENTER)?(cxRect-cxMax)/2:(TA_RIGHT==(fAlign&TA_RIGHT)?cxRect-cxMax:0); long yOffset = rc.bottom - rc.top; if (yOffset >= 0) BitBlt(hdc, pRect->left+x, pRect->top+yOffset/2, cxMax, rc.top, hMemoryDC, x, 0, SRCCOPY); else BitBlt(hdc, pRect->left+x, pRect->top, cxMax, rc.top+yOffset, hMemoryDC, x, -yOffset/2, SRCCOPY);
最后一行的宽度还没有计入最大行宽,此处进行保存。
如果最后一行包含内容,行高不为0,进行水平对齐,并移动显示Y坐标到下一行。
前面所有的显示操作都是在内存DC中进行(创建销毁内存DC的代码不在本文中说明)。最后,将显示区域以最小的宽度和高度,按垂直居中对齐的方式显示到目标DC。
问题
- 还未对文本中正常的"<"做特殊处理,当文本包含"<"时将导致解析错误。
- 使用ScrollDC的方式实现对齐,比预先解析每行宽度效率更低。