zoukankan      html  css  js  c++  java
  • 类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的方式实现对齐,比预先解析每行宽度效率更低。
  • 相关阅读:
    前端开发试题
    操作手册
    border-box有什么用
    npm安装react-dom...
    html-webpack-plugin按需加载的js/css也会被提取出来吗
    洛谷P3957 跳房子(Noip2017普及组 T4)
    【react】利用prop-types第三方库对组件的props中的变量进行类型检测
    React 进入页面以后自动 focus 到某个输入框
    React 更新阶段的生命周期 componentWillReceiveProps->shouldComponentUpdate->componentWillUpdate
    React 生命周期 constructor->componentWillMount->render->componentDidMount->componentWillUnmount
  • 原文地址:https://www.cnblogs.com/armageddon/p/3032596.html
Copyright © 2011-2022 走看看