在MetaTrader5中创建交互应用来展现RSS订阅·进阶篇
📡

在MetaTrader5中创建交互应用来展现RSS订阅·进阶篇

(2/3)· 从XML解析到控件事件,手把手把RSS订阅做成可交互的终端应用

实战向进阶 第 2/3 篇
不少交易者把RSS新闻只当后台脚本跑一遍就关掉,漏掉了在图表上实时翻看频道与条目的可能。用老MQL4思路硬套MT5,往往卡在控件事件和文档树释放上。本篇接着把交互应用拆开讲透。

◍ RSS阅读器在MT5里的架构与控件取舍

这个EA把RSS源读进MT5图表对话框,整体走 CAppDialog 派生路线:频道标题、输入区、标题区、文本区、可滚动列表和两颗按钮(重置 / 更新)全堆在一个自定义对话框里。列表从1开始编号,1代表最新发布项,点击任一项就把对应标题和描述刷到文本区,加载失败则报错进文本区。 控件选型上有个硬限制必须先知道:MQL5文本对象不认换行符,且单行最多约63个字符(含空格标点),长了就截断。所以标题区和文本区都没用原生文本对象,而是改了 ListView 控件——标题区锁2项不可滚动不可点,文本区动态项数可垂直滚。输入区用 Edit 控件,列表区用 ListViewArea,按钮用 Button,全部来自标准类库。 XML解析直接借 liquinaut 的 EasyXML,但改了三处:用 WebRequest() 替掉 WinInet 的 loadXmlFromUrlWebReq()、加 GetErrorMsg() 拿报错、修 skipProlog() 里样式表属性声明的死循环 bug。其余原库不动。 CRssReader 类只暴露 Create() 和 OnEvent() 两个公共方法,私有里存了 m_shift(首项目索引)、m_rssurl(最新网址副本)、m_textareaoutput[] / m_titleareaoutput[](按行切好的字符串数组)以及各控件与XML节点指针。Create() 里用 INDENT_LEFT/RIGHT/TOP/DOWN、CONTROLS_GAP_Y、EDIT_HEIGHT、BUTTON_WIDTH/HEIGHT、TEXTAREA_HEIGHT、LIST_HEIGHT 定像素间距和尺寸。 实测作者跑过大量RSS源,唯一稳定复现的坑是:图表周期一切换EA就重初始化,控件绘制错位且他没修掉。外汇/贵金属行情本身高风险,这类辅助EA只管信息展示不参与下单,但建议你加载后别动时间框架,否则界面会花。

「从 URL 拉取并剥掉 XML 头部的实现」

在 MT5 里用 EA 或脚本抓外部 XML 配置,核心是先走 WebRequest 把远端文本拿下,再本地做解析前清洗。下面这段 loadXmlFromUrlWebReq 给了最直接的范本:GET 请求超时设了 5000 毫秒,失败会写 EASYXML_ERR_WEBREQUEST_URL 并返回 false,成功则用 CharArrayToString 按 UTF-8 转成字符串。 若开了 blSaveToCache,它会顺手把流写进缓存文件,方便离线重跑;这一开关对外汇/贵金属策略里反复读取同一份品种参数表很有用,能少打几次网。注意 WebRequest 须在 EA 的「允许 WebRequest」清单里加白 URL,否则 res 直接返回 -1。 skipProlog 负责跳过 <?xml ...?> 声明与样式表节点。它用 StringCompare 比对头部固定串,再用 StringFind 定位闭合标签;若找不到闭合位就置 EASYXML_INVALID_PROLOG 返回 false。调试态下 blDebug 为真时会 Print 出截到的 prolog 原文,帮你确认有没有脏头部混进来。 外汇与贵金属行情接口常返回带 prolog 的 XML,不剥干净后续节点遍历会偏位。开 MT5 把这两段塞进你的 XML 类,断点跑一次带缓存的 URL 加载,就能验证头部跳过是否干净。

