Duilib 源码分析之 xml 解析篇

来源:互联网 发布:淘宝开通花呗条件 编辑:程序博客网 时间:2024/06/07 02:03

上一篇文章介绍了 Duilib 是如何读取 xml 的 (Duilib 源码分析之 xml 加载篇),接下来介绍 Duilib 是如何解析加载到内存的 xml 的。

首先列出解析 xml 的几个关键类:
- UIDlgBuilder
- UIMarkup
- CMarkupNode

在 xml 加载篇的帖子中,已经介绍过,在窗口创建的消息相应函数 OnCreate 中,调用了 UIDlgBuilder ::Create 方法来加载 xml。 而实际把 xml 加载到内存并进行解析是由类 UIMarkup 完成的。

下面我们来着重介绍一下类 UIMarkup

  • 关键的数据结构类型:XMLELEMENT

    typedef struct tagXMLELEMENT{     ULONG iStart;     ULONG iChild;     ULONG iNext;     ULONG iParent;     ULONG iData;} XMLELEMENT;
    • iStart : 节点的开头位置,例如节点 xxxxx <Label xxxxx /> xxxxx , iStart = 指向 L 的指针 - m_pstrXML
    • iChild :第一个子节点的序号,这个序号指的是节点的序号,第一个有效节点的序号为 1, 节点 0 预留给解析错误的节点
    • iNext : 第一个兄弟节点的序号
    • iParent : 父节点的序号
    • iData : 节点结束的位置,以 /> 结束的节点结束位置为符号 / 的位置;以 <xxx>***</xxx> 形式出现的节点,结束位置为 <xxx> 的下一个字符位置
  • 解析的关键几个成员为类 UIMarkup 的几个私有函数:

    • XMLELEMENT* _ReserveElement()
      申请保存节点所需内存,并返回当前节点的数据结构,保存节点信息的成员函数为 m_pElements,这是一个数组, 刚开始就申请 500*sizeof(XMLELEMENT) 大小的内存控件用来保存节点信息,m_nReservedElements 刚开始为 500,代表预留节点数,当实际解析到的节点数大于这个值时,再重新申请更大的内存空间

    • void _SkipWhitespace(LPTSTR& pstr) const
      跳过接下来的空格、回车、换行符。判断方式为 *pstr <= _T(' '),回车和换行的 ASCII 码分别为 13 和 10, 空格的 ASCII 码为 32

    • void _SkipIdentifier(LPCTSTR& pstr)
      这个函数实现为 (*pstr == _T('_') || *pstr == _T(':') || _istalnum(*pstr)),跳过冒号、下划线、字母和数字,在代码中此函数有 2 处调用,第一处是开始解析某一节点时跳过此节点的名称,第二处是调用 _ParseAttributes 时跳过当前属性处理下一个属性用。

    • void _ParseMetaChar(LPTSTR& pstrText, LPTSTR& pstrDest)
      解析 xml 中可能出现的 &amp;&lt;&gt;&quot;&apos;,分别代表字符 &<>"'

    • bool _ParseAttributes(LPTSTR& pstrText)
      处理节点的所有属性,注意,这个函数并没有解析出这个节点有哪些属性,各个属性的值是多少,整个 _Parse 的过程其实只是构造了所有的节点信息: m_pElements,并修改了内存中的 xml 字符串的内容: m_pstrXML指向的这部分内存,修改 xml 字符串的内容只是为了解析属性做准备,后续会根据所有的节点信息逐个取得属性并创建控件,这个稍后具体再讲。此函数的退出条件是当前解析到的字符为 / 或 >。

    • bool _Parse(LPTSTR& pstrText, ULONG iParent)

      • 这个函数主要就是一个 for 循环 ,每一个 for 循环代表对一个节点的解析
      • 每个 for 循环的开始都会调用 _SkipWhitespace ,为了跳过 2 个节点之间的空白部分
      • 以下代码片段:

        if( *pstrText == _T('!') || *pstrText == _T('?') ) {    TCHAR ch = *pstrText;    if( *pstrText == _T('!') ) ch = _T('-');    while( *pstrText != _T('\0') && !(*pstrText == ch && *(pstrText + 1) == _T('>')) )        pstrText = ::CharNext(pstrText);    if( *pstrText != _T('\0') ) pstrText += 2;    _SkipWhitespace(pstrText);    continue;}

        这是为了跳过 xml 开头的声明部分,和 xml 中的所有注释部分 ,注释和声明格式为 <!-- --><? xxxx>,所以才会在 if 语句中判断字符等于 ! 或者 ?。

      • 此函数中有一个递归调用, 是出现在节点有子节点的情况。条件是当调用 _ParseAttributes结束后,发现当前字符不是 /,也就意味着可能有子节点,之所以说可能,是因为有 <xxx></xxx> 的情况。所以还需判断接下来不是 </ 开头的字符才递归调用。

      • 整个 _Parse 函数执行完之后,就像前面所说,只是构造了所有的节点信息: m_pElements,而且 m_pstrXML 指向的一段内存数据也大变样了。我们来看个实例:

        <?xml version="1.0" encoding="utf-8" standalone="yes" ?><Window size="920,490" showshadow="true"   shadowimage = "Image\shadow.png" shadowcorner="23,13,23,33" caption="0,0,0,100" >  <Include source="font\DefaultCfg.dat"/>  <VerticalLayout bkcolor="#FFEEEEEE">    <HorizontalLayout height="30" inset="10,10,0,0" bkcolor="#FF000000">      <Label text="TestDuilib&amp;&apos;&quot;&lt;&gt;" textcolor="#FFFFFFFF" />      <Control />      <Button  name="btnClose" padding="0,0,10,0" height="18" width="18" normalimage="Image\close_default.png" hotimage="close_hover.png" />    </HorizontalLayout>  </VerticalLayout>    <!-- --></Window>

        以上是一个简单的 xml,执行 _Parse 解析之后,m_pstrXML 指向的内存数据为:

        \0?xml version="1.0" encoding="utf-8" standalone="yes" ?>\0Window\0size\0"920,490\0 showshadow\0"true\0   shadowimage\0  "Image\shadow.png\0 shadowcorner\0"23,13,23,33\0 caption\0"0,0,0,100\0 >  \0Include\0source\0"font\DefaultCfg.dat\0\0>  \0VerticalLayout\0bkcolor\0"#FFEEEEEE\0>    \0HorizontalLayout\0height\0"30\0 inset\0"10,10,0,0\0 bkcolor\0"#FF000000\0>      \0Label\0text\0"TestDuilib&'"<>\0                     textcolor\0"#FFFFFFFF\0 \0>      \0Control\0\0>      \0Button\0 name\0"btnClose\0 padding\0"0,0,10,0\0 height\0"18\0 width\0"18\0 normalimage\0"Image\close_default.png\0 hotimage\0"close_hover.png\0 \0>    \0/HorizontalLayout>  \0/VerticalLayout>    \0!-- -->\0/Window>

        为了阅读方便,回车换行并没有用实际的字符表示。大家可以看到,解析后,主要的变化就是多出了很多 \0 字符,这就是为了解析每个节点的属性信息做准备的,后续会在类 CMarkupNode 中做实际的属性解析。

