类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。

问题

  1. 还未对文本中正常的"<"做特殊处理,当文本包含"<"时将导致解析错误。
  2. 使用ScrollDC的方式实现对齐,比预先解析每行宽度效率更低。
原文地址:https://www.cnblogs.com/armageddon/p/3032596.html