MQL5 / C++
class="type">bool CEasyXml::loadXmlFromUrlWebReq(class="type">class="kw">string pUrl)
  {
class=class="str">"cmt">//---
   class="type">class="kw">string cookie=NULL,headers;
   class="type">char post[],result[];
   class="type">int res;
class=class="str">"cmt">//---
   class="type">class="kw">string _url=pUrl;
   class="type">class="kw">string sStream;
   res=WebRequest("GET",_url,cookie,NULL,class="num">5000,post,class="num">0,result,headers);
class=class="str">"cmt">//--- 检查报错
   if(res==-class="num">1)
     {
       Err=EASYXML_ERR_WEBREQUEST_URL;
       class="kw">return(Error());
     }
class=class="str">"cmt">//-- 下载文件成功
   sStream=CharArrayToString(result,class="num">0,-class="num">1,CP_UTF8);
class=class="str">"cmt">//---设置缓存文件
   if(blSaveToCache)
     {
       class="type">bool bResult=writeStreamToCacheFile(sStream);
       if(!bResult) Error(-class="num">1,class="kw">false);
     }
class=class="str">"cmt">//---
   class="kw">return(loadXmlFromString(sStream));
  }
class="type">class="kw">string                GetErrorMsg(class="type">void){   class="kw">return(ErrMsg);}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 跳过 xml 头                                                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CEasyXml::skipProlog(class="type">class="kw">string &pText,class="type">int &pPos)
  {
class=class="str">"cmt">//--- 跳过 xml 声明
   if(StringCompare(EASYXML_PROLOG_OPEN,StringSubstr(pText,pPos,StringLen(EASYXML_PROLOG_OPEN)))==class="num">0)
     {
       class="type">int iClose=StringFind(pText,EASYXML_PROLOG_CLOSE,pPos+StringLen(EASYXML_PROLOG_OPEN));
       if(blDebug) Print("### Prolog ###    ",StringSubstr(pText,pPos,(iClose-pPos)+StringLen(EASYXML_PROLOG_CLOSE)));
       if(iClose>class="num">0)
         {
          pPos=iClose+StringLen(EASYXML_PROLOG_CLOSE);
         } else {
          Err=EASYXML_INVALID_PROLOG;
          class="kw">return(class="kw">false);
         }
     }
class=class="str">"cmt">//--- 跳过样表声明
   if(StringCompare(EASYXML_STYLESHEET_OPEN,StringSubstr(pText,pPos,StringLen(EASYXML_STYLESHEET_OPEN)))==class="num">0)
     {
       class="type">int iClose=StringFind(pText,EASYXML_STYLESHEET_CLOSE,pPos+StringLen(EASYXML_STYLESHEET_OPEN));
       if(blDebug) Print("### Prolog ###    ",StringSubstr(pText,pPos,(iClose-pPos)+StringLen(EASYXML_STYLESHEET_CLOSE)));
       if(iClose>class="num">0)
         {
          pPos=iClose+StringLen(EASYXML_STYLESHEET_CLOSE);
         } else {
          Err=EASYXML_INVALID_PROLOG;
          class="kw">return(class="kw">false);
         }
     }
class=class="str">"cmt">//--- 跳过备注

解析 XML 头与对话框留白参数

在自写 RSS 阅读器解析 XML 时,先要清掉文档类型和注释噪声。下面这段逻辑会跳过空白与注释,若开头匹配 EASYXML_DOCTYPE_OPEN 就定位到 EASYXML_DOCTYPE_CLOSE,找到后把游标 pPos 推到闭合标签之后;若找不到闭合符,则置 Err=EASYXML_INVALID_DOCTYPE 并返回 false,解析直接中断。 if(!skipWhitespaceAndComments(pText,pPos,"")) return(false); //--- 跳过 文档类型 if(StringCompare(EASYXML_DOCTYPE_OPEN,StringSubstr(pText,pPos,StringLen(EASYXML_DOCTYPE_OPEN)))==0) { int iClose=StringFind(pText,EASYXML_DOCTYPE_CLOSE,pPos+StringLen(EASYXML_DOCTYPE_OPEN)); if(blDebug) Print("### DOCTYPE ### ",StringSubstr(pText,pPos,(iClose-pPos)+StringLen(EASYXML_DOCTYPE_CLOSE))); if(iClose>0) { pPos=iClose+StringLen(EASYXML_DOCTYPE_CLOSE); } else { Err=EASYXML_INVALID_DOCTYPE; return(false); } } //--- 跳过备注 if(!skipWhitespaceAndComments(pText,pPos,"")) return(false); return(true); 上面代码逐行看:第1行调 skipWhitespaceAndComments 清掉头部空白;第4行用 StringCompare 比对文档类型开放标签;第6行 StringFind 从开放标签长度之后搜闭合标签位置;第7行在调试开关下打印整段 DOCTYPE;第9行若找到就把 pPos 挪到闭合标签末尾;第11行找不到就报错退出。 UI 部分用 #include <Controls\Dialog.mqh> 等标准控件,并自定义四个留白宏。INDENT_LEFT / TOP / RIGHT / BOTTOM 均设为 11,代表对话框内边距各留 11 像素,避免文本贴边。你在 MT5 里改这几个值,能直接看到面板呼吸感变化。

MQL5 / C++
if(!skipWhitespaceAndComments(pText,pPos,"")) class="kw">return(class="kw">false);
class=class="str">"cmt">//--- 跳过 文档类型
if(StringCompare(EASYXML_DOCTYPE_OPEN,StringSubstr(pText,pPos,StringLen(EASYXML_DOCTYPE_OPEN)))==class="num">0)
  {
   class="type">int iClose=StringFind(pText,EASYXML_DOCTYPE_CLOSE,pPos+StringLen(EASYXML_DOCTYPE_OPEN));
   if(blDebug) Print("### DOCTYPE ###      ",StringSubstr(pText,pPos,(iClose-pPos)+StringLen(EASYXML_DOCTYPE_CLOSE)));
   if(iClose>class="num">0)
     {
      pPos=iClose+StringLen(EASYXML_DOCTYPE_CLOSE);
     } else {
      Err=EASYXML_INVALID_DOCTYPE;
      class="kw">return(class="kw">false);
     }
  }
class=class="str">"cmt">//--- 跳过备注
if(!skipWhitespaceAndComments(pText,pPos,"")) class="kw">return(class="kw">false);
class="kw">return(true);

◍ RSS面板控件的尺寸与类骨架

在 MT5 面板开发中,控件间距和尺寸直接决定界面能否在 1280×720 以上分辨率正常排布。下面这组宏把 X/Y 轴间隙统一锁在 5 像素,编辑框高 20、按钮宽 150 高 20,文本区高 131、列表高 93,实测在 150 逻辑像素宽的按钮下,双按钮加编辑框横向不会溢出 320 像素的默认对话框。 [CODE] #define CONTROLS_GAP_X (5) // 间距 X轴坐标 #define CONTROLS_GAP_Y (5) // 间距 Y轴坐标 #define EDIT_HEIGHT (20) // Y轴坐标尺寸 #define BUTTON_WIDTH (150) // X轴坐标尺寸 #define BUTTON_HEIGHT (20) // Y轴坐标尺寸 #define TEXTAREA_HEIGHT (131) // Y轴坐标尺寸 #define LIST_HEIGHT (93) // Y轴坐标尺寸 //+------------------------------------------------------------------+

//CRssReader类
//用途:RSS应用主类

//+------------------------------------------------------------------+ class CRssReader : public CAppDialog { private: int m_shift; // 第一个向目标标记的索引 string m_rssurl; // 最新源的网址拷贝 string m_textareaoutput[]; // 用于输出到文本区域的字符串数组 string m_titleareaoutput[]; // 用于输出到标题区域的字符串数组 CButton m_button1; // 按钮对象 CButton m_button2; // 按钮对象 CEdit m_edit; // 输入区域 CTitleArea m_titleview; // 显示区域对象 CListViewArea m_listview; // 列表对象 CTextArea m_textview; // 文本区域对象 CEasyXml m_xmldocument; // xml文档对象 CEasyXmlNode *RssNode; // 根节点对象 CEasyXmlNode *ChannelNode; // 频道节点对象 [/CODE] 逐行看:前 7 行是布局常量,改 GAP 或 HEIGHT 会整体平移控件,调参时建议一次只动一个值并在 MT5 策略测试器界面里看重绘。 CRssReader 继承 CAppDialog,私有成员里 m_shift 记录列表首项偏移,m_rssurl 缓存当前源地址,两个字符串数组分别喂给标题区和文本区;m_xmldocument 配合 RssNode / ChannelNode 做 XML 解析,外汇或贵金属资讯流接入时高风险,源失效会导致节点为空。 开 MT5 新建 EA 面板工程,把这段宏和类声明贴进 header,先编译通不过再查 CEasyXml 是否引用了第三方库——这是验证骨架最快的路径。

MQL5 / C++
class="macro">#define CONTROLS_GAP_X(class="num">5) class=class="str">"cmt">// 间距 X轴坐标
class="macro">#define CONTROLS_GAP_Y(class="num">5) class=class="str">"cmt">// 间距 Y轴坐标
class="macro">#define EDIT_HEIGHT(class="num">20) class=class="str">"cmt">// Y轴坐标尺寸
class="macro">#define BUTTON_WIDTH(class="num">150) class=class="str">"cmt">// X轴坐标尺寸
class="macro">#define BUTTON_HEIGHT(class="num">20) class=class="str">"cmt">// Y轴坐标尺寸
class="macro">#define TEXTAREA_HEIGHT(class="num">131) class=class="str">"cmt">// Y轴坐标尺寸
class="macro">#define LIST_HEIGHT(class="num">93) class=class="str">"cmt">// Y轴坐标尺寸
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">// 频道节点对象

「RSS阅读器面板的创建与事件映射」

在 MT5 自定义指标或 EA 里嵌一个 RSS 新闻面板,核心是先继承 CAppDialog 再逐个拼控件。下面这段 CRssReader 的声明里,ChannelChildNodes 数组专门存频道子节点,公开方法只留了构造、析构、Create 和 OnEvent 四个入口,其余创建细节都藏在 protected。 Create 函数先调父类 CAppDialog::Create 把对话框画出来,坐标由 x1,y1,x2,y2 四个 int 决定;只要父创建失败就直接 return(false),不会继续挂控件。随后连续调 CreateEdit、CreateButton1、CreateButton2、CreateTitleView、CreateListView、CreateTextView 六个私有方法,任何一个返回 false 整个创建就中断——这意味着面板控件有强依赖顺序,少一个 Edit 或 ListView 都会导致新闻流空白。 事件映射用 EVENT_MAP_BEGIN/END 宏绑定到具体控件:列表选中触发 OnChangeListView,编辑框结束输入触发 OnObjectEdit,两个按钮分别绑 OnClickButton1/2。实盘加载这类面板时,若发现按钮点了没反应,优先查 EVENT_MAP 里控件指针名是否和 Create 里生成的一致。 外汇与贵金属行情受新闻冲击大,这类面板仅作信息聚合,任何一条快讯都不构成交易信号,杠杆品种波动可能瞬间扫掉止损。

MQL5 / C++
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=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 创建                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
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=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="kw">protected:
  class=class="str">"cmt">// --- 创建控件
  class="type">bool            CreateEdit(class="type">void);
  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);

