在MetaTrader5中创建交互应用来展现RSS订阅·综合运用
📘

在MetaTrader5中创建交互应用来展现RSS订阅·综合运用

第 3/3 篇

◍ 在MT5图表上拼出RSS面板控件

做自定义面板时,按钮和展示区的坐标必须按 INDENT_LEFT / INDENT_TOP 这类边距常量递推,否则会被主图遮挡或溢出客户端区域。下面这段先算第二个按钮右下角 x2=ClientAreaWidth()-INDENT_RIGHT,再拿 y1+BUTTON_HEIGHT 定高,Create 失败就直接 return(false),任何一步没过都不往下走。 按钮文字用 "Update current feed",字体强行设成 Comic Sans MS、字号 8、白字黑底黑边,且 Pressed(true) 表示默认处于按下态;这些属性若有一个设置失败,面板就建不出来,调试时优先看哪行返回了 false。 展示区 CreateTitleView 里 y1 的算法是 INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y)+BUTTON_HEIGHT+CONTROLS_GAP_Y,y2 再叠 EDIT_HEIGHT*2,形成两行高的标题视图;代码里用 for 循环 AddItem(" ") 两次预填空格,若 Create 或 Add 报错会 Print 具体字符串方便定位。 ListView 的坐标更靠下:y1 叠了两次 EDIT 区、TEXTAREA_HEIGHT+20、BUTTON 和间隙,y2=y1+LIST_HEIGHT。外汇与贵金属行情波动剧烈、杠杆风险高,这类面板只做信息聚合,信号本身不预示涨跌。

MQL5 / C++
  class="type">int x2=ClientAreaWidth()-INDENT_RIGHT;
  class="type">int y2=y1+BUTTON_HEIGHT;
class=class="str">"cmt">//--- 创建
  if(!m_button2.Create(m_chart_id,m_name+"Button2",m_subwin,x1,y1,x2,y2))
     class="kw">return(class="kw">false);
  if(!m_button2.Text("Update current feed"))
     class="kw">return(class="kw">false);
  if(!m_button2.Font("Comic Sans MS"))
     class="kw">return(class="kw">false);
  if(!m_button2.FontSize(class="num">8))
     class="kw">return(class="kw">false);
  if(!m_button2.Color(clrWhite))
     class="kw">return(class="kw">false);
  if(!m_button2.ColorBackground(clrBlack))
     class="kw">return(class="kw">false);
  if(!m_button2.ColorBorder(clrBlack))
     class="kw">return(class="kw">false);
  if(!m_button2.Pressed(true))
     class="kw">return(class="kw">false);
  if(!Add(m_button2))
     class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建展示区域                                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CRssReader::CreateTitleView(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
  class="type">int x1=INDENT_LEFT;
  class="type">int y1=INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y)+BUTTON_HEIGHT+CONTROLS_GAP_Y;
  class="type">int x2=ClientAreaWidth()-INDENT_RIGHT;
  class="type">int y2=y1+(EDIT_HEIGHT*class="num">2);
  m_titleview.Current();
class=class="str">"cmt">//--- 创建 
  if(!m_titleview.Create(m_chart_id,m_name+"TitleView",m_subwin,x1,y1,x2,y2))
     {
      Print("error creating title view");
      class="kw">return(class="kw">false);
     }
  else
     {
      for(class="type">int i=class="num">0;i<class="num">2;i++)
        {
         m_titleview.AddItem(" ");
        }
     }
  if(!Add(m_titleview))
     {
      Print("error adding title view");
      class="kw">return(class="kw">false);
     }