接下来我们来看一下类 CMarkupNode,一个 CMarkupNode 的实例对应一个 XMLELEMENT 的数据,我们来看一下成员函数:

  • CMarkupNode GetParent() 获取父节点,也就是 XMLELEMENTiParent 对应的节点
  • CMarkupNode GetSibling() 获取兄弟节点,也就是 XMLELEMENTiNext 对应的节点
  • CMarkupNode GetChild() 获取第一个子节点,也就是 XMLELEMENTiChild 对应的节点
  • CMarkupNode GetChild(LPCTSTR pstrName) 获取指定节点名称的子节点,其实是先获取子节点,然后逐个获取子节点的下一个节点,直到取得指定名称的节点或遍历结束
  • void _MapAttributes() 解析出这个节点有几个属性,每个属性的值是什么
    我们来看个例子:

    \0Button\0 name\0"btnClose\0 padding\0"0,0,10,0\0 height\0"18\0 width\0"18\0 normalimage\0"Image\close_default.png\0 hotimage\0"close_hover.png\0 \0>

    再看一下 void _MapAttributes() 函数的源码:

    m_nAttributes = 0;LPCTSTR pstr = m_pOwner->m_pstrXML + m_pOwner->m_pElements[m_iPos].iStart;LPCTSTR pstrEnd = m_pOwner->m_pstrXML + m_pOwner->m_pElements[m_iPos].iData;pstr += _tcslen(pstr) + 1;while( pstr < pstrEnd ) {    m_pOwner->_SkipWhitespace(pstr);    m_aAttributes[m_nAttributes].iName = pstr - m_pOwner->m_pstrXML;        pstr += _tcslen(pstr) + 1;    m_pOwner->_SkipWhitespace(pstr);    if( *pstr++ != _T('\"') ) return; // if( *pstr != _T('\"') ) { pstr = ::CharNext(pstr); return; }    m_aAttributes[m_nAttributes++].iValue = pstr - m_pOwner->m_pstrXML;    if( m_nAttributes >= MAX_XML_ATTRIBUTES ) return;    pstr += _tcslen(pstr) + 1;}

    While 之前的代码 pstr += _tcslen(pstr) + 1跳过了节点的名称,此时 pstr 指向 name\0xxxxxx 的开头,然后开始循环,每次循环都将属性名称的开头字符位置和属性的 Value 的字符位置记录下来,循环结束后数组 m_aAttributes 就记录了所有属性的名称起始位置和值的起始位置,接下来通过函数 LPCTSTR GetAttributeName(int iIndex) 和函数LPCTSTR GetAttributeValue(int iIndex) 获取实际的名称和值。

1 0