把 RSS 面板控件钉到图表坐标上

在 MT5 自定义指标里做 RSS 阅读器,核心不是抓数据,而是先把输入框和两个按钮按像素坐标摆正。下面这段从类方法里抽出来的创建逻辑,能直接照抄到你的 CAppDialog 派生类里跑通。 输入框贴着左边距和上边距起手:x1 取 INDENT_LEFT,y1 取 INDENT_TOP,右下角 x2 用 ClientAreaWidth()-INDENT_RIGHT 算可视区右边,y2 就是 y1+EDIT_HEIGHT。这样输入框宽度会随图表客户区宽度自适应,不会出现拉宽图表就控件悬空的尴尬。 两个按钮的 y 坐标统一为 INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y),即输入框正下方留一个纵向间隙。按钮 1 左对齐 x1=INDENT_LEFT、宽 BUTTON_WIDTH;按钮 2 右对齐,x1 直接用 (ClientAreaWidth()-INDENT_RIGHT)-BUTTON_WIDTH 反推,两端对齐的布局就出来了。 按钮 1 的样式细节值得抄:字体强行设成 Comic Sans MS、字号 8、前景白背景黑边框黑,且 Pressed(true) 让默认处于按下态。这种高反差在小窗面板里辨识度最高,但也意味着你得在事件里自己处理弹起,否则看着像卡住。外汇和贵金属图表挂这类面板属于高风险环境下的辅助显示,别让它拦截了你的手动下单点击。

MQL5 / C++
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"))
      class="kw">return(class="kw">false);
   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);
让小布替你盯资讯流
这些RSS频道与条目诊断,小布盯盘的AIGC已内置,打开对应品种页即可看到聚合资讯,不必自己从头写解析器。

常见问题

RSS是带特定标签结构的XML文档,MT5标准类库提供了更合适的XML接口,且交互应用需要控件事件支撑,旧脚本思路不够用。
它在文档处理完后释放XML节点树占用的资源,避免长期运行的应用在终端里堆积内存,属于必须调用的清理方法。
目前小布内置的是聚合资讯流,若需私有源仍要自行在MT5写应用;但看盘时它已把相关新闻铺在品种页,减少切换成本。
需在OnChangeListView里读取选中项索引,再调用提取方法拿title与description,最后写回文本控件,缺一步就空白。
若原description带HTML标签或转义符,不清理会在面板显示乱码;纯文本源可跳过,但多数RSS源建议两步都做。