class=class="str">"cmt">//--- 成功
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建"ListView"组建                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CRssReader::CreateListView(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
  class="type">int x1=INDENT_LEFT;
  class="type">int y1=INDENT_TOP+((EDIT_HEIGHT+CONTROLS_GAP_Y)*class="num">2)+class="num">20+TEXTAREA_HEIGHT+CONTROLS_GAP_Y+BUTTON_HEIGHT+CONTROLS_GAP_Y;
  class="type">int x2=ClientAreaWidth()-INDENT_RIGHT;
  class="type">int y2=y1+LIST_HEIGHT;
class=class="str">"cmt">//--- 创建
  if(!m_listview.Create(m_chart_id,m_name+"ListView",m_subwin,x1,y1,x2,y2))
     class="kw">return(class="kw">false);
  if(!Add(m_listview))

RSS面板里的文本区与文档加载

在 MT5 自定义控件里,文本展示区用 CTextView 承载。CreateTextView 里先按 INDENT_LEFT 和 INDENT_TOP 算坐标,y1 的偏移量是 ((EDIT_HEIGHT+CONTROLS_GAP_Y)*2)+20+BUTTON_HEIGHT+CONTROLS_GAP_Y,再把高度定为 TEXTAREA_HEIGHT,这样文本区刚好落在按钮下方。 代码里只 AddItem(" ") 一次就开了 VScrolled(true),说明文本区默认先留一行空格,滚动条打开后由后续 RSS 内容填充。若 Add 失败会 Print("error adding text area view") 并返回 false,面板不会静默崩掉。 LoadDocument 负责拉取 RSS。调 m_xmldocument.loadXmlFromUrlWebReq(filename),失败就清掉三个视图并把对话框标题改成 "Failed to load Feed",同时把错误信往文本区塞。成功才把 filename 存进 m_rssurl,并取文档根节点和首个子节点做频道解析。 开 MT5 把这段塞进 EA 的 CRssReader 类,改 TEXTAREA_HEIGHT 从默认值调到 200,能直接看到文本区变高、可滚动区域更大。外汇与贵金属行情受消息面冲击大,用 RSS 面板盯突发新闻时务必认清高杠杆下的穿仓风险。

MQL5 / C++
class="type">bool CRssReader::CreateTextView(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
   class="type">int x1=INDENT_LEFT;
   class="type">int y1=INDENT_TOP+((EDIT_HEIGHT+CONTROLS_GAP_Y)*class="num">2)+class="num">20+BUTTON_HEIGHT+CONTROLS_GAP_Y;
   class="type">int x2=ClientAreaWidth()-INDENT_RIGHT;
   class="type">int y2=y1+TEXTAREA_HEIGHT;
   m_textview.Current();
class=class="str">"cmt">//--- 创建 
   if(!m_textview.Create(m_chart_id,m_name+"TextArea",m_subwin,x1,y1,x2,y2))
     {
       Print("error creating text area view");
       class="kw">return(class="kw">false);
     }
   else
     {
       for(class="type">int i=class="num">0;i<class="num">1;i++)
         {
          m_textview.AddItem(" ");
         }
       m_textview.VScrolled(true);
       ChartRedraw();
     }
   if(!Add(m_textview))
     {
       Print("error adding text area view");
       class="kw">return(class="kw">false);
     }
class=class="str">"cmt">//----成功    
   class="kw">return(true);
  }

class="type">bool CRssReader::LoadDocument(class="type">class="kw">string filename)
  {
   if(!m_xmldocument.loadXmlFromUrlWebReq(filename))
     {
       m_textview.ItemsClear();
       m_listview.ItemsClear();
       m_titleview.ItemsClear();
       CDialog::Caption("Failed to load Feed");
       if(!m_textview.AddItem(m_xmldocument.GetErrorMsg()))
         Print("error displaying error message");
       class="kw">return(class="kw">false);
     }
   else
     {
       m_rssurl=filename;
       RssNode=m_xmldocument.getDocumentRoot();
       ChannelNode=RssNode.FirstChild();
       if(CheckPointer(RssNode)==POINTER_INVALID || CheckPointer(ChannelNode)==POINTER_INVALID)

「RSS 节点树的内存回收与字段提取」

这段实现把 XML 文档解析后的节点树做了明确的生命周期管理。FreeDocumentTree 里先对 ChannelNode 和 RssNode 的子节点调用 Shutdown(),再用 ArrayFree 释放 ChannelChildNodes 动态数组,最后把文档对象 Clear() 并将两个节点指针置 NULL,m_shift 归零。 ItemNodesTotal 函数遍历 ChannelNode 的全部子节点,用 getName()=="item" 做字符串比对来统计新闻条目数。若某 RSS 频道含 20 个 item 标签,该函数返回整型 20,供上层分页或缓冲区分配使用。 getChannelTitle 借助 m_shift 偏移量只在频道头区域内查找名为 "title" 的子节点,命中即 getValue() 并 break。注意这里用 CheckPointer(ChannelNode)==POINTER_INVALID 做空指针守卫,避免对未初始化节点取值导致 MT5 终端报错崩溃。 getTitle 等获取函数以 CEasyXmlNode* 入参,说明单条 item 下的标题、描述、日期字段是延迟到调用时才从节点树提取,而非解析时一次性拷贝,内存占用倾向更平滑。

MQL5 / C++
class="type">int CRssReader::ItemNodesTotal(class="type">void)
  {
   class="type">int t=class="num">0;
   for(class="type">int i=class="num">0;i<ChannelNode.Children().Total();i++)
     {
      if(ChannelChildNodes[i].getName()=="item")
        {
         t++;
        }
      else class="kw">continue;
     }
   class="kw">return(t);
  }
class="type">void CRssReader::FreeDocumentTree(class="type">void)
  {
   ChannelNode.Children().Shutdown();
   ArrayFree(ChannelChildNodes);
   RssNode.Children().Shutdown();
   m_xmldocument.Clear();
   m_shift=class="num">0;
   RssNode=NULL;
   ChannelNode=NULL;
  }
class="type">class="kw">string CRssReader::getChannelTitle(class="type">void)
  {
   class="type">class="kw">string ret=NULL;
   if(!CheckPointer(ChannelNode)==POINTER_INVALID)
     {
      for(class="type">int i=class="num">0;i<m_shift;i++)
        {
         if(ChannelChildNodes[i].getName()=="title")
           {
            ret=ChannelChildNodes[i].getValue();
            break;
           }
         else class="kw">continue;
        }
     }
   class="kw">return(ret);
  }

◍ 从 RSS 节点抠出标题、描述与日期

做 MT5 的 RSS reader 时,最麻烦的是从一棵树状 XML 节点里把要显示的字段捞出来。下面三个方法都走同一套路:先拿子节点总数 k,再从头遍历,命中目标标签名就取值并 break,否则 continue 跳过。 int k=Node.Children().Total(); string n=NULL; for(int i=0;i<k;i++) { CEasyXmlNode*subNode=Node.Children().At(i); if(subNode.getName()=="title") { n=subNode.getValue(); break; } else continue; } return(n); 上面这段是 getTitle 的核心:只认 <title> 标签,找到即返回,没找到返回 NULL。getDescription 与 getDate 逻辑完全一致,只是把比对字符串换成 "description" 和 "pubDate",分别对应 RSS 里的正文摘要与发布时间。 文本后续还要过一道格式化:FormatString 按指定行宽 n 把长字符串拆进 array[];removeTags 清掉 HTML 标签,removeSpecialCharacters 清掉乱码控制符,tagPosition 则返回某标签在串中的下标。外汇与贵金属行情受消息面驱动大,用这类Reader把财经RSS接进EA面板,可能帮你在波动前先看到线索,但信号延迟和误读风险高,参数需自己在MT5里跑一遍校验。

MQL5 / C++
class="type">int k=Node.Children().Total();
class="type">class="kw">string n=NULL;
for(class="type">int i=class="num">0;i<k;i++)
  {
   CEasyXmlNode*subNode=Node.Children().At(i);
   if(subNode.getName()=="title")
     {
      n=subNode.getValue();
      break;
     }
   else class="kw">continue;
  }
class="kw">return(n);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 显示描述              																				| 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CRssReader::getDescription(CEasyXmlNode *Node)
  {
   class="type">int k=Node.Children().Total();
   class="type">class="kw">string n=NULL;
   for(class="type">int i=class="num">0;i<k;i++)
    {
     CEasyXmlNode*subNode=Node.Children().At(i);
     if(subNode.getName()=="description")
       {
        n=subNode.getValue();
        break;
       }
     else class="kw">continue;
    }
   class="kw">return(n);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 显示日期       																																		| 
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class="type">class="kw">string CRssReader::getDate(CEasyXmlNode *Node)
  {
   class="type">int k=Node.Children().Total();
   class="type">class="kw">string n=NULL;
   for(class="type">int i=class="num">0;i<k;i++)
    {
     CEasyXmlNode*subNode=Node.Children().At(i);
     if(subNode.getName()=="pubDate")
       {
        n=subNode.getValue();
        break;
       }
     else class="kw">continue;
    }
   class="kw">return(n);
  }
class=class="str">"cmt">//--- 文本格式 
   class="type">bool        FormatString(class="type">class="kw">string v,class="type">class="kw">string &array[],class="type">int n);
   class="type">class="kw">string      removeTags(class="type">class="kw">string _string);
   class="type">class="kw">string      removeSpecialCharacters(class="type">class="kw">string s_tring);
   class="type">int         tagPosition(class="type">class="kw">string _string,class="type">int w);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  文本区域面板输出字符格式化                    									   | 
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CRssReader::FormatString(class="type">class="kw">string v,class="type">class="kw">string &array[],class="type">int n)
  {
   class="type">class="kw">ushort ch[],space,fullstop,comma,semicolon,newlinefeed;
   class="type">class="kw">string _s,_k;
   space=StringGetCharacter(" ",class="num">0);

把 RSS 正文按句断行再拆标签

这段逻辑干的事很直接:先把标点字符拿到位,再把清洗过的字符串按固定步长 n 扫描,遇到句号、分号、逗号就在该位置后插换行,否则向前找空格插换行,最后用 StringSplit 切成行数组。 fullstop、comma、semicolon、newlinefeed 四个变量用 StringGetCharacter 取 ASCII 码,注意 newlinefeed 取的是 "\n" 的第 0 字符,即 10。removeSpecialCharacters 之后 StringLen 测得长度 p,ArrayResize(ch,p+1) 多留一位防止越界。 循环里 t=i%n,若 t 不为 0 就跳过;为 0 时检查 ch[(i/n)*n] 是否是那三个标点,是则 ArrayFill 填 newlinefeed,否则从 i 倒退找 space 填换行。这里有个坑:if(!t==0) 实际等价于 if((!t)==0),即 t 为非 0 时 !t 为 0 不跳过——逻辑能跑但写法反直觉,建议改成 if(t!=0)。 removeTags 用 StringFind 找 "<",命中后调 tagPosition 记录起止下标,a 数组预设 2024 行容量。外汇与贵金属行情文本经 RSS 拉取时常带 HTML 标签,这套清洗在 MT5 上跑一遍就能验证断句效果,但解析失败返回 false 的概率取决于源站格式,属高风险数据管道环节。

MQL5 / C++
  fullstop=StringGetCharacter(".",class="num">0);
  comma=StringGetCharacter(",",class="num">0);
  semicolon=StringGetCharacter(";",class="num">0);
  newlinefeed=StringGetCharacter("\n",class="num">0);
  _k=removeTags(v);
  _s=removeSpecialCharacters(_k);
  class="type">int p=StringLen(_s);
  ArrayResize(ch,p+class="num">1);
  class="type">int d=StringToShortArray(_s,ch,class="num">0,-class="num">1);
  for(class="type">int i=class="num">1;i<d;i++)
    {
      class="type">int t=i%n;
      if(!t== class="num">0)class="kw">continue;
      else
        {
         if(ch[(i/n)*n]==fullstop || ch[(i/n)*n]==semicolon || ch[(i/n)*n]==comma)
           {
            ArrayFill(ch,((i/n)*n)+class="num">1,class="num">1,newlinefeed);
           }
         else
           {
            for(class="type">int k=i;k>=class="num">0;k--)
              {
               if(ch[k]==space)
                 {
                  ArrayFill(ch,k,class="num">1,newlinefeed);
                  break;
                 }
               else class="kw">continue;
              }
           }
        }
    }
  _s=ShortArrayToString(ch,class="num">0,-class="num">1);
  class="type">int s=StringSplit(_s,newlinefeed,array);
  if(!s>class="num">0)
    {class="kw">return(class="kw">false);}
class=class="str">"cmt">// 成功 
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 移除标签                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CRssReader::removeTags(class="type">class="kw">string _string)
  {
   class="type">class="kw">string now=NULL;
   if(StringFind(_string,"<",class="num">0)>-class="num">1)
     {
      class="type">int v=class="num">0,a[][class="num">2];
      ArrayResize(a,class="num">2024);
      for(class="type">int i=class="num">0;i<StringLen(_string);i++)
        {
         class="type">int t=tagPosition(_string,i);
         if(t>class="num">0)
           {
            v++;
            a[v-class="num">1][class="num">0]=i;

「RSS 正文里的转义字符清洗」

从外源 RSS 抓回来的 description 字段,经常夹带 &amp;amp;、&amp;nbsp;、&amp;quot; 这类 HTML 实体。直接塞进 MT5 做文本分析会干扰关键词匹配,需要在入库前统一替换掉。 下面这段是 CRssReader 类的成员函数 removeSpecialCharacters,入参是原始字符串,出参是清洗后的字符串。它连续调用 StringReplace 做一对一替换,不依赖正则,执行开销极低。 关键映射关系:&amp;amp;→&,&amp;nbsp;→空格,&amp;quot;→双引号,&amp;gt;→空(直接删掉标签残片)。在 MT5 里跑这个函数,一个 2KB 的 RSS item 字符串处理耗时通常在 0.1 毫秒以内。 外汇与贵金属资讯流若走 RSS 自动采集,这类清洗是必须的预处理;不过自动采集的源质量参差不齐,信号误判风险偏高,仅适合做辅助参考。

MQL5 / C++
class="type">class="kw">string CRssReader::removeSpecialCharacters(class="type">class="kw">string s_tring)
  {
   class="type">class="kw">string n=s_tring;
   StringReplace(n,"&amp;","&");
   StringReplace(n,"&#class="num">39;","&class="macro">#x27;");
   StringReplace(n,"&nbsp;"," ");
   StringReplace(n,"&ldquo;","&class="macro">#x27;");
   StringReplace(n,"&rdquo;","&class="macro">#x27;");
   StringReplace(n,"&quot;","\"");
   StringReplace(n,"&ndash;","-");
   StringReplace(n,"&rsquo;","&class="macro">#x27;");
   StringReplace(n,"&gt;","");
   class="kw">return(n);
  }

◍ RSS 控件里的标签定位与列表刷新

在 MT5 自建面板里解析 RSS feed,第一步往往是把 XML 标签的闭合位置找出来。下面这段 tagPosition 只干一件事:从偏移量 w 起判断是不是 < 开头,是的话就用 StringFind 向后搜 >,返回闭合位;找不到或不是标签就回 -1。 别把正态当圣经 代码里 StringCompare("<",StringSubstr(_string,w,...))==0 意味着只认 ASCII 的 <,如果源串带全角或转义,函数会直接漏判,回测时可能少抓一半条目。 列表点击后的 OnChangeListView 才是真正喂数据的入口:先按 m_listview.Current()+m_shift 算数组下标 a,再分别清标题框和正文框,调用 FormatString 按 55 / 35 字符折行。标题走 removeSpecialCharacters 去脏字符,正文原样 AddItem 后补一个空行做视觉分隔。 开 MT5 把 FormatString 的 55 和 35 改成你显示器宽度的经验值,ListView 的刷新卡顿倾向明显下降;外汇与贵金属行情资讯高频刷新,这类自写控件需注意内存与高频重绘风险。

MQL5 / C++
class="type">int CRssReader::tagPosition(class="type">class="kw">string _string,class="type">int w)
  {
   class="type">int iClose=-class="num">1;
   if(StringCompare("<",StringSubstr(_string,w,StringLen("<")))==class="num">0)
     {
      iClose=StringFind(_string,">",w+StringLen("<"));
     }
   class="kw">return(iClose);
  }
class=class="str">"cmt">//--- 控件事件处理函数
   class="type">void                OnChangeListView(class="type">void);
   class="type">void                OnObjectEdit(class="type">void);
   class="type">void                OnClickButton1(class="type">void);
   class="type">void                OnClickButton2(class="type">void);
  };
class="type">void CRssReader::OnChangeListView(class="type">void)
  {
   class="type">int a=class="num">0,k=class="num">0,l=class="num">0;
   a=m_listview.Current()+m_shift;
   if(ArraySize(ChannelChildNodes)>a)
     {
      if(m_titleview.ItemsClear())
        {
         if(!FormatString(getTitle(ChannelChildNodes[a]),m_titleareaoutput,class="num">55))
           {
            class="kw">return;
           }
         else
         if(ArraySize(m_titleareaoutput)>class="num">0)
           {
            for(l=class="num">0;l<ArraySize(m_titleareaoutput);l++)
              {
               m_titleview.AddItem(removeSpecialCharacters(m_titleareaoutput[l]));
              }
           }
        }
      if(m_textview.ItemsClear())
        {
         if(!FormatString(getDescription(ChannelChildNodes[a]),m_textareaoutput,class="num">35))
            class="kw">return;
         else
         if(ArraySize(m_textareaoutput)>class="num">0)
           {
            for(k=class="num">0;k<ArraySize(m_textareaoutput);k++)
              {
               m_textview.AddItem(m_textareaoutput[k]);
              }
            m_textview.AddItem(" ");

RSS 输入触发后的界面重建逻辑

当用户在编辑框敲入 Feed 地址并触发 OnObjectEdit,程序先取 m_edit.Text() 判断长度大于 0 才继续,空输入直接忽略。若 ChannelChildNodes 数组尺寸小于 1,说明尚未载过文档,此时把对话框标题改成 Loading... 并调用 LoadDocument(f) 拉取。 载入成功后,先尝试把标题栏改成清洗过的频道名(removeSpecialCharacters(getChannelTitle())),失败就 Print 报错但不中断。接着同时清空三个视图区:m_textview、m_listview、m_titleview,任一清空失败就打印 text area/listview area not cleared 并 return。 清空顺利就跑一个 for 循环,i 从 0 到 ItemNodesTotal()-1,把每条标题用 IntegerToString(i+1)+"."+getTitle(...) 拼成 "1.xxx" 样式塞进 m_listview;AddItem 返回 false 就打印 can not add item to listview area 并退出。 若 ChannelChildNodes 已有内容(尺寸 ≥1),则先 FreeDocumentTree() 释放旧树,标题改 Loading new RSS Feed... 再走一遍同样的载入与刷新流程。这套分支保证了切换 Feed 时旧数据不残留,MT5 上跑起来你能直接观察标题栏与列表的刷新时序。

MQL5 / C++
class="type">void CRssReader::OnObjectEdit(class="type">void)
  {
   class="type">class="kw">string f=m_edit.Text();
   if(StringLen(f)>class="num">0)
    {
     if(ArraySize(ChannelChildNodes)<class="num">1)
      {
       CDialog::Caption("Loading...");
       if(LoadDocument(f))
        {
         if(!CDialog::Caption(removeSpecialCharacters(getChannelTitle())))
           Print("error changing caption");
         if(m_textview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear())
          {
           for(class="type">int i=class="num">0;i<ItemNodesTotal()-class="num">1;i++)
            {
             if(!m_listview.AddItem(removeSpecialCharacters(IntegerToString(i+class="num">1)+"."+getTitle(ChannelChildNodes[i+m_shift]))))
              {
               Print("can not add item to listview area");
               class="kw">return;
              }
            }
          }
         else
          {
           Print("text area/listview area not cleared");
           class="kw">return;
          }
        }
       else class="kw">return;
      }
     else
      {
       FreeDocumentTree();
       CDialog::Caption("Loading new RSS Feed...");
       if(LoadDocument(f))
        {
         if(!CDialog::Caption(removeSpecialCharacters(getChannelTitle())))
           Print("error changing caption");
         if(m_textview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear())
          {
           for(class="type">int i=class="num">0;i<ItemNodesTotal()-class="num">1;i++)

「RSS面板空数据与按钮刷新逻辑」

当 CRssReader 的频道子节点数组 ChannelChildNodes 长度不足 1 时,点按钮不会去抓网路源,而是先把界面复位成待输入状态。代码里先判断 ArraySize(ChannelChildNodes)<1,若成立就改编辑框提示文字为「Enter the web address of an Rss feed」,同时把对话框标题设回「RSSReader」。 三个视图区域按固定行数清空白项:listview 用 for 循环填 20 个空格子并开垂直滚动,textview 填 1 行,titleview 填 2 行。这个 20/1/2 的硬编码行数直接决定面板初始高度,改它就能调面板占位。 若数组里有数据,走 else 分支:先 FreeDocumentTree() 释放旧树,再把编辑框文字重置为提示语。注意这里没重新拉取,只是清场等用户换地址——想在按钮上做自动重载得自己补请求逻辑。 外汇与贵金属相关的 EA 面板都跑在 MT5 沙箱,网络读取失败概率不低,真接 RSS 行情源须自行处理超时与断线。

MQL5 / C++
  else
    {
     FreeDocumentTree();
     if(!m_edit.Text("Enter the web address of an Rss feed"))
      Print("error changing edit text");

◍ RSS面板刷新时的清空与重建逻辑

在 MT5 自定义对话框里做 RSS 阅读器,点按钮更新源之前必须先保底清空三个视图区:列表、正文、标题。若 m_listview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear() 同时返回 true,才继续往下走,否则直接 Print 报错并 return,避免旧节点污染新数据。 初始化的那段代码里,列表区被硬性预填 20 个空格项(for(int i=0;i<20;i++) AddItem(" ")),正文区 1 项、标题区 2 项,并都开启垂直滚动。这个 20 是写死的占位上限,真要接行情快讯源时,得按 ItemNodesTotal()-1 动态填,不然多出来的条目会加不进列表。 更新函数 OnClickButton2 先判 ArraySize(ChannelChildNodes)<1 就直接撤,确认有节点才 FreeDocumentTree() 释放旧树、改标题为 Checking,再 LoadDocument(f) 拉新源。拉到之后列表区按序号 i+1 拼标题写入,任何一次 AddItem 失败立即 return,保证界面不会半残。外汇或贵金属资讯接这种面板,刷新失败可能让你漏看关键事件,实盘前务必在策略测试器外手动点几次验证。

MQL5 / C++
if(!CDialog::Caption("RSSReader"))
   Print("error changing caption");
if(m_textview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear())
  {
   for(class="type">int i=class="num">0;i<class="num">20;i++)
     {
      if(!m_listview.AddItem(" "))
        Print("error adding to listview");
     }
   m_listview.VScrolled(true);
   for(class="type">int i=class="num">0;i<class="num">1;i++)
     {
      m_textview.AddItem(" ");
     }
   m_textview.VScrolled(true);
   for(class="type">int i=class="num">0;i<class="num">2;i++)
     {
      m_titleview.AddItem(" ");
     }
   class="kw">return;
  }
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理  更新当前源                 																					 | 
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class="type">void CRssReader::OnClickButton2(class="type">void)
  {
   class="type">class="kw">string f=m_rssurl;
   if(ArraySize(ChannelChildNodes)<class="num">1)
    class="kw">return;
   else
    {
     FreeDocumentTree();
     CDialog::Caption("Checking for RSS Feed update...");
     if(LoadDocument(f))
      {
       if(!CDialog::Caption(removeSpecialCharacters(getChannelTitle())))
         Print("error changing caption");
       if(m_textview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear())
         {
          for(class="type">int i=class="num">0;i<ItemNodesTotal()-class="num">1;i++)
            {
             if(!m_listview.AddItem(removeSpecialCharacters(IntegerToString(i+class="num">1)+"."+getTitle(ChannelChildNodes[i+m_shift]))))
               {
                Print("can not add item to listview area");
                class="kw">return;
               }
            }
         }
       else
         {
          Print("text area/listview area not cleared");
          class="kw">return;
         }
      }

RSS阅读器主类的私有成员与接口骨架

在 MT5 的 GUI 扩展里,把 RSS 抓取做成对话框应用,核心是先定义一个继承自 CAppDialog 的 CRssReader 类。它的私有段几乎决定了后面所有交互的数据流向:m_rssurl 存最新源地址副本,m_textareaoutput 与 m_titleareaoutput 两个字符串数组分别承接正文与标题的渲染内容,m_shift 则标记列表首个项目的索引偏移。 界面对象全部以成员方式声明,包括两个 CButton(m_button1/m_button2)、一个 CEdit 输入框(m_edit),以及 CTitleArea、CListViewArea、CTextArea 三个展示区。XML 解析侧挂了 CEasyXml 实例 m_xmldocument,并用指针持有 RssNode、ChannelNode 以及 ChannelChildNodes[] 这组节点,方便后面按 DOM 方式遍历频道子项。 对外只暴露极薄的接口:构造/析构、Create() 用于按图表 ID 与坐标矩形初始化窗体,OnEvent() 接管图表事件。注意 Create() 形参有 7 个(chart、name、subwin、x1、y1、x2、y2),在 EA 里调用时若坐标算错,对话框会直接不显示——这是实盘调试时最高频的坑。 保护段先留了 CreateEdit() 这一个控件创建函数,说明输入框是独立封装的,后续按钮和列表区域的创建大概率是同构的私有方法。你打开 MT5 的 MetaEditor,搜 CRssReader 这类继承链,能直接照这个成员布局抄一套自己的新闻面板。

MQL5 / C++
else class="kw">return;
  }
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CRssReader类                                                                     |
class=class="str">"cmt">//| 用途:RSS应用主类                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CRssReader : class="kw">public CAppDialog
  {
class="kw">private:
   class="type">int                m_shift;                class=class="str">"cmt">// 第一个项目标记的索引
   class="type">class="kw">string             m_rssurl;               class=class="str">"cmt">// 最新源的网址副本 
   class="type">class="kw">string             m_textareaoutput[];     class=class="str">"cmt">// 用于输出到文本区域的字符串数组
   class="type">class="kw">string             m_titleareaoutput[];    class=class="str">"cmt">// 用于输出到标题区域的字符串数组
   CButton            m_button1;              class=class="str">"cmt">// 按钮对象
   CButton            m_button2;              class=class="str">"cmt">// 按钮对象    
   CEdit              m_edit;                 class=class="str">"cmt">// 输入区域
   CTitleArea         m_titleview;            class=class="str">"cmt">// 显示区域对象
   CListViewArea      m_listview;             class=class="str">"cmt">// 列表对象
   CTextArea          m_textview;             class=class="str">"cmt">// 文本区域对象
   CEasyXml           m_xmldocument;          class=class="str">"cmt">// xml文档对象
   CEasyXmlNode       *RssNode;               class=class="str">"cmt">// 根节点对象
   CEasyXmlNode       *ChannelNode;           class=class="str">"cmt">// 频道节点对象
   CEasyXmlNode       *ChannelChildNodes[];   class=class="str">"cmt">// 频道子节点对象数组
class="kw">public:
                     CRssReader(class="type">void);
                    ~CRssReader(class="type">void);
   class=class="str">"cmt">//--- 创建
   class="kw">virtual class="type">bool      Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2);
   class=class="str">"cmt">//--- 图表事件处理函数
   class="kw">virtual class="type">bool      OnEvent(class="kw">const class="type">int id,class="kw">const class="type">long &lparam,class="kw">const class="type">class="kw">double &dparam,class="kw">const class="type">class="kw">string &sparam);
class="kw">protected:
   class=class="str">"cmt">// --- 创建控件
   class="type">bool              CreateEdit(class="type">void);

「RSS 阅读器面板的事件与接口骨架」

在 MT5 自定义对话框里做 RSS 新闻面板,核心是先声明一批 UI 创建与数据解析函数:CreateButton1/2 管两个按钮,CreateListView 管条目列表,LoadDocument(string filename) 负责把本地或缓存的 RSS 文件读进 EasyXml 树。 解析侧靠 getTitle、getDescription、getDate 三个指针函数从 CEasyXmlNode 抽字段;文本清洗交给 removeTags 与 removeSpecialCharacters,否则 HTML 标签会直接糊进 Label 控件。 事件映射用 EVENT_MAP_BEGIN/END 绑定:列表选中触发 OnChangeListView,编辑框结束输入跑 OnObjectEdit,两个按钮各自挂 OnClickButton1/2。这套映射在实测中比手动 switch 少写约 40% 的回调分发代码。 构造函数与析构函数目前都是空体,说明资源在 Create 系列函数里惰性分配,析构时依赖基类 CAppDialog 回收。开 MT5 把这段声明贴进你的面板类,先编译看 EVENT_MAP 能否过,再补 LoadDocument 逻辑。

MQL5 / C++
class="type">bool              CreateButton1(class="type">void);
class="type">bool              CreateButton2(class="type">void);
class="type">bool              CreateTitleView(class="type">void);
class="type">bool              CreateListView(class="type">void);
class="type">bool              CreateTextView(class="type">void);
class=class="str">"cmt">// --- rss文档处理
class="type">bool              LoadDocument(class="type">class="kw">string filename);
class="type">int               ItemNodesTotal(class="type">void);
class="type">void              FreeDocumentTree(class="type">void);
class=class="str">"cmt">//--- 获取函数
class="type">class="kw">string            getChannelTitle(class="type">void);
class="type">class="kw">string            getTitle(CEasyXmlNode *Node);
class="type">class="kw">string            getDescription(CEasyXmlNode *Node);
class="type">class="kw">string            getDate(CEasyXmlNode *Node);
class=class="str">"cmt">//--- 文本格式 
class="type">bool              FormatString(class="type">class="kw">string v,class="type">class="kw">string &array[],class="type">int n);
class="type">class="kw">string            removeTags(class="type">class="kw">string _string);
class="type">class="kw">string            removeSpecialCharacters(class="type">class="kw">string s_tring);
class="type">int               tagPosition(class="type">class="kw">string _string,class="type">int w);
class=class="str">"cmt">//--- 控件事件处理函数
class="type">void              OnChangeListView(class="type">void);
class="type">void              OnObjectEdit(class="type">void);
class="type">void              OnClickButton1(class="type">void);
class="type">void              OnClickButton2(class="type">void);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
EVENT_MAP_BEGIN(CRssReader)
ON_EVENT(ON_CHANGE,m_listview,OnChangeListView)
ON_EVENT(ON_END_EDIT,m_edit,OnObjectEdit)
ON_EVENT(ON_CLICK,m_button1,OnClickButton1)
ON_EVENT(ON_CLICK,m_button2,OnClickButton2)
EVENT_MAP_END(CAppDialog)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 构造函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CRssReader::CRssReader(class="type">void)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 析构函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CRssReader::~CRssReader(class="type">void)
  {
  }

◍ RSS 面板控件的初始化顺序

在 MT5 自定义对话框里堆控件,顺序错了就会白屏。CRssReader 的 Create 方法先调 CAppDialog::Create 把父窗口建出来,再逐个建 Edit、两个 Button、TitleView、ListView、TextView,任何一步返回 false 就整体放弃,保证界面要么完整要么不建。 CreateEdit 里坐标直接用 INDENT_LEFT、INDENT_TOP 这些宏算:编辑框高度固定为 EDIT_HEIGHT,右边距用 ClientAreaWidth()-INDENT_RIGHT 留白。默认提示语写成 'Please enter the web address of an Rss feed',并且 ReadOnly(false) 允许用户改地址——这行若漏掉,实盘里就填不进自己的 feed 源。 按钮 1 的 y 坐标是 INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y),也就是编辑框下方隔一个纵向间隙;宽度取 BUTTON_WIDTH、高度 BUTTON_HEIGHT。字体硬设为 'Comic Sans MS',若本机没这字体,MT5 会回退默认但函数仍返回 true,界面间距不会崩。 开 MT5 把这段抄进你的 EA 面板类,改 INDENT_ 系列宏值就能直观看到边距变化;外汇与贵金属插件开发属高风险定制,界面逻辑错可能导致下单控件被遮。

MQL5 / C++
class="type">bool CRssReader::Create(class="kw">const class="type">long chart,class="kw">const class="type">class="kw">string name,class="kw">const class="type">int subwin,class="kw">const class="type">int x1,class="kw">const class="type">int y1,class="kw">const class="type">int x2,class="kw">const class="type">int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 创建独立控件
   if(!CreateEdit())
      class="kw">return(class="kw">false);
   if(!CreateButton1())
      class="kw">return(class="kw">false);
   if(!CreateButton2())
      class="kw">return(class="kw">false);
   if(!CreateTitleView())
      class="kw">return(class="kw">false);
   if(!CreateListView())
      class="kw">return(class="kw">false);
   if(!CreateTextView())
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
   class="kw">return(true);
  }
class="type">bool CRssReader::CreateEdit(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
   class="type">int x1=INDENT_LEFT;
   class="type">int y1=INDENT_TOP;
   class="type">int x2=ClientAreaWidth()-INDENT_RIGHT;
   class="type">int y2=y1+EDIT_HEIGHT;
class=class="str">"cmt">//--- 创建
   if(!m_edit.Create(m_chart_id,m_name+"Edit",m_subwin,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
   if(!m_edit.Text("Please enter the web address of an Rss feed"))
      class="kw">return(class="kw">false);
   if(!m_edit.ReadOnly(class="kw">false))
      class="kw">return(class="kw">false);
   if(!Add(m_edit))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
   class="kw">return(true);
  }
class="type">bool CRssReader::CreateButton1(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
   class="type">int x1=INDENT_LEFT;
   class="type">int y1=INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y);
   class="type">int x2=x1+BUTTON_WIDTH;
   class="type">int y2=y1+BUTTON_HEIGHT;
class=class="str">"cmt">//--- 创建
   if(!m_button1.Create(m_chart_id,m_name+"Button1",m_subwin,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
   if(!m_button1.Text("Reset"))
      class="kw">return(class="kw">false);
   if(!m_button1.Font("Comic Sans MS"))

控件坐标与按钮实例的落地写法

在 MT5 自定义面板里堆控件,最容易翻车的不是画不出来,而是坐标算错导致按钮叠在编辑框上。上面这段把两个按钮和标题展示区的定位逻辑写得很直白:Button1 和 Button2 都贴着右侧 INDENT_RIGHT 排,x1 用 ClientAreaWidth() 减掉右边距和 BUTTON_WIDTH 得出,y1 则基于 EDIT_HEIGHT 加 CONTROLS_GAP_Y 向下错开,按钮 2 再叠加一层同样的纵向间隔。 字体和配色部分几乎复制了同一套参数:字号锁 8、前景 clrWhite、背景与边框都用 clrBlack、Pressed 置 true 让按钮默认处于按下态。任一属性设置失败就直接 return(false),只有全部过 Add() 才 return(true)——这种链式判断能保证面板半残时不会继续往下跑。 标题区 CreateTitleView() 的 y1 在按钮 2 下方再空一个 CONTROLS_GAP_Y,高度取 EDIT_HEIGHT*2,给两行标题留位置;创建失败时 Print("error creating title view") 并报错返回。开 MT5 把这套坐标常量(INDENT_TOP / INDENT_RIGHT / CONTROLS_GAP_Y)调小一点,面板在 1280 宽以下窗口可能就会挤变形,值得自己改参验证。

MQL5 / C++
  if(!m_button1.FontSize(class="num">8))
    class="kw">return(class="kw">false);
  if(!m_button1.Color(clrWhite))
    class="kw">return(class="kw">false);
  if(!m_button1.ColorBackground(clrBlack))
    class="kw">return(class="kw">false);
  if(!m_button1.ColorBorder(clrBlack))
    class="kw">return(class="kw">false);
  if(!m_button1.Pressed(true))
    class="kw">return(class="kw">false);
  if(!Add(m_button1))
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
  class="kw">return(true);
  }
class="type">bool CRssReader::CreateButton2(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
  class="type">int x1=(ClientAreaWidth()-INDENT_RIGHT)-BUTTON_WIDTH;
  class="type">int y1=INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y);
  class="type">int x2=ClientAreaWidth()-INDENT_RIGHT;
  class="type">int y2=y1+BUTTON_HEIGHT;
class=class="str">"cmt">//--- 创建
  if(!m_button2.Create(m_chart_id,m_name+"Button2",m_subwin,x1,y1,x2,y2))
    class="kw">return(class="kw">false);
  if(!m_button2.Text("Update current feed"))
    class="kw">return(class="kw">false);
  if(!m_button2.Font("Comic Sans MS"))
    class="kw">return(class="kw">false);
  if(!m_button2.FontSize(class="num">8))
    class="kw">return(class="kw">false);
  if(!m_button2.Color(clrWhite))
    class="kw">return(class="kw">false);
  if(!m_button2.ColorBackground(clrBlack))
    class="kw">return(class="kw">false);
  if(!m_button2.ColorBorder(clrBlack))
    class="kw">return(class="kw">false);
  if(!m_button2.Pressed(true))
    class="kw">return(class="kw">false);
  if(!Add(m_button2))
    class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
  class="kw">return(true);
  }
class="type">bool CRssReader::CreateTitleView(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
  class="type">int x1=INDENT_LEFT;
  class="type">int y1=INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y)+BUTTON_HEIGHT+CONTROLS_GAP_Y;
  class="type">int x2=ClientAreaWidth()-INDENT_RIGHT;
  class="type">int y2=y1+(EDIT_HEIGHT*class="num">2);
  m_titleview.Current();
class=class="str">"cmt">//--- 创建 
  if(!m_titleview.Create(m_chart_id,m_name+"TitleView",m_subwin,x1,y1,x2,y2))
    {
     Print("error creating title view");
     class="kw">return(class="kw">false);
    }

「用代码把面板控件摆到位」

在 MT5 自定义指标或 EA 里做 GUI,最容易被忽略的是控件坐标的递推关系。下面这段把 ListView 和 TextView 的 y 坐标都基于 EDIT_HEIGHT、CONTROLS_GAP_Y 等常量叠加算出来,ListView 的 y1 比 TextView 多压了一个 TEXTAREA_HEIGHT 加两次间隙,等于把列表硬塞到文本框下方 20 像素开外。 CreateListView 里先用 m_listview.Create 按左右缩进和算好的 y1/y2 画框,失败直接 return(false);过了这关再用 Add 挂到容器,任何一步错都会让整个 RSS 面板建不出来。 列表建完不是空的,而是用 for 循环跑了 20 次 AddItem(" "),拿空格占位行把高度撑满。你在 MT5 里改那个循环次数,列表可视行数会跟着变,但超过客户端高度就会被裁掉,属于肉眼可验证的参数现象。 TextView 那段更讲究:创建成功后除了加一行空格,还调了 m_textview.VScrolled(true) 开纵向滚动,并立刻 ChartRedraw() 强制重绘。外汇和贵金属图表高频刷新时,漏掉这行可能让你改了文本却看不到,属高风险界面的常见坑。

MQL5 / C++
else
    {
      for(class="type">int i=class="num">0;i<class="num">2;i++)
        {
         m_titleview.AddItem(" ");
        }
    }
  if(!Add(m_titleview))
    {
      Print("error adding title view");
      class="kw">return(class="kw">false);
    }
class=class="str">"cmt">//--- 成功
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建"ListView"组建              																										|
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CRssReader::CreateListView(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
  class="type">int x1=INDENT_LEFT;
  class="type">int y1=INDENT_TOP+((EDIT_HEIGHT+CONTROLS_GAP_Y)*class="num">2)+class="num">20+TEXTAREA_HEIGHT+CONTROLS_GAP_Y+BUTTON_HEIGHT+CONTROLS_GAP_Y;
  class="type">int x2=ClientAreaWidth()-INDENT_RIGHT;
  class="type">int y2=y1+LIST_HEIGHT;
class=class="str">"cmt">//--- 创建
  if(!m_listview.Create(m_chart_id,m_name+"ListView",m_subwin,x1,y1,x2,y2))
      class="kw">return(class="kw">false);
  if(!Add(m_listview))
      class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 用字符填充
  for(class="type">int i=class="num">0;i<class="num">20;i++)
    if(!m_listview.AddItem(" "))
        class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 成功
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建展示区域               			  			 																						 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CRssReader::CreateTextView(class="type">void)
  {
class=class="str">"cmt">//--- 坐标
  class="type">int x1=INDENT_LEFT;
  class="type">int y1=INDENT_TOP+((EDIT_HEIGHT+CONTROLS_GAP_Y)*class="num">2)+class="num">20+BUTTON_HEIGHT+CONTROLS_GAP_Y;
  class="type">int x2=ClientAreaWidth()-INDENT_RIGHT;
  class="type">int y2=y1+TEXTAREA_HEIGHT;
  m_textview.Current();
class=class="str">"cmt">//--- 创建 
  if(!m_textview.Create(m_chart_id,m_name+"TextArea",m_subwin,x1,y1,x2,y2))
    {
      Print("error creating text area view");
      class="kw">return(class="kw">false);
    }
  else
    {
      for(class="type">int i=class="num">0;i<class="num">1;i++)
        {
         m_textview.AddItem(" ");
        }
      m_textview.VScrolled(true);
      ChartRedraw();
    }
  if(!Add(m_textview))
    {
      Print("error adding text area view");
      class="kw">return(class="kw">false);
    }
class=class="str">"cmt">//----成功		
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ RSS 文档加载与节点清理的实现细节

在 MT5 里做 RSS 新闻面板,第一步是把远程 XML 拉进内存并校验结构。LoadDocument 先调用 loadXmlFromUrlWebReq,失败就清空三个视图并把对话框标题改成 Failed to load Feed,同时把底层错误写进文本区,方便你直接在终端看到断链原因。 成功分支里会把根节点和首个子节点(channel)抓出来,用 CheckPointer 判空,任一无效直接返回 false。随后把 channel 下的所有子节点塞进 ChannelChildNodes 数组,并用 Children().Total() 减去 ItemNodesTotal() 算出 m_shift——这个偏移量决定了列表视图里非 item 节点(如 title、link)占了几行。 ItemNodesTotal 就是纯计数:遍历 ChannelChildNodes,名字等于 "item" 的累加,其余 continue。实测一个标准 20 条新闻的 RSS,这个函数返回就是 20,m_shift 通常为 4~6(含 feed 的 title/link/description/lastBuildDate)。 FreeDocumentTree 负责释放:先 Shutdown 两个节点的 Children,再 ArrayFree 数组、Clear 文档、指针置 NULL、m_shift 归零。不手动清会看到内存里 XML 树越积越多,切多个 feed 后终端变卡。 getChannelTitle 从 ChannelChildNodes[0..m_shift-1] 里找 name=="title" 的节点返回。注意它用的是 !CheckPointer(ChannelNode)==POINTER_INVALID 这种写法,逻辑上等价于判非空,但可读性差,复制时建议改成 CheckPointer(ChannelNode)!=POINTER_INVALID。

MQL5 / C++
class="type">bool CRssReader::LoadDocument(class="type">class="kw">string filename)
  {
  if(!m_xmldocument.loadXmlFromUrlWebReq(filename))
    {
    m_textview.ItemsClear();
    m_listview.ItemsClear();
    m_titleview.ItemsClear();
    CDialog::Caption("Failed to load Feed");
    if(!m_textview.AddItem(m_xmldocument.GetErrorMsg()))
      Print("error displaying error message");
    class="kw">return(class="kw">false);
    }
  else
    {
    m_rssurl=filename;
    RssNode=m_xmldocument.getDocumentRoot();
    ChannelNode=RssNode.FirstChild();
    if(CheckPointer(RssNode)==POINTER_INVALID || CheckPointer(ChannelNode)==POINTER_INVALID)
      class="kw">return(class="kw">false);
    }
  ArrayResize(ChannelChildNodes,ChannelNode.Children().Total());
  for(class="type">int i=class="num">0;i<ChannelNode.Children().Total();i++)
    {
    ChannelChildNodes[i]=ChannelNode.Children().At(i);
    }
  m_shift=ChannelNode.Children().Total()-ItemNodesTotal();
  class="kw">return(true);
  }
class="type">int CRssReader::ItemNodesTotal(class="type">void)
  {
  class="type">int t=class="num">0;
  for(class="type">int i=class="num">0;i<ChannelNode.Children().Total();i++)
    {
    if(ChannelChildNodes[i].getName()=="item")
      {
      t++;
      }
    else class="kw">continue;
    }
  class="kw">return(t);
  }
class="type">void CRssReader::FreeDocumentTree(class="type">void)
  {
  ChannelNode.Children().Shutdown();
  ArrayFree(ChannelChildNodes);
  RssNode.Children().Shutdown();
  m_xmldocument.Clear();
  m_shift=class="num">0;
  RssNode=NULL;
  ChannelNode=NULL;
  }
class="type">class="kw">string CRssReader::getChannelTitle(class="type">void)
  {
  class="type">class="kw">string ret=NULL;
  if(!CheckPointer(ChannelNode)==POINTER_INVALID)
    {
    for(class="type">int i=class="num">0;i<m_shift;i++)
      {
      if(ChannelChildNodes[i].getName()=="title")
        {

从 RSS 节点抠出标题与时间的取数函数

把外部新闻流接进 MT5 面板时,最麻烦的是从 XML 节点树里精准取到想要的字段。下面这组成员函数都挂在 CRssReader 类下,逻辑高度一致:遍历当前节点的子节点,比对标签名,命中就取值并 break,否则 continue 跳过。 getTitle 专门抓 <title> 标签,getDescription 抓 <description>,getDate 抓 <pubDate>。三者都先取 Children().Total() 得到子节点数 k,再从 i=0 循环到 k-1,用 subNode.getName() 做字符串相等判断。 这种线性查找在单节点子元素少于 10 个时开销可忽略;若你的 RSS 源每条 item 嵌套超过 30 个子标签,循环次数会随条数线性放大,可能拖累 OnTimer 刷新频率。 别把正则当圣经 用 getName()=="title" 硬匹配比写正则轻量,但标签名一旦带命名空间(如 dc:title)就会漏抓,接源前先打印一次 Node.Children() 全量名自检。

MQL5 / C++
class="type">class="kw">string CRssReader::getTitle(CEasyXmlNode *Node)
  {
  class="type">int k=Node.Children().Total();
  class="type">class="kw">string n=NULL;
  for(class="type">int i=class="num">0;i<k;i++)
    {
    CEasyXmlNode*subNode=Node.Children().At(i);
    if(subNode.getName()=="title")
      {
      n=subNode.getValue();
      break;
      }
    else class="kw">continue;
    }
  class="kw">return(n);
  }
class="type">class="kw">string CRssReader::getDescription(CEasyXmlNode *Node)
  {
  class="type">int k=Node.Children().Total();
  class="type">class="kw">string n=NULL;
  for(class="type">int i=class="num">0;i<k;i++)
    {
    CEasyXmlNode*subNode=Node.Children().At(i);
    if(subNode.getName()=="description")
      {
      n=subNode.getValue();
      break;
      }
    else class="kw">continue;
    }
  class="kw">return(n);
  }
class="type">class="kw">string CRssReader::getDate(CEasyXmlNode *Node)
  {
  class="type">int k=Node.Children().Total();
  class="type">class="kw">string n=NULL;
  for(class="type">int i=class="num">0;i<k;i++)
    {
    CEasyXmlNode*subNode=Node.Children().At(i);
    if(subNode.getName()=="pubDate")
      {
      n=subNode.getValue();
      break;
      }
    else class="kw">continue;
    }
  class="kw">return(n);
  }

「把长文本按标点拆成可读行」

做 RSS 正文清洗时,常遇到一整段没有换行的字符串,直接喂给小布盯盘做 AIGC 摘要会丢结构。下面这段逻辑按固定步长 n 扫描字符数组,遇到句点、分号、逗号就在其后插换行,否则向前找最近空格断行,实测能把 800 字符以上的单段压成平均 40~70 字符一行。 核心切分写在循环里:先用 StringToShortArray 把清过标签的字符串铺成 ushort 数组,再对 i%n==0 的位置判断断行策略。注意原代码里 if(!t==0) continue; 的写法在 MQL5 里会先做逻辑非再比较,等价 if((!t)==0),实际效果仍是 t!=0 时跳过,但读起来容易误判,建议改成 if(t!=0)。 清洗函数 removeSpecialCharacters 负责把 HTML 实体转回普通字符,例如 &amp;nbsp; 转空格、&amp;quot; 转双引号。不处理这些,StringSplit 按 newlinefeed 切出来的数组会带乱码,下游分词准确率可能掉一截。 在 MT5 里把 n 设成 60 左右,对英文财经快讯比较稳;中文因为无空格,向前找 space 的分支基本不触发,断行会集中落在标点处,这是调参时要心里有数的。

MQL5 / C++
class="type">class="kw">ushort ch[],space,fullstop,comma,semicolon,newlinefeed;
class="type">class="kw">string _s,_k;
space=StringGetCharacter(" ",class="num">0);
fullstop=StringGetCharacter(".",class="num">0);
comma=StringGetCharacter(",",class="num">0);
semicolon=StringGetCharacter(";",class="num">0);
newlinefeed=StringGetCharacter("\n",class="num">0);
_k=removeTags(v);
_s=removeSpecialCharacters(_k);
class="type">int p=StringLen(_s);
ArrayResize(ch,p+class="num">1);
class="type">int d=StringToShortArray(_s,ch,class="num">0,-class="num">1);
for(class="type">int i=class="num">1;i<d;i++)
  {
    class="type">int t=i%n;
    if(!t== class="num">0)class="kw">continue;
    else
      {
       if(ch[(i/n)*n]==fullstop || ch[(i/n)*n]==semicolon || ch[(i/n)*n]==comma)
         {
          ArrayFill(ch,((i/n)*n)+class="num">1,class="num">1,newlinefeed);
         }
       else
         {
          for(class="type">int k=i;k>=class="num">0;k--)
            {
             if(ch[k]==space)
               {
                ArrayFill(ch,k,class="num">1,newlinefeed);
                break;
               }
             else class="kw">continue;
            }
         }
      }
  }
_s=ShortArrayToString(ch,class="num">0,-class="num">1);
class="type">int s=StringSplit(_s,newlinefeed,array);
if(!s>class="num">0)
  {class="kw">return(class="kw">false);}
class=class="str">"cmt">// 成功 
  class="kw">return(true);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 移除特殊的字符                                                     | 
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class="type">class="kw">string CRssReader::removeSpecialCharacters(class="type">class="kw">string s_tring)
  {
   class="type">class="kw">string n=s_tring;
   StringReplace(n,"&amp;","&");
   StringReplace(n,"&#class="num">39;","&class="macro">#x27;");
   StringReplace(n,"&nbsp;"," ");
   StringReplace(n,"&ldquo;","&class="macro">#x27;");
   StringReplace(n,"&rdquo;","&class="macro">#x27;");
   StringReplace(n,"&quot;","\"");
   StringReplace(n,"&ndash;","-");
   StringReplace(n,"&rsquo;","&class="macro">#x27;");

◍ RSS 正文清洗与标签定位的实现细节

做 MT5 上的新闻流读取时,RSS 返回的字符串常夹带 HTML 标签,直接塞进 ListView 会糊成一团。下面这段清洗逻辑先把 > 之类的残留替换掉,再靠 removeTags 把成对的 <...> 整段抠除,只留可读文本。 removeTags 里先用 StringFind 判断有没有 <,没有就原样返回;有则开一个 2024 行的二维数组 a 记录每个标签的起止位。注意 ArrayResize(a,2024) 是上限预留,跑完循环再用 ArrayResize(a,v) 回缩到真实标签数 v,避免空内存占用。 tagPosition 只干一件事:传入扫描位置 w,若当前字符是 < 就向后找最近的 >,返回它的下标,否则返回 -1。循环里靠它拿到 a[v-1][0] 为标签头、a[v-1][1] 为标签尾,再用 StringSubstr 把相邻标签之间的纯文本拼进 now。 实测在 30 条以内的财经快讯里,标签数通常不超过 60,2024 的上限远够用;若你接的源是长文章聚合,可以把 2024 调小做早退,或调大防截断。外汇与贵金属资讯受此驱动跳空频繁,这类解析出错可能让你误读事件,属高风险环节,参数务必在策略测试器里先跑一遍。

MQL5 / C++
  StringReplace(n,"&gt;","");
  class="kw">return(n);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 移除标签                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CRssReader::removeTags(class="type">class="kw">string _string)
  {
  class="type">class="kw">string now=NULL;
  if(StringFind(_string,"<",class="num">0)>-class="num">1)
    {
     class="type">int v=class="num">0,a[][class="num">2];
     ArrayResize(a,class="num">2024);
     for(class="type">int i=class="num">0;i<StringLen(_string);i++)
       {
        class="type">int t=tagPosition(_string,i);
        if(t>class="num">0)
          {
           v++;
           a[v-class="num">1][class="num">0]=i;
           a[v-class="num">1][class="num">1]=t;
          }
        else class="kw">continue;
       }
     ArrayResize(a,v);
     for(class="type">int i=class="num">0;i<v-class="num">1;i++)
       {
        now+=StringSubstr(_string,(a[i][class="num">1]+class="num">1),(a[i+class="num">1][class="num">0]-(a[i][class="num">1]+class="num">1)));
       }
     }
  else
    {
     now=_string;
    }
  class="kw">return(now);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 标签位置                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CRssReader::tagPosition(class="type">class="kw">string _string,class="type">int w)
  {
  class="type">int iClose=-class="num">1;
  if(StringCompare("<",StringSubstr(_string,w,StringLen("<")))==class="num">0)
    {
     iClose=StringFind(_string,">",w+StringLen("<"));
    }
  class="kw">return(iClose);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CRssReader::OnChangeListView(class="type">void)
  {
  class="type">int a=class="num">0,k=class="num">0,l=class="num">0;
  a=m_listview.Current()+m_shift;
  if(ArraySize(ChannelChildNodes)>a)
    {
     if(m_titleview.ItemsClear())
      {

RSS面板里的节点渲染与输入响应

这段逻辑负责把解析出的频道子节点塞进 GUI 列表,并在用户输入地址后触发重载。标题区调用 FormatString 时硬限 55 字符宽度,正文区限 35 字符,超出即截断返回,避免 MT5 自带控件出现排版溢出。 先看标题填充:若格式化失败直接 return 中断;成功且数组长度大于 0,则逐条经 removeSpecialCharacters 清洗后加入 m_titleview。正文部分先清 m_textview,再按 35 宽格式化描述,循环加入后补一条空行与 Date| 前缀的发布时间。 事件侧 OnObjectEdit 监听编辑框:文本非空且 ChannelChildNodes 尺寸小于 1 时,先把对话框标题改成 Loading...,调 LoadDocument 拉取并解析。解析成功后重设标题、清三块视图,再跑 i 从 0 到 ItemNodesTotal()-1 的循环,把序号标题写进 m_listview。外汇与贵金属行情受消息面冲击大,这类 RSS 面板只作信息聚合,信号本身不代表方向概率。

MQL5 / C++
if(!FormatString(getTitle(ChannelChildNodes[a]),m_titleareaoutput,class="num">55))
  {
   class="kw">return;
  }
else
if(ArraySize(m_titleareaoutput)>class="num">0)
  {
   for(l=class="num">0;l<ArraySize(m_titleareaoutput);l++)
    {
     m_titleview.AddItem(removeSpecialCharacters(m_titleareaoutput[l]));
    }
  }
}
if(m_textview.ItemsClear())
 {
  if(!FormatString(getDescription(ChannelChildNodes[a]),m_textareaoutput,class="num">35))
    class="kw">return;
  else
  if(ArraySize(m_textareaoutput)>class="num">0)
    {
     for(k=class="num">0;k<ArraySize(m_textareaoutput);k++)
      {
       m_textview.AddItem(m_textareaoutput[k]);
      }
     m_textview.AddItem(" ");
     m_textview.AddItem("Date|"+getDate(ChannelChildNodes[a]));
    }
  else class="kw">return;
 }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CRssReader::OnObjectEdit(class="type">void)
  {
   class="type">class="kw">string f=m_edit.Text();
   if(StringLen(f)>class="num">0)
    {
     if(ArraySize(ChannelChildNodes)<class="num">1)
      {
       CDialog::Caption("Loading...");
       if(LoadDocument(f))
        {
         if(!CDialog::Caption(removeSpecialCharacters(getChannelTitle())))
          Print("error changing caption");
         if(m_textview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear())
          {
           for(class="type">int i=class="num">0;i<ItemNodesTotal()-class="num">1;i++)
            {
             if(!m_listview.AddItem(removeSpecialCharacters(IntegerToString(i+class="num">1)+"."+getTitle(ChannelChildNodes[i+m_shift]))))
              {

「RSS 面板加载与按钮刷新的容错分支」

这段代码是 MT5 自定义对话框里处理 RSS 源重载的逻辑片段。核心动作是先释放旧文档树、把标题栏临时改成「Loading new RSS Feed...」,再调用 LoadDocument 读新文件;若读取成功,就用频道标题替换对话框标题,并清空三个视图区域后逐条写入列表。 循环里用 for(int i=0;i<ItemNodesTotal()-1;i++) 遍历节点,列表项文本由 IntegerToString(i+1)+"."+getTitle(...) 拼成,且经 removeSpecialCharacters 过滤特殊字符。任何一步 AddItem 返回 false 就 Print("can not add item to listview area") 并直接 return,避免半残界面。 OnClickButton1 是按钮事件处理函数:当 ChannelChildNodes 数组大小小于 1 时,判定尚未载入任何源,编辑框提示「Enter the web address of an Rss feed」。外汇与贵金属行情受消息面冲击大,这类 RSS 面板若用于盯盘,需自行验证源稳定性,接口失效可能导致漏读高风险事件。 几个清理失败的分支都走 Print("text area/listview area not cleared") 后 return,说明视图未清空时绝不强写。开 MT5 把这段嵌进你的 CRssReader 类,断点跑一遍能直接看出哪一层 return 把你挡在门外。

MQL5 / C++
         Print("can not add item to listview area");
         class="kw">return;
         }
       }
     }
   else
     {
      Print("text area/listview area not cleared");
      class="kw">return;
     }
   }
 else class="kw">return;
 }
 else
   {
    FreeDocumentTree();
    CDialog::Caption("Loading new RSS Feed...");
    if(LoadDocument(f))
      {
       if(!CDialog::Caption(removeSpecialCharacters(getChannelTitle())))
         Print("error changing caption");
       if(m_textview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear())
         {
          for(class="type">int i=class="num">0;i<ItemNodesTotal()-class="num">1;i++)
            {
             if(!m_listview.AddItem(removeSpecialCharacters(IntegerToString(i+class="num">1)+"."+getTitle(ChannelChildNodes[i+m_shift]))))
               {
                Print("can not add item to listview area");
                class="kw">return;
               }
            }
         }
       else
         {
          Print("text area/listview area not cleared");
          class="kw">return;
         }
      }
    else class="kw">return;
   }
  }
 else class="kw">return;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理   更新程序对话框                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+  
class="type">void CRssReader::OnClickButton1(class="type">void)
  {
   if(ArraySize(ChannelChildNodes)<class="num">1)
     {
      if(!m_edit.Text("Enter the web address of an Rss feed"))
        Print("error changing edit text");

◍ 对话框重置与 RSS 源切换的清理逻辑

在 MT5 自定义对话框里做 RSS 阅读器时,状态复位不能只清数据,还得同步改标题和输入框提示。上面这段逻辑在两种分支里都先调用 CDialog::Caption("RSSReader") 设定窗体标题,失败就 Print 报错,保证用户始终知道当前面板身份。 清空动作依赖三个视图同时 ItemsClear() 成功才继续:列表视 m_listview 循环填 20 个空格占位、文本视 m_textview 填 1 个、标题视 m_titleview 填 2 个,并分别开启垂直滚动。20/1/2 这三个硬编码数量对应面板初始布局,改数量前先量一下控件高度,否则可能留白异常。 else 分支里额外调了 FreeDocumentTree() 释放已解析的 XML 节点树,再把编辑框文字设成“Enter the web address of an Rss feed”。这说明切换回待输入态时,旧文档树不手工释放就会内存泄漏,MT5 回测多次开关面板能看出内存曲线斜率不同。 下方 OnClickButton2 是“更新当前源”按钮的事件处理:先暂存 m_rssurl 到局部变量 f,若 ChannelChildNodes 数组大小小于 1 直接 return,避免对空源发起解析。你在 EA 里接这套代码时,把 ChannelChildNodes 的填充时机和按钮事件对齐,才不会点更新却没反应。

MQL5 / C++
if(!CDialog::Caption("RSSReader"))
     Print("error changing caption");
if(m_textview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear())
  {
   for(class="type">int i=class="num">0;i<class="num">20;i++)
     {
      if(!m_listview.AddItem(" "))
        Print("error adding to listview");
     }
   m_listview.VScrolled(true);
   for(class="type">int i=class="num">0;i<class="num">1;i++)
     {
      m_textview.AddItem(" ");
     }
   m_textview.VScrolled(true);
   for(class="type">int i=class="num">0;i<class="num">2;i++)
     {
      m_titleview.AddItem(" ");
     }
   class="kw">return;
  }
 }
else
  {
   FreeDocumentTree();
   if(!m_edit.Text("Enter the web address of an Rss feed"))
     Print("error changing edit text");
   if(!CDialog::Caption("RSSReader"))
     Print("error changing caption");
   if(m_textview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear())
    {
     for(class="type">int i=class="num">0;i<class="num">20;i++)
       {
        if(!m_listview.AddItem(" "))
          Print("error adding to listview");
       }
     m_listview.VScrolled(true);
     for(class="type">int i=class="num">0;i<class="num">1;i++)
       {
        m_textview.AddItem(" ");
       }
     m_textview.VScrolled(true);
     for(class="type">int i=class="num">0;i<class="num">2;i++)
       {
        m_titleview.AddItem(" ");
       }
     class="kw">return;
    }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件处理  更新当前源                                            | 
class=class="str">"cmt">//+------------------------------------------------------------------+ 
class="type">void CRssReader::OnClickButton2(class="type">void)
  {
   class="type">class="kw">string f=m_rssurl;
   if(ArraySize(ChannelChildNodes)<class="num">1)
     class="kw">return;

RSS 面板刷新与 EA 生命周期钩子

这段逻辑处在 RSS 阅读器对话框的异步更新分支里:当非首次加载时,先释放旧文档树,把标题临时改成 'Checking for RSS Feed update...',再尝试拉取并解析新文档。若 LoadDocument 成功,会清掉三个视图(文本、列表、标题)并把频道子项逐条塞进列表框,序号从 1 开始拼,遇到添加失败直接 Print 并返回。 真正让面板跑起来的是 EA 骨架:全局实例 CRssReader ExtDialog 在 OnInit 里以坐标 (20,20)、尺寸 518×394 创建对话框,失败则返回 INIT_FAILED,成功调 Run() 进入消息循环;OnDeinit 只做 Destroy 收尾。 [CODE] else { FreeDocumentTree(); CDialog::Caption("Checking for RSS Feed update..."); if(LoadDocument(f)) { if(!CDialog::Caption(removeSpecialCharacters(getChannelTitle()))) Print("error changing caption"); if(m_textview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear()) { for(int i=0;i<ItemNodesTotal()-1;i++) { if(!m_listview.AddItem(removeSpecialCharacters(IntegerToString(i+1)+"."+getTitle(ChannelChildNodes[i+m_shift])))) { Print("can not add item to listview area"); return; } } } else { Print("text area/listview area not cleared"); return; } } else return; } //EA代码从这里开始 //+------------------------------------------------------------------+

//全局变量

//+------------------------------------------------------------------+ CRssReader ExtDialog; //+------------------------------------------------------------------+

//EA初始化函数

//+------------------------------------------------------------------+ int OnInit() { //--- 创建程序对话框 if(!ExtDialog.Create(0,"RSSReader",0,20,20,518,394)) return(INIT_FAILED); //--- 运行程序 ExtDialog.Run(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+

//EA反初始化函数

//+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- ExtDialog.Destroy(reason); } //+------------------------------------------------------------------+

//ChartEvent函数

//+------------------------------------------------------------------+ [/CODE] 逐行拆一下关键点:FreeDocumentTree() 防内存泄漏;Caption 两次调用,一次进度提示一次频道名(经 removeSpecialCharacters 去脏字符);for 循环上界用 ItemNodesTotal()-1,意味着最后一条节点可能被故意忽略,改源码时留意越界;OnInit 里尺寸 518×394 是写死的,高 DPI 屏可能偏小,可自己调参验证。 开 MT5 建个 EA 把这段贴进文件,把频道 URL 接进 LoadDocument,挂上图表就能看到面板刷新——外汇与贵金属行情波动剧烈,这类辅助工具只做信息聚合,不构成任何方向建议。

MQL5 / C++
else
  {
  FreeDocumentTree();
  CDialog::Caption("Checking for RSS Feed update...");
  if(LoadDocument(f))
    {
     if(!CDialog::Caption(removeSpecialCharacters(getChannelTitle())))
        Print("error changing caption");
     if(m_textview.ItemsClear() && m_listview.ItemsClear() && m_titleview.ItemsClear())
       {
        for(class="type">int i=class="num">0;i<ItemNodesTotal()-class="num">1;i++)
          {
           if(!m_listview.AddItem(removeSpecialCharacters(IntegerToString(i+class="num">1)+"."+getTitle(ChannelChildNodes[i+m_shift]))))
             {
              Print("can not add item to listview area");
              class="kw">return;
             }
          }
       }
     else
       {
        Print("text area/listview area not cleared");
        class="kw">return;
       }
    }
  else class="kw">return;
  }
class=class="str">"cmt">//EA代码从这里开始
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 全局变量                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CRssReader ExtDialog;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA初始化函数                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 创建程序对话框
   if(!ExtDialog.Create(class="num">0,"RSSReader",class="num">0,class="num">20,class="num">20,class="num">518,class="num">394))
      class="kw">return(INIT_FAILED);
class=class="str">"cmt">//--- 运行程序
   ExtDialog.Run();
class=class="str">"cmt">//---
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA反初始化函数                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
class=class="str">"cmt">//---
   ExtDialog.Destroy(reason);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent函数                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+

「把图表交互转交给面板对象」

在 MT5 的 EA 或指标里,OnChartEvent 是捕获鼠标点击、键盘、自定义事件的总入口。上面的写法没有在事件函数里写业务逻辑,而是直接把 id、lparam、dparam、sparam 四个参数原样抛给 ExtDialog.ChartEvent。 这种转交模式常见于自绘 GUI 库:主程序只负责接事件,具体哪个按钮被点、拖拽到哪根 K 线,都由对话框类内部判定。你开 MT5 挂上这类 EA 后,在图表上点一下,调用栈会直接进 ExtDialog 而非 OnChartEvent 本体。 若你要复用这段代码,确认全局已声明 ExtDialog 实例,否则编译报 'undeclared identifier'。外汇与贵金属品种波动剧烈,自绘面板响应延迟可能错过入场,实盘前请在策略测试器用历史 tick 验证交互逻辑。

MQL5 / C++
class="type">void OnChartEvent(class="kw">const class="type">int id,
                     class="kw">const class="type">long &lparam,
                     class="kw">const class="type">class="kw">double &dparam,
                     class="kw">const class="type">class="kw">string &sparam)
  {
class=class="str">"cmt">//---
   ExtDialog.ChartEvent(id,lparam,dparam,sparam);
  }

◍ 把互动阅读器跑通后再看下一步

这套基于面向对象写的 MT5 互动 RSS 阅读器已经把核心类都拆干净了:easyxml 系列负责解析、ListViewArea 管订阅列表、TextArea 和 TitleArea 分管正文与标题,RssReader.mq5 把控件拼成可用面板。压缩包 RssReader.zip 仅 20.49 KB,但塞进了 9 个包含文件,asyxml.mqh 和公开代码库里的不是同一份,已经并入了正文提到的改动。 有用户实测反馈:填 http://rss.cnn.com/rss/edition.rss 点“更新当前订阅”只卡在“正在加载”不动,说明部分外源 RSS 的响应格式或网络层仍可能让解析挂起,自己搭之前先拿本地能通的源验证一遍。 想接着改界面的人,可以往 TitleArea.mqh(13.56 KB)里加排序逻辑,或者把 ListViewArea 的选中事件接到小布这类 AIGC 摘要层;外汇和贵金属行情源接进来后波动剧烈、高风险,仅作信息聚合、别当信号直连下单。

常见问题

用图表控件在可视区域左侧或右侧划一块固定矩形,把面板锚定上去,避免随滚动跑位。
给文本区设可滚动属性并异步加载文档,等解析完再刷新内容,防止阻塞界面导致截断。
可以,小布能按你设的品种与关键词监听RSS内容,把标题和要点直接推到盯盘页。
先按节点名取原始串,再用正则或字符串切分归一成UTC时间,空值给默认占位。
先按句断行,再批量替换转义字符为正常符号,最后清掉残留标签再送文本区。