在MetaTrader5中创建交互应用来展现RSS订阅·综合运用
- 在MT5图表上拼出RSS面板控件
- RSS面板里的文本区与文档加载
- RSS 节点树的内存回收与字段提取
- 从 RSS 节点抠出标题、描述与日期
- 把 RSS 正文按句断行再拆标签
- RSS 正文里的转义字符清洗
- RSS 控件里的标签定位与列表刷新
- RSS 输入触发后的界面重建逻辑
- RSS面板空数据与按钮刷新逻辑
- RSS面板刷新时的清空与重建逻辑
- RSS阅读器主类的私有成员与接口骨架
- RSS 阅读器面板的事件与接口骨架
- RSS 面板控件的初始化顺序
- 控件坐标与按钮实例的落地写法
- 用代码把面板控件摆到位
- RSS 文档加载与节点清理的实现细节
- 从 RSS 节点抠出标题与时间的取数函数
- 把长文本按标点拆成可读行
- RSS 正文清洗与标签定位的实现细节
- RSS面板里的节点渲染与输入响应
- RSS 面板加载与按钮刷新的容错分支
- 对话框重置与 RSS 源切换的清理逻辑
- RSS 面板刷新与 EA 生命周期钩子
- 把图表交互转交给面板对象
- 把互动阅读器跑通后再看下一步
◍ 在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。外汇与贵金属行情波动剧烈、杠杆风险高,这类面板只做信息聚合,信号本身不预示涨跌。
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 面板盯突发新闻时务必认清高杠杆下的穿仓风险。
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 下的标题、描述、日期字段是延迟到调用时才从节点树提取,而非解析时一次性拷贝,内存占用倾向更平滑。
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里跑一遍校验。
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 的概率取决于源站格式,属高风险数据管道环节。
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;、&nbsp;、&quot; 这类 HTML 实体。直接塞进 MT5 做文本分析会干扰关键词匹配,需要在入库前统一替换掉。 下面这段是 CRssReader 类的成员函数 removeSpecialCharacters,入参是原始字符串,出参是清洗后的字符串。它连续调用 StringReplace 做一对一替换,不依赖正则,执行开销极低。 关键映射关系:&amp;→&,&nbsp;→空格,&quot;→双引号,&gt;→空(直接删掉标签残片)。在 MT5 里跑这个函数,一个 2KB 的 RSS item 字符串处理耗时通常在 0.1 毫秒以内。 外汇与贵金属资讯流若走 RSS 自动采集,这类清洗是必须的预处理;不过自动采集的源质量参差不齐,信号误判风险偏高,仅适合做辅助参考。
class="type">class="kw">string CRssReader::removeSpecialCharacters(class="type">class="kw">string s_tring) { class="type">class="kw">string n=s_tring; StringReplace(n,"&","&"); StringReplace(n,"&#class="num">39;","&class="macro">#x27;"); StringReplace(n," "," "); StringReplace(n,"“","&class="macro">#x27;"); StringReplace(n,"”","&class="macro">#x27;"); StringReplace(n,""","\""); StringReplace(n,"–","-"); StringReplace(n,"’","&class="macro">#x27;"); StringReplace(n,">",""); 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 的刷新卡顿倾向明显下降;外汇与贵金属行情资讯高频刷新,这类自写控件需注意内存与高频重绘风险。
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 上跑起来你能直接观察标题栏与列表的刷新时序。
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 行情源须自行处理超时与断线。
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,保证界面不会半残。外汇或贵金属资讯接这种面板,刷新失败可能让你漏看关键事件,实盘前务必在策略测试器外手动点几次验证。
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 这类继承链,能直接照这个成员布局抄一套自己的新闻面板。
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 逻辑。
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_ 系列宏值就能直观看到边距变化;外汇与贵金属插件开发属高风险定制,界面逻辑错可能导致下单控件被遮。
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 宽以下窗口可能就会挤变形,值得自己改参验证。
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() 强制重绘。外汇和贵金属图表高频刷新时,漏掉这行可能让你改了文本却看不到,属高风险界面的常见坑。
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。
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() 全量名自检。
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 实体转回普通字符,例如 &nbsp; 转空格、&quot; 转双引号。不处理这些,StringSplit 按 newlinefeed 切出来的数组会带乱码,下游分词准确率可能掉一截。
在 MT5 里把 n 设成 60 左右,对英文财经快讯比较稳;中文因为无空格,向前找 space 的分支基本不触发,断行会集中落在标点处,这是调参时要心里有数的。
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,"&","&"); StringReplace(n,"&#class="num">39;","&class="macro">#x27;"); StringReplace(n," "," "); StringReplace(n,"“","&class="macro">#x27;"); StringReplace(n,"”","&class="macro">#x27;"); StringReplace(n,""","\""); StringReplace(n,"–","-"); StringReplace(n,"’","&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 调小做早退,或调大防截断。外汇与贵金属资讯受此驱动跳空频繁,这类解析出错可能让你误读事件,属高风险环节,参数务必在策略测试器里先跑一遍。
StringReplace(n,">",""); 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 面板只作信息聚合,信号本身不代表方向概率。
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 把你挡在门外。
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 的填充时机和按钮事件对齐,才不会点更新却没反应。
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,挂上图表就能看到面板刷新——外汇与贵金属行情波动剧烈,这类辅助工具只做信息聚合,不构成任何方向建议。
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 验证交互逻辑。
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 摘要层;外汇和贵金属行情源接进来后波动剧烈、高风险,仅作信息聚合、别当信号直连下单。