在MetaTrader5中创建交互应用来展现RSS订阅·进阶篇
(2/3)· 从XML解析到控件事件,手把手把RSS订阅做成可交互的终端应用
◍ 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 加载,就能验证头部跳过是否干净。
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 里改这几个值,能直接看到面板呼吸感变化。
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 是否引用了第三方库——这是验证骨架最快的路径。
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 里生成的一致。 外汇与贵金属行情受新闻冲击大,这类面板仅作信息聚合,任何一条快讯都不构成交易信号,杠杆品种波动可能瞬间扫掉止损。
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) 让默认处于按下态。这种高反差在小窗面板里辨识度最高,但也意味着你得在事件里自己处理弹起,否则看着像卡住。外汇和贵金属图表挂这类面板属于高风险环境下的辅助显示,别让它拦截了你的手动下单点击。
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);