English Русский Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
在MetaTrader5中创建交互应用来展现RSS订阅

在MetaTrader5中创建交互应用来展现RSS订阅

MetaTrader 5EA交易 | 30 九月 2015, 16:59
1 813 0
Francis Dube
Francis Dube

目录


简介

文章"通过MQL4语言读取RSS新闻"介绍了一种基本的脚本,它使用简单的库在终端中来展现RSS订阅,最初是用于解析HTML文档的。

随着MetaTrader 5和MQL5变成语言的出现,我认为创建互动应用来展现RSS内容将成为可能。本文描述如何使用MQL5标准类库以及MQL5社区会员开发的一些工具来制作此应用。


1. RSS文档资料

在我们详细讨论此应用的细节前,我觉得有必要回顾一下RSS文档的总体架构。

为了理解下面的描述,你需要熟悉可扩展标记语言及相关概念。如果你对XML文档不熟悉的话,请参考XML教程。请注意在本文中,node代表XML文档中的一个标签。如上述提到的MQL4文章,RSS文件是带特定标签结构的XML文档。

每个RSS文档都有一个全局容器,即RSS标签。这是RSS文档的共同点。频道标签是RSS标签的直接继承者。它包含网站和源的描述信息。由此,RSS文档可根据其包含的特定标签的不同而不同,当时有一些标签是所有RSS文档必须具备的,用以标识其为RSS文件。

所需的标签有:

  • title - 频道的主题。要包含网站的名称;
  • link - 提供该频道的网站URL;
  • description - 网站简介;
  • item - 至少有一个内容标签。

以上标签必须是频道标签的子节点。项目节点包含同特定内容相关的数据。

每一个项目节点都包含如下标签:

  • title - 内容的主题;
  • link - 内容的URL链接;
  • description - 内容简介;
  • date - 网站发布的数据内容。

所有的RSS文档都包含所述标签并具备相同结果。

一个完整的RSS文档例子如下。


<rss version="2.0">
  <channel>
    <title>Xul.fr: Tutorials and Applications of the Web 2.0</title>
    <link>http://www.xul.fr/</link>
    <description>Ajax, JavaScript, XUL, RSS, PHP and all technologies of the Web 2.0. Building a CMS, tutorial and application.</description>
    <pubDate>Wed, 07 Feb 2007 14:20:24 GMT</pubDate>
    <item>
    <title>News on interfaces of the Web in 2010</title>
    <link>http://www.xul.fr/en/2010.php</link>
    <description>Steve Jobs explains why iPad does not support Adobe Flash:&lt;em&gt;At Adobe they are lazy. 
    They have the potential to make  interesting things, but they refuse to do so. 
    Apple does not support Flash because it is too buggy.
     Each time a Mac crashes, most often it is because of Flash. Nobody will use Flash. 
     The world is moving  to &lt;a href="http://www.xul.fr/en/html5/" target="_parent"&gt;HTML 5&lt;/a&gt;</description>
     <pubDate>Sat, 11 Dec 10 09:41:06 +0100</pubDate>
    </item>
    <item>
      <title>Textured Border in CSS</title>
      <link>http://www.xul.fr/en/css/textured-border.php</link>
      <description>   The border attribute of the style sheets can vary in color and width, but it was not expected to give it a texture. However, only a CSS rule is required to add this graphic effect...   The principle is to assign a texture to the whole &lt;em&gt;fieldset&lt;/em&gt; and insert into it another &lt;em&gt;fieldset&lt;/em&gt; (for rounded edges) or a &lt;em&gt;div&lt;/em&gt;, whose background is the same as that of the page</description>
      <pubDate>Wed, 29 Jul 09 15:56:54  0200</pubDate>
    </item>
    <item>
      <title>Create an RSS feed from SQL, example with Wordpress</title>
      <link>http://www.xul.fr/feed/rss-sql-wordpress.html</link>
      <description>Articles contain at least the following items: And possibly, author's name, or an image. This produces the following table: The returned value is true if the database is found, false otherwise. It remains to retrieve the data from the array</description>
      <pubDate>Wed, 29 Jul 09 15:56:50  0200</pubDate>
    </item>
    <item>
      <title>Firefox 3.5</title>
      <link>http://www.xul.fr/gecko/firefox35.php</link>
      <description>Les balises audio et vid&#xE9;o sont impl&#xE9;ment&#xE9;es. Le format de donn&#xE9;e JSON est reconnu nativement par Firefox. L'avantage est d'&#xE9;viter l'utilisation de la fonction eval() qui n'est pas s&#xFB;r, ou d'employer des librairies additionnelles, qui est nettement plus lent</description>
      <pubDate>Wed, 24 Jun 09 15:18:47  0200</pubDate>
    </item>
    <item>
      <title>Contestation about HTML 5</title>
      <link>http://www.xul.fr/en/html5/contestation.php</link>
      <description>  Nobody seemed to be worried so far, but the definition of HTML 5 that is intended to be the format of billions of Web pages in coming years, is conducted and decided by a single person! &lt;em&gt;Hey, wait! Pay no attention to the multi-billions dollar Internet corporation behind the curtain. It's me Ian Hickson! I am my own man</description>
      <pubDate>Wed, 24 Jun 09 15:18:29  0200</pubDate>
    </item>
    <item>
      <title>Form Objects in HTML 4</title>
      <link>http://www.xul.fr/javascript/form-objects.php</link>
      <description>   It is created by the HTML &lt;em&gt;form&lt;/em&gt; tag:   The name or id attribute can access by script to its content. It is best to use both attributes with the same identifier, for the sake of compatibility.   The &lt;em&gt;action&lt;/em&gt; attribute indicates the page to which send the form data. If this attribute is empty, the page that contains the form that will be charged the data as parameters</description>
      <pubDate>Wed, 24 Jun 09 15:17:49  0200</pubDate>
    </item>
    <item>
      <title>DOM Tutorial</title>
      <link>http://www.xul.fr/en/dom/</link>
      <description>  The Document Object Model describes the structure of an XML or HTML document, a web page and allows access to each individual element.</description>
      <pubDate>Wed, 06 May 2009 18:30:11 GMT</pubDate>
    </item>    
  </channel>
</rss>


2. 应用的总体架构

在这里我给出RSS阅读器要展示的信息描述以及该应用程序的用户图形界面。

应用程序首先要展示频道标题,它包含在标题标签中。此信息将用于作为网站源的参照。

这个应用也应能展示源所描述的所有内容的截图。这涉及文档中所有项的标签。对于每一个标签,内容的标题都将展现。最后,我想让RSS阅读器展现内容的描述,即包含在每个标签描述中的内容。


2.1. 用户界面

用户界面是应用程序用于展现信息的函数。

关于用户界面的想法下图能够最好的表达。


应用程序会话框架

图 1. 应用程序会话框架


下图展现了构成银狐界面的不用模块。

  • 首先是标题栏。频道标题在此展现;
  • 输入区域。用户在此输入RSS源的网址;
  • 标题区域。特定内容的标题在此展现;
  • 文本区域。内容的描述在此展现;
  • 列表区域。此滚动列表将显示源所包含的所有内容的标题;
  • 左侧的按钮重置和清空显示在标题,文本和列表区域的文本;
  • 更新当前源的按钮检索当前加载的源的最新更新内容。

RSS阅读器工作原理如下——当程序加载到图表后,空白的程序对话框将显示,用户在输入区域键入想要的RSS源网址,然后按回车。这将加载所有内容标题,例如,加载每个项目标签的标题到列表视图区。列表从1开始,1代表最新发布的内容。

每个列表项都是可以点击的,点击每个列表项,将突出显示并且相应的标题描述内容会显示在文本区域。与此同时,内容标题将更为清晰的显示在标题区域。如果不管什么原因在加载源时发生了错误,报错信息将会显示在文本区域。

重置按钮将能用于清除任何文本区域、列表展示区域及标题区域的内容。

更新当前源将检查源的任何最新更新。


2.2. 代码实现

RSS阅读器将被实现为一个EA,并且使用MQL5标准类库

代码将被包含在CRssReader类中,它是CAppDialog类的子类。CAppDialog类在Dialog.mqh文件中给出,提供标题栏功能,以及控制最大化、最小化和关闭等应用程序对话实现。这将是用户界面的基础,在此之上其他模块将被加入。增加的部分是:标题区域,文本区域,列表展示区域及按钮。每一个都是一个控件。按钮将实现为控件,如Button.mqh文件中所描述的。

ListViewArea.mqh文件中的列表展示控件将用于构建RSS阅读器的可视列表区域。

编辑控件显然用于构成输入区域,此控件在Edit.mqh文件中定义。

标题和文本区域的实现是一个挑战。问题在于两者都必须支持多行显示的文本。MQL5中的文本对象无法识别换行符。另一个问题是一行文本对象中只能显示一小部分字符。也就意味着如果创建一个长描述的文本对象,那么对象将显示截断的本文,只能显示特定数量的一些字符。通过尝试和报错信息我发现只能显示63个字符,包括空格和标点符号。

为了克服这些困难,我决定这两个地方都使用修正的列表可视化控件。针对标题区域,修正的列表控件将不能滚动,并且固定列表项数量(2)。每个列表项都不能点击或者可选,控件看上去不像列表。这2个列表项展现为两行文本。如果文本太长一行放不下,将会被分割成2行显示。标题区域的控件在TitleArea.mqh文件中定义。

对于文本区域,应用类似的方式。此时列表项的数量将是动态的,修改后的列表控件将是可垂直滚动的。此控件在TextArea.mqh文件中给出。

此处提到的类库都是用于处理用户界面的。在此还有一个程序用到的非常重要的类库需要介绍。就是用于解析XML文档的类库。


2.3. 简单的XML解析器

因为一个RSS文档是一个XML文件,由liquinaut开发的EasyXML - XML 解析器类库能够在代码库中找到,将被用于应用程序。

这个类库非常广泛,涵盖了几乎所有RSS阅读器所需的功能。我在原始类库上做了修改,加了些我认为必要的额外特性。

这些都是次要的补充。第一个额外附加的方法是loadXmlFromUrlWebReq()。这个方法是替代loadXmlFromUrl()方法,loadXmlFromUrl()依赖于WinInet库来处理网页请求,loadXmlFromUrlWebReq()则使用内嵌的WebRequest()函数来从因特网上做下载。

//+------------------------------------------------------------------+
//| 使用MQL5webrequest函数加载给定url的xml                              |
//+------------------------------------------------------------------+
bool CEasyXml::loadXmlFromUrlWebReq(string pUrl)
  {
//---
   string cookie=NULL,headers;
   char post[],result[];
   int res;
//---
   string _url=pUrl;
   string sStream;
   res=WebRequest("GET",_url,cookie,NULL,5000,post,0,result,headers);
//--- 检查报错
   if(res==-1)
     {
      Err=EASYXML_ERR_WEBREQUEST_URL;
      return(Error());
     }
//-- 下载文件成功
   sStream=CharArrayToString(result,0,-1,CP_UTF8);
//---设置缓存文件
   if(blSaveToCache)
     {
      bool bResult=writeStreamToCacheFile(sStream);
      if(!bResult) Error(-1,false);
     }
//---
   return(loadXmlFromString(sStream));
  }

第二个附加的方法是GetErrorMsg(),当错解析器报错时可以获取报错信息。

string            GetErrorMsg(void){   return(ErrMsg);}

最后一个附加方法是用于修正一个严重的缺陷,我在测试easyxml解析器时发现的。

我发现这个类库无法识别XML类型的表的声明。代码中弄错了样式表的一个属性声明。这导致程序陷入死循环,代码不断的查找不存在的属性值。

对skipProlog()方法进行下小修改就能更正。

//+------------------------------------------------------------------+
//| 跳过 xml 头                                                       |
//+------------------------------------------------------------------+
bool CEasyXml::skipProlog(string &pText,int &pPos)
  {
//--- 跳过 xml 声明
   if(StringCompare(EASYXML_PROLOG_OPEN,StringSubstr(pText,pPos,StringLen(EASYXML_PROLOG_OPEN)))==0)
     {
      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>0)
        {
         pPos=iClose+StringLen(EASYXML_PROLOG_CLOSE);
           } else {
         Err=EASYXML_INVALID_PROLOG;
         return(false);
        }
     }
//--- 跳过样表声明
   if(StringCompare(EASYXML_STYLESHEET_OPEN,StringSubstr(pText,pPos,StringLen(EASYXML_STYLESHEET_OPEN)))==0)
     {
      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>0)
        {
         pPos=iClose+StringLen(EASYXML_STYLESHEET_CLOSE);
           } else {
         Err=EASYXML_INVALID_PROLOG;
         return(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);
  }

除了上述问题,liquinaut的代码无需改动,easyxml.mqh是出色的工具。


2.4. EA代码

至此,所有应用程序所需的类库已经准备就绪,是时候将这些组件集成起来定义CRssReader类了。

请注意,RSS阅读器EA代码将从定义CRssReader类开始。

//+------------------------------------------------------------------+
//|                                                    RssReader.mq5 |
//|                                                          Ufranco |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Ufranco"
#property link      "https://www.mql5.com"
#property version   "1.00"
#include <Controls\Dialog.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Button.mqh>
#include <TitleArea.mqh>
#include <TextArea.mqh>
#include <ListViewArea.mqh>
#include <easyxml.mqh>
//+------------------------------------------------------------------+
//| 定义                                                              |
//+------------------------------------------------------------------+
//--- 缩进和间隔
#define INDENT_LEFT                         (11)      // 左边距(留出边界宽度)
#define INDENT_TOP                          (11)      // 顶边距(留出边界宽度)
#define INDENT_RIGHT                        (11)      // 右边距(留出边界宽度)
#define INDENT_BOTTOM                       (11)      // 底边距(留出边界宽度)
#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轴坐标尺寸

从包含必要的文件开始。该定义指令用于设置控件像素的物理参数。

INDENT_LEFT, INDENT_RIGHT, INDENT_TOP 和 INDENT_DOWN 设置控件和应用程序对话框间的间距。

  • CONTROLS_GAP_Y 是控件间的垂直距离;
  • EDIT_HEIGHT 设置构成输入区域的Edit控件的高度;
  • BUTTON_WIDTH 和 BUTTON_HEIGHT 定义所有按钮控件的高度;
  • TEXTAREA_HEIGHT 是文本区域的高度;
  • LIST_HEIGHT 设置列表控件的高度。

定义完我们开始定义CRssReader类。

//+------------------------------------------------------------------+
//| 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;                // 频道节点对象
   CEasyXmlNode     *ChannelChildNodes[];        // 频道子节点对象数组

如前所述CRssReader继承自CAppDialog类。

这个类有几个私有属性如下:

  • m_shift - 这个整型变量存储ChannelChildnodes数组中第一个项目节点的索引;
  • m_rssurl - 是一个字符串,它保存作为输入参数的最新URL的副本。
  • m_textareaoutput[] - 是一个字符串数组,每个元素对应一行字符数量一定的文本。
  • m_titleareaoutput[] - 也是一个字符串数组,用处同前一个字符串数组类似。
  • m_button1 和 m_button2 是CButton类型的对象;
  • m_listview是列表控件的对象;
  • m_edit 是CEdit对象,实例化输入区域;
  • m_titleview 是标题区域的对象;
  • m_textview - 文本区域对象;
  • m_xmldocument 是xml文档对象;
  • RssNode 是根节点对象;
  • ChannelNode 是频道节点对象;
  • ChannelChildNodes数组是一系列指针,指向Channel标记的直接继承者。

我们的类仅仅有两个公共方法。

public:
                     CRssReader(void);
                    ~CRssReader(void);
   //--- 创建
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   //--- 图表事件处理函数
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

第一个方法Create()设置应用程序对话框的大小和初始位置。

它也初始化所有RSS阅读程序的控件(记住我们的类从 CAppDialog类继承而来,因此父类的公共方法和它的子类能够被实例CRssReader调用)。

//+------------------------------------------------------------------+
//| 创建                                                              |
//+------------------------------------------------------------------+
bool CRssReader::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
//--- 创建独立控件
   if(!CreateEdit())
      return(false);
   if(!CreateButton1())
      return(false);
   if(!CreateButton2())
      return(false);
   if(!CreateTitleView())
      return(false);
   if(!CreateListView())
      return(false);
   if(!CreateTextView())
      return(false);
//--- 成功
   return(true);
  }

其次是OnEvent()方法,此函数为控件和处理函数分配相应的特定事件来实现交互。

//+------------------------------------------------------------------+
//| 事件处理                                                          |  
//+------------------------------------------------------------------+
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)


2.5. 控件初始化方法

CreateEdit(), CreateButton1() ,CreateButton2(), CreateTitleView(), CreateListView() 和 CreateTextView() 保护方法被 Create()调用,用以初始化相应的控件。

protected:
   // --- 创建控件
   bool              CreateEdit(void);
   bool              CreateButton1(void);
   bool              CreateButton2(void);
   bool              CreateTitleView(void);
   bool              CreateListView(void);
   bool              CreateTextView(void);

正是在这些函数中设置控件的大小,位置和属性(例如 字体,字体大小,颜色,边界颜色,边界类型)。

//+------------------------------------------------------------------+
//| 创建展示区域                                                       | 
//+------------------------------------------------------------------+
bool CRssReader::CreateEdit(void)
  {
//--- 坐标
   int x1=INDENT_LEFT;
   int y1=INDENT_TOP;
   int x2=ClientAreaWidth()-INDENT_RIGHT;
   int y2=y1+EDIT_HEIGHT;
//--- 创建
   if(!m_edit.Create(m_chart_id,m_name+"Edit",m_subwin,x1,y1,x2,y2))
      return(false);
   if(!m_edit.Text("Please enter the web address of an Rss feed"))
      return(false);
   if(!m_edit.ReadOnly(false))
      return(false);
   if(!Add(m_edit))
      return(false);
//--- 成功
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建按钮 1                                                        |
//+------------------------------------------------------------------+ 
bool CRssReader::CreateButton1(void)
  {
//--- 坐标
   int x1=INDENT_LEFT;
   int y1=INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y);
   int x2=x1+BUTTON_WIDTH;
   int y2=y1+BUTTON_HEIGHT;
//--- 创建
   if(!m_button1.Create(m_chart_id,m_name+"Button1",m_subwin,x1,y1,x2,y2))
      return(false);
   if(!m_button1.Text("Reset"))
      return(false);
   if(!m_button1.Font("Comic Sans MS"))
      return(false);
   if(!m_button1.FontSize(8))
      return(false);
   if(!m_button1.Color(clrWhite))
      return(false);
   if(!m_button1.ColorBackground(clrBlack))
      return(false);
   if(!m_button1.ColorBorder(clrBlack))
      return(false);
   if(!m_button1.Pressed(true))
      return(false);
   if(!Add(m_button1))
      return(false);
//--- 成功
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建按钮 2                                                        |
//+------------------------------------------------------------------+ 
bool CRssReader::CreateButton2(void)
  {
//--- 坐标
   int x1=(ClientAreaWidth()-INDENT_RIGHT)-BUTTON_WIDTH;
   int y1=INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y);
   int x2=ClientAreaWidth()-INDENT_RIGHT;
   int y2=y1+BUTTON_HEIGHT;
//--- 创建
   if(!m_button2.Create(m_chart_id,m_name+"Button2",m_subwin,x1,y1,x2,y2))
      return(false);
   if(!m_button2.Text("Update current feed"))
      return(false);
   if(!m_button2.Font("Comic Sans MS"))
      return(false);
   if(!m_button2.FontSize(8))
      return(false);
   if(!m_button2.Color(clrWhite))
      return(false);
   if(!m_button2.ColorBackground(clrBlack))
      return(false);
   if(!m_button2.ColorBorder(clrBlack))
      return(false);
   if(!m_button2.Pressed(true))
      return(false);
   if(!Add(m_button2))
      return(false);
//--- 成功
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建展示区域                                                       | 
//+------------------------------------------------------------------+
bool CRssReader::CreateTitleView(void)
  {
//--- 坐标
   int x1=INDENT_LEFT;
   int y1=INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y)+BUTTON_HEIGHT+CONTROLS_GAP_Y;
   int x2=ClientAreaWidth()-INDENT_RIGHT;
   int y2=y1+(EDIT_HEIGHT*2);
   m_titleview.Current();
//--- 创建 
   if(!m_titleview.Create(m_chart_id,m_name+"TitleView",m_subwin,x1,y1,x2,y2))
     {
      Print("error creating title view");
      return(false);
     }
   else
     {
      for(int i=0;i<2;i++)
        {
         m_titleview.AddItem(" ");
        }
     }
   if(!Add(m_titleview))
     {
      Print("error adding title view");
      return(false);
     }
//--- 成功
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建"ListView"组建                                                |
//+------------------------------------------------------------------+
bool CRssReader::CreateListView(void)
  {

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


2.6. RSS文档处理方法

// --- rss文档处理
   bool              LoadDocument(string filename);
   int               ItemNodesTotal(void);
   void              FreeDocumentTree(void);

2.6.1. LoadDocument()

此函数有不少重要的用处。主要的一个就是处理网站请求。loadXmlFromUrlWebReq()用于下载RSS文件。

如果成功,函数执行第二步操作初始化RssNode,ChannelNode指针并填充ChannelChildnodes数组。正是在这里设置m_rssurl和m_shift指针。所有这些完成后,函数返回true。

如果RSS文件无法下载,那么标题区域、列表区域以及文本区域中的文本将被清除,并且在标题栏中展示状态信息。随其后的是在文本区域输出的报错信息。函数返回false。

//+------------------------------------------------------------------+
//|   加载文档                                                        |
//+------------------------------------------------------------------+
bool CRssReader::LoadDocument(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");
      return(false);
     }
   else
     {
      m_rssurl=filename;
      RssNode=m_xmldocument.getDocumentRoot();
      ChannelNode=RssNode.FirstChild();
      if(CheckPointer(RssNode)==POINTER_INVALID || CheckPointer(ChannelNode)==POINTER_INVALID)
         return(false);
     }
   ArrayResize(ChannelChildNodes,ChannelNode.Children().Total());
   for(int i=0;i<ChannelNode.Children().Total();i++)
     {
      ChannelChildNodes[i]=ChannelNode.Children().At(i);
     }
   m_shift=ChannelNode.Children().Total()-ItemNodesTotal();
   return(true);
  }


2.6.2. ItemNodesTotal()

此函数用于LoadDocument()方法中。它返回继承于频道标签的项目节点数量的整型值。

如果没有项目节点,将为无效的RSS文档并且函数返回0.

//+------------------------------------------------------------------+
//|  统计文档中标签项数量的函数                                          |     
//+------------------------------------------------------------------+
int CRssReader::ItemNodesTotal(void)
  {
   int t=0;
   for(int i=0;i<ChannelNode.Children().Total();i++)
     {
      if(ChannelChildNodes[i].getName()=="item")
        {
         t++;
        }
      else continue;
     }
   return(t);
  }

2.6.3. FreeDocumentTree()

此函数重置所有CEasyXmlNode指针。

首先通过调用CArrayObj类的Shutdown()方法删除ChannelChildnodes数组的元素。然后调用一次ArrayFree(),该数组被释放。

接着频道节点的指针被删除,easyxml解析器的文档树被清除。这些操作导致RssNode和ChannelNode指针变成空指针,这也是他们被分配NULL值的原因。

//+------------------------------------------------------------------+
//|  释放文档树并重置指针值                                             |
//+------------------------------------------------------------------+ 
void CRssReader::FreeDocumentTree(void)
  {
   ChannelNode.Children().Shutdown();
   ArrayFree(ChannelChildNodes);
   RssNode.Children().Shutdown();
   m_xmldocument.Clear();
   m_shift=0;
   RssNode=NULL;
   ChannelNode=NULL;
  }


2.7. 从文档树中提取信息的方法

这些函数用于从RSS文档中获取文本。

//--- 获取函数
   string            getChannelTitle(void);
   string            getTitle(CEasyXmlNode *Node);
   string            getDescription(CEasyXmlNode *Node);
   string            getDate(CEasyXmlNode *Node);

2.7.1. getChannelTitle()

该函数获取RSS文档的当前频道的标题。

它从检查频道节点的指针有效性开始。如果指针有效,它循环搜索所有频道节点的直接继承者,查找主题标签。

for循环使用m_shift属性来限制循环搜索的频道节点数量。如果函数不成功返回NULL。

//+------------------------------------------------------------------+
//| 获取频道标题                                                       |
//+------------------------------------------------------------------+
string CRssReader::getChannelTitle(void)
  {
   string ret=NULL;
   if(!CheckPointer(ChannelNode)==POINTER_INVALID)
     {
      for(int i=0;i<m_shift;i++)
        {
         if(ChannelChildNodes[i].getName()=="title")
           {
            ret=ChannelChildNodes[i].getValue();
            break;
           }
         else continue;
        }
     }
//---return value
   return(ret);
  }


2.7.2. getTitle()

该函数将输入指针指向一个项目标签,并遍历该标签的继承对象,查找主题标签并返回其值。

getDescription() 和 getDate() 函数有着相同的格式并且运行方式类似。成功调用该函数返回一个字符串,否则返回NULL。

//+------------------------------------------------------------------+
//| 显示标题                                                          |
//+------------------------------------------------------------------+
string CRssReader::getTitle(CEasyXmlNode *Node)
  {
   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);
  }
//+------------------------------------------------------------------+
//| 显示描述                                                          |
//+------------------------------------------------------------------+
string CRssReader::getDescription(CEasyXmlNode *Node)
  {
   int k=Node.Children().Total();
   string n=NULL;
   for(int i=0;i<k;i++)
     {
      CEasyXmlNode*subNode=Node.Children().At(i);
      if(subNode.getName()=="description")
        {
         n=subNode.getValue();
         break;
        }
      else continue;
     }
   return(n);
  }
//+------------------------------------------------------------------+
//| 显示日期                                                          |
//+------------------------------------------------------------------+ 
string CRssReader::getDate(CEasyXmlNode *Node)
  {
   int k=Node.Children().Total();
   string n=NULL;
   for(int i=0;i<k;i++)
     {
      CEasyXmlNode*subNode=Node.Children().At(i);
      if(subNode.getName()=="pubDate")
        {
         n=subNode.getValue();
         break;
        }
      else continue;
     }
   return(n);
  }


2.8. 格式化文本的方法

这些函数用于为输出的文本对象准备文本,以便克服一些文本对象所面临的限制。

 //--- 文本格式 
   bool              FormatString(string v,string &array[],int n);
   string            removeTags(string _string);
   string            removeSpecialCharacters(string s_tring);
   int               tagPosition(string _string,int w);

2.8.1. FormatString()

这个是从RSS文档中提取文档输出给应用程序的主要函数。

它的主要作用是以字符串作为输入参数,将文本分割为"n"个字符一行。"n" 是每一行文本的字符个数。在文本中的每"n"个字符后,代码查找合适的位置来插入新的一行。接着真个字符串都处理完毕,新的一行源字符被插入到初始文本位置。

StringSplit()函数用于创建字符串数组,每个元素不超过"n"个字符。此函数返回一个布尔型值以及一个字符串作为输出。

//+------------------------------------------------------------------+
//|  文本区域面板输出字符格式化                                          |
//+------------------------------------------------------------------+
bool CRssReader::FormatString(string v,string &array[],int n)
  {
   ushort ch[],space,fullstop,comma,semicolon,newlinefeed;
   string _s,_k;
   space=StringGetCharacter(" ",0);
   fullstop=StringGetCharacter(".",0);
   comma=StringGetCharacter(",",0);
   semicolon=StringGetCharacter(";",0);
   newlinefeed=StringGetCharacter("\n",0);
   _k=removeTags(v);
   _s=removeSpecialCharacters(_k);
   int p=StringLen(_s);
   ArrayResize(ch,p+1);
   int d=StringToShortArray(_s,ch,0,-1);
   for(int i=1;i<d;i++)
     {
      int t=i%n;
      if(!t== 0)continue;
      else 
        {
         if(ch[(i/n)*n]==fullstop || ch[(i/n)*n]==semicolon || ch[(i/n)*n]==comma)
           {
            ArrayFill(ch,((i/n)*n)+1,1,newlinefeed);
           }
         else
           {
            for(int k=i;k>=0;k--)
              {
               if(ch[k]==space)
                 {
                  ArrayFill(ch,k,1,newlinefeed);
                  break;
                 }
               else continue;
              }
           }
        }
     }
   _s=ShortArrayToString(ch,0,-1);
   int s=StringSplit(_s,newlinefeed,array);
   if(!s>0)
     {return(false);}
// 成功 
   return(true);
  }


2.8.2. removeTags()

当我注意到许多RSS文档在XML节点中包含HTML标记时,此函数变为必要的了。

一些RSS文档以这种方式发布,许多RSS聚合应用在浏览器中运行。

此函数以字符串作为参数,在文本中查找标记。如果在文本中找到任何标记,将标记字符的开始和结束位置保存在2维数组a[][]中。这个数组用于提取标记间的文本,返回被提取的字符串。如果没有找到标记,输入字符串被返回。

//+------------------------------------------------------------------+
//| 移除标签                                                          |
//+------------------------------------------------------------------+
string CRssReader::removeTags(string _string)
  {
   string now=NULL;
   if(StringFind(_string,"<",0)>-1)
     {
      int v=0,a[][2];
      ArrayResize(a,2024);
      for(int i=0;i<StringLen(_string);i++)
        {
         int t=tagPosition(_string,i);
         if(t>0)
           {
            v++;
            a[v-1][0]=i;
            a[v-1][1]=t;
           }
         else continue;
        }
      ArrayResize(a,v);
      for(int i=0;i<v-1;i++)
        {
         now+=StringSubstr(_string,(a[i][1]+1),(a[i+1][0]-(a[i][1]+1)));
        }
     }
   else
     {
      now=_string;
     }
   return(now);
  }

此种文档的部分样例如下。

<item>            
    <title>GIGABYTE X99-Gaming G1 WIFI Motherboard Review</title>
    <author>Ian Cutress</author>
    <description><![CDATA[ <p>The gaming motherboard range from a manufacturer is one with a lot of focus in terms of design and function due to the increase in gaming related PC sales. On the Haswell-E side of gaming, GIGABYTE is putting forward the X99-Gaming G1 WIFI at the top of its stack, and this is what we are reviewing today.&nbsp;</p>
<p align="center"><a href='http://dynamic1.anandtech.com/www/delivery/ck.php?n=a1f2f01f&amp;cb=582254849' target='_blank'><img src='http://dynamic1.anandtech.com/www/delivery/avw.php?zoneid=24&amp;cb=582254849&amp;n=a1f2f01f' border='0' alt='' /></a><img src="http://toptenreviews.122.2o7.net/b/ss/tmn-test/1/H.27.3--NS/0" height="1" width="1" border="0" alt="" /></p>]]></description>
    <link>http://www.anandtech.com/show/8788/gigabyte-x99-gaming-g1-wifi-motherboard-review</link>
        <pubDate>Thu, 18 Dec 2014 10:00:00 EDT</pubDate>
        <guid isPermaLink="false">tag:www.anandtech.com,8788:news</guid>
        <category><![CDATA[ Motherboards]]></category>                               
</item>  


2.8.3. removeSpecialCharacters()

此函数将特定的字符串替换为正确的字符。

例如,xml中的ampersand字符串可以表示成"&amp"。当出现这类情况时,此函数使用内置StringReplace()函数来替换。

//+------------------------------------------------------------------+
//| 移除特殊的字符                                                     |
//+------------------------------------------------------------------+ 
string CRssReader::removeSpecialCharacters(string s_tring)
  {
   string n=s_tring;
   StringReplace(n,"&amp;","&");
   StringReplace(n,"&#39;","'");
   StringReplace(n,"&nbsp;"," ");
   StringReplace(n,"&ldquo;","\'");
   StringReplace(n,"&rdquo;","\'");
   StringReplace(n,"&quot;","\"");
   StringReplace(n,"&ndash;","-");
   StringReplace(n,"&rsquo;","'");
   StringReplace(n,"&gt;","");
   return(n);
  }


2.8.4. tagPosition()

此函数是一个辅助函数,在函数removeTags()中被调用。它的输入为一个字符串和一个整型值。

输入的整型值代表字符串中字符的位置,函数从这个位置开始搜索标签的起始字符。例如,"<"。如果起始标记找到,然后函数开始查找结束标记并且u哦为输出返回对饮的结束标记字符 ">"的位置。如果没有找到标记则返回-1.

//+------------------------------------------------------------------+
//| 标签位置                                                          |
//+------------------------------------------------------------------+
int CRssReader::tagPosition(string _string,int w)
  {
   int iClose=-1;
   if(StringCompare("<",StringSubstr(_string,w,StringLen("<")))==0)
     {
      iClose=StringFind(_string,">",w+StringLen("<"));
     }

   return(iClose);
  }


2.9. 处理独立控件事件的方法

这些函数处理特定控件捕获的事件。

//--- 控件事件处理函数
   void              OnChangeListView(void);
   void              OnObjectEdit(void);
   void              OnClickButton1(void);
   void              OnClickButton2(void);
  };

2.9.1. OnChangeListView()

这是一个事件处理函数,当应用程序可视化列表区域的一个列表项被点击时被调用。

此函数用于RSS文档中某些内容摘要的可视化。

此函数清除文本及标题区域的任何文字,从文档树中获取新的数据并输出。所有这些仅在ChannelChildnodes数组不为空时发生。

//+------------------------------------------------------------------+
//| 事件处理                                                          |
//+------------------------------------------------------------------+
void CRssReader::OnChangeListView(void)
  {
   int a=0,k=0,l=0;
   a=m_listview.Current()+m_shift;
   if(ArraySize(ChannelChildNodes)>a)
     {
      if(m_titleview.ItemsClear())
        {
         if(!FormatString(getTitle(ChannelChildNodes[a]),m_titleareaoutput,55))
           {
            return;
           }
         else
         if(ArraySize(m_titleareaoutput)>0)
           {
            for(l=0;l<ArraySize(m_titleareaoutput);l++)
              {
               m_titleview.AddItem(removeSpecialCharacters(m_titleareaoutput[l]));
              }
           }
        }
      if(m_textview.ItemsClear())
        {
         if(!FormatString(getDescription(ChannelChildNodes[a]),m_textareaoutput,35))
            return;
         else
         if(ArraySize(m_textareaoutput)>0)
           {
            for(k=0;k<ArraySize(m_textareaoutput);k++)
              {
               m_textview.AddItem(m_textareaoutput[k]);
              }
            m_textview.AddItem(" ");
            m_textview.AddItem("Date|"+getDate(ChannelChildNodes[a]));
           }
         else return;
        }
     }
  }


2.9.2. OnObjectEdit()

当用户在输入区域输入文本结束后,此处理函数被调用。

函数调用LoadDocument()方法。如果下载成功,文本将从整个程序中清除。接下来,标题改变并且新的内容输出到列表区域。

//+------------------------------------------------------------------+
//| 事件处理                                                          |
//+------------------------------------------------------------------+
void CRssReader::OnObjectEdit(void)
  {
   string f=m_edit.Text();
   if(StringLen(f)>0)
     {
      if(ArraySize(ChannelChildNodes)<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(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;
        }
      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(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;
        }
     }
   else return;
  }


2.9.3. OnClickButton1/2()

当用户点击重置或源更新按钮时,这些处理函数被调用。

当EA首次加载时,点击重置按钮更新应用程序对话框。

点击“check for feed update”按钮触发回调LoadDocument()方法,RSS源数据将被下载,更新可视列表区域。

//+------------------------------------------------------------------+
//| 事件处理  更新程序对话框                                            |
//+------------------------------------------------------------------+   
void CRssReader::OnClickButton1(void)
  {
   if(ArraySize(ChannelChildNodes)<1)
     {
      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(int i=0;i<20;i++)
           {
            if(!m_listview.AddItem(" "))
               Print("error adding to listview");
           }
         m_listview.VScrolled(true);
         for(int i=0;i<1;i++)
           {
            m_textview.AddItem(" ");
           }
         m_textview.VScrolled(true);
         for(int i=0;i<2;i++)
           {
            m_titleview.AddItem(" ");
           }
         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(int i=0;i<20;i++)
           {
            if(!m_listview.AddItem(" "))
               Print("error adding to listview");
           }
         m_listview.VScrolled(true);
         for(int i=0;i<1;i++)
           {
            m_textview.AddItem(" ");
           }
         m_textview.VScrolled(true);
         for(int i=0;i<2;i++)
           {
            m_titleview.AddItem(" ");
           }
         return;
        }
     }
  }
//+------------------------------------------------------------------+
//| 事件处理  更新当前源                                               |
//+------------------------------------------------------------------+ 
void CRssReader::OnClickButton2(void)
  {
   string f=m_rssurl;
   if(ArraySize(ChannelChildNodes)<1)
      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(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;
     }
  }

至此完成了CRssReader类的定义。


2.10. CRssReader类的实现

//+------------------------------------------------------------------+
//| 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;               // 频道节点对象
   CEasyXmlNode     *ChannelChildNodes[];       // 频道子节点对象数组

public:
                     CRssReader(void);
                    ~CRssReader(void);
   //--- 创建
   virtual bool      Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2);
   //--- 图表事件处理函数
   virtual bool      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);

protected:
   // --- 创建控件
   bool              CreateEdit(void);
   bool              CreateButton1(void);
   bool              CreateButton2(void);
   bool              CreateTitleView(void);
   bool              CreateListView(void);
   bool              CreateTextView(void);
   // --- rss文档处理
   bool              LoadDocument(string filename);
   int               ItemNodesTotal(void);
   void              FreeDocumentTree(void);
   //--- 获取函数
   string            getChannelTitle(void);
   string            getTitle(CEasyXmlNode *Node);
   string            getDescription(CEasyXmlNode *Node);
   string            getDate(CEasyXmlNode *Node);
   //--- 文本格式 
   bool              FormatString(string v,string &array[],int n);
   string            removeTags(string _string);
   string            removeSpecialCharacters(string s_tring);
   int               tagPosition(string _string,int w);
   //--- 控件事件处理函数
   void              OnChangeListView(void);
   void              OnObjectEdit(void);
   void              OnClickButton1(void);
   void              OnClickButton2(void);
  };
//+------------------------------------------------------------------+
//| 事件处理                                                          |  
//+------------------------------------------------------------------+
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)
//+------------------------------------------------------------------+
//| 构造函数                                                          |
//+------------------------------------------------------------------+
CRssReader::CRssReader(void)
  {

  }
//+------------------------------------------------------------------+
//| 析构函数                                                          |
//+------------------------------------------------------------------+
CRssReader::~CRssReader(void)
  {
  }
//+------------------------------------------------------------------+
//| 创建                                                              |
//+------------------------------------------------------------------+
bool CRssReader::Create(const long chart,const string name,const int subwin,const int x1,const int y1,const int x2,const int y2)
  {
   if(!CAppDialog::Create(chart,name,subwin,x1,y1,x2,y2))
      return(false);
//--- 创建独立控件
   if(!CreateEdit())
      return(false);
   if(!CreateButton1())
      return(false);
   if(!CreateButton2())
      return(false);
   if(!CreateTitleView())
      return(false);
   if(!CreateListView())
      return(false);
   if(!CreateTextView())
      return(false);
//--- 成功
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建展示区域                                                       | 
//+------------------------------------------------------------------+
bool CRssReader::CreateEdit(void)
  {
//--- 坐标
   int x1=INDENT_LEFT;
   int y1=INDENT_TOP;
   int x2=ClientAreaWidth()-INDENT_RIGHT;
   int y2=y1+EDIT_HEIGHT;
//--- 创建
   if(!m_edit.Create(m_chart_id,m_name+"Edit",m_subwin,x1,y1,x2,y2))
      return(false);
   if(!m_edit.Text("Please enter the web address of an Rss feed"))
      return(false);
   if(!m_edit.ReadOnly(false))
      return(false);
   if(!Add(m_edit))
      return(false);
//--- 成功
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建按钮 1                                                        |
//+------------------------------------------------------------------+ 
bool CRssReader::CreateButton1(void)
  {
//--- 坐标
   int x1=INDENT_LEFT;
   int y1=INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y);
   int x2=x1+BUTTON_WIDTH;
   int y2=y1+BUTTON_HEIGHT;
//--- 创建
   if(!m_button1.Create(m_chart_id,m_name+"Button1",m_subwin,x1,y1,x2,y2))
      return(false);
   if(!m_button1.Text("Reset"))
      return(false);
   if(!m_button1.Font("Comic Sans MS"))
      return(false);
   if(!m_button1.FontSize(8))
      return(false);
   if(!m_button1.Color(clrWhite))
      return(false);
   if(!m_button1.ColorBackground(clrBlack))
      return(false);
   if(!m_button1.ColorBorder(clrBlack))
      return(false);
   if(!m_button1.Pressed(true))
      return(false);
   if(!Add(m_button1))
      return(false);
//--- 成功
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建按钮 2                                                        |
//+------------------------------------------------------------------+ 
bool CRssReader::CreateButton2(void)
  {
//--- 坐标
   int x1=(ClientAreaWidth()-INDENT_RIGHT)-BUTTON_WIDTH;
   int y1=INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y);
   int x2=ClientAreaWidth()-INDENT_RIGHT;
   int y2=y1+BUTTON_HEIGHT;
//--- 创建
   if(!m_button2.Create(m_chart_id,m_name+"Button2",m_subwin,x1,y1,x2,y2))
      return(false);
   if(!m_button2.Text("Update current feed"))
      return(false);
   if(!m_button2.Font("Comic Sans MS"))
      return(false);
   if(!m_button2.FontSize(8))
      return(false);
   if(!m_button2.Color(clrWhite))
      return(false);
   if(!m_button2.ColorBackground(clrBlack))
      return(false);
   if(!m_button2.ColorBorder(clrBlack))
      return(false);
   if(!m_button2.Pressed(true))
      return(false);
   if(!Add(m_button2))
      return(false);
//--- 成功
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建展示区域                                                       | 
//+------------------------------------------------------------------+
bool CRssReader::CreateTitleView(void)
  {
//--- 坐标
   int x1=INDENT_LEFT;
   int y1=INDENT_TOP+(EDIT_HEIGHT+CONTROLS_GAP_Y)+BUTTON_HEIGHT+CONTROLS_GAP_Y;
   int x2=ClientAreaWidth()-INDENT_RIGHT;
   int y2=y1+(EDIT_HEIGHT*2);
   m_titleview.Current();
//--- 创建 
   if(!m_titleview.Create(m_chart_id,m_name+"TitleView",m_subwin,x1,y1,x2,y2))
     {
      Print("error creating title view");
      return(false);
     }
   else
     {
      for(int i=0;i<2;i++)
        {
         m_titleview.AddItem(" ");
        }
     }
   if(!Add(m_titleview))
     {
      Print("error adding title view");
      return(false);
     }
//--- 成功
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建"ListView"组建                                                |
//+------------------------------------------------------------------+
bool CRssReader::CreateListView(void)
  {

//--- 坐标
   int x1=INDENT_LEFT;
   int y1=INDENT_TOP+((EDIT_HEIGHT+CONTROLS_GAP_Y)*2)+20+TEXTAREA_HEIGHT+CONTROLS_GAP_Y+BUTTON_HEIGHT+CONTROLS_GAP_Y;
   int x2=ClientAreaWidth()-INDENT_RIGHT;
   int y2=y1+LIST_HEIGHT;
//--- 创建
   if(!m_listview.Create(m_chart_id,m_name+"ListView",m_subwin,x1,y1,x2,y2))
      return(false);
   if(!Add(m_listview))
      return(false);
//--- 用字符填充
   for(int i=0;i<20;i++)
      if(!m_listview.AddItem(" "))
         return(false);
//--- 成功
   return(true);
  }
//+------------------------------------------------------------------+
//| 创建展示区域                                                       | 
//+------------------------------------------------------------------+
bool CRssReader::CreateTextView(void)
  {
//--- 坐标
   int x1=INDENT_LEFT;
   int y1=INDENT_TOP+((EDIT_HEIGHT+CONTROLS_GAP_Y)*2)+20+BUTTON_HEIGHT+CONTROLS_GAP_Y;
   int x2=ClientAreaWidth()-INDENT_RIGHT;
   int y2=y1+TEXTAREA_HEIGHT;
   m_textview.Current();
//--- 创建 
   if(!m_textview.Create(m_chart_id,m_name+"TextArea",m_subwin,x1,y1,x2,y2))
     {
      Print("error creating text area view");
      return(false);
     }
   else
     {
      for(int i=0;i<1;i++)
        {
         m_textview.AddItem(" ");
        }
      m_textview.VScrolled(true);
      ChartRedraw();
     }
   if(!Add(m_textview))
     {
      Print("error adding text area view");
      return(false);
     }
//----成功      
   return(true);
  }
//+------------------------------------------------------------------+
//|   加载文档                                                        |
//+------------------------------------------------------------------+
bool CRssReader::LoadDocument(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");
      return(false);
     }
   else
     {
      m_rssurl=filename;
      RssNode=m_xmldocument.getDocumentRoot();
      ChannelNode=RssNode.FirstChild();
      if(CheckPointer(RssNode)==POINTER_INVALID || CheckPointer(ChannelNode)==POINTER_INVALID)
         return(false);
     }
   ArrayResize(ChannelChildNodes,ChannelNode.Children().Total());
   for(int i=0;i<ChannelNode.Children().Total();i++)
     {
      ChannelChildNodes[i]=ChannelNode.Children().At(i);
     }
   m_shift=ChannelNode.Children().Total()-ItemNodesTotal();
   return(true);
  }
//+------------------------------------------------------------------+
//|  统计文档中标签项数量的函数                                          |     
//+------------------------------------------------------------------+
int CRssReader::ItemNodesTotal(void)
  {
   int t=0;
   for(int i=0;i<ChannelNode.Children().Total();i++)
     {
      if(ChannelChildNodes[i].getName()=="item")
        {
         t++;
        }
      else continue;
     }
   return(t);
  }
//+------------------------------------------------------------------+
//|  释放文档树并重置指针值                                             |
//+------------------------------------------------------------------+ 
void CRssReader::FreeDocumentTree(void)
  {
   ChannelNode.Children().Shutdown();
   ArrayFree(ChannelChildNodes);
   RssNode.Children().Shutdown();
   m_xmldocument.Clear();
   m_shift=0;
   RssNode=NULL;
   ChannelNode=NULL;
  }
//+------------------------------------------------------------------+
//| 获取频道标题                                                       |
//+------------------------------------------------------------------+
string CRssReader::getChannelTitle(void)
  {
   string ret=NULL;
   if(!CheckPointer(ChannelNode)==POINTER_INVALID)
     {
      for(int i=0;i<m_shift;i++)
        {
         if(ChannelChildNodes[i].getName()=="title")
           {
            ret=ChannelChildNodes[i].getValue();
            break;
           }
         else continue;
        }
     }
//---return value
   return(ret);
  }
//+------------------------------------------------------------------+
//| 显示标题                                                          |
//+------------------------------------------------------------------+
string CRssReader::getTitle(CEasyXmlNode *Node)
  {
   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);
  }
//+------------------------------------------------------------------+
//| 显示描述                                                          |
//+------------------------------------------------------------------+
string CRssReader::getDescription(CEasyXmlNode *Node)
  {
   int k=Node.Children().Total();
   string n=NULL;
   for(int i=0;i<k;i++)
     {
      CEasyXmlNode*subNode=Node.Children().At(i);
      if(subNode.getName()=="description")
        {
         n=subNode.getValue();
         break;
        }
      else continue;
     }
   return(n);
  }
//+------------------------------------------------------------------+
//| 显示日期                                                          |
//+------------------------------------------------------------------+ 
string CRssReader::getDate(CEasyXmlNode *Node)
  {
   int k=Node.Children().Total();
   string n=NULL;
   for(int i=0;i<k;i++)
     {
      CEasyXmlNode*subNode=Node.Children().At(i);
      if(subNode.getName()=="pubDate")
        {
         n=subNode.getValue();
         break;
        }
      else continue;
     }
   return(n);
  }
//+------------------------------------------------------------------+
//|  文本区域面板输出字符格式化                                          |
//+------------------------------------------------------------------+
bool CRssReader::FormatString(string v,string &array[],int n)
  {
   ushort ch[],space,fullstop,comma,semicolon,newlinefeed;
   string _s,_k;
   space=StringGetCharacter(" ",0);
   fullstop=StringGetCharacter(".",0);
   comma=StringGetCharacter(",",0);
   semicolon=StringGetCharacter(";",0);
   newlinefeed=StringGetCharacter("\n",0);
   _k=removeTags(v);
   _s=removeSpecialCharacters(_k);
   int p=StringLen(_s);
   ArrayResize(ch,p+1);
   int d=StringToShortArray(_s,ch,0,-1);
   for(int i=1;i<d;i++)
     {
      int t=i%n;
      if(!t== 0)continue;
      else 
        {
         if(ch[(i/n)*n]==fullstop || ch[(i/n)*n]==semicolon || ch[(i/n)*n]==comma)
           {
            ArrayFill(ch,((i/n)*n)+1,1,newlinefeed);
           }
         else
           {
            for(int k=i;k>=0;k--)
              {
               if(ch[k]==space)
                 {
                  ArrayFill(ch,k,1,newlinefeed);
                  break;
                 }
               else continue;
              }
           }
        }
     }
   _s=ShortArrayToString(ch,0,-1);
   int s=StringSplit(_s,newlinefeed,array);
   if(!s>0)
     {return(false);}
// 成功 
   return(true);
  }
//+------------------------------------------------------------------+
//| 移除特殊的字符                                                     |
//+------------------------------------------------------------------+ 
string CRssReader::removeSpecialCharacters(string s_tring)
  {
   string n=s_tring;
   StringReplace(n,"&amp;","&");
   StringReplace(n,"&#39;","'");
   StringReplace(n,"&nbsp;"," ");
   StringReplace(n,"&ldquo;","\'");
   StringReplace(n,"&rdquo;","\'");
   StringReplace(n,"&quot;","\"");
   StringReplace(n,"&ndash;","-");
   StringReplace(n,"&rsquo;","'");
   StringReplace(n,"&gt;","");
   return(n);
  }
//+------------------------------------------------------------------+
//| 移除标签                                                          |
//+------------------------------------------------------------------+
string CRssReader::removeTags(string _string)
  {
   string now=NULL;
   if(StringFind(_string,"<",0)>-1)
     {
      int v=0,a[][2];
      ArrayResize(a,2024);
      for(int i=0;i<StringLen(_string);i++)
        {
         int t=tagPosition(_string,i);
         if(t>0)
           {
            v++;
            a[v-1][0]=i;
            a[v-1][1]=t;
           }
         else continue;
        }
      ArrayResize(a,v);
      for(int i=0;i<v-1;i++)
        {
         now+=StringSubstr(_string,(a[i][1]+1),(a[i+1][0]-(a[i][1]+1)));
        }
     }
   else
     {
      now=_string;
     }
   return(now);
  }
//+------------------------------------------------------------------+
//| 标签位置                                                          |
//+------------------------------------------------------------------+
int CRssReader::tagPosition(string _string,int w)
  {
   int iClose=-1;
   if(StringCompare("<",StringSubstr(_string,w,StringLen("<")))==0)
     {
      iClose=StringFind(_string,">",w+StringLen("<"));
     }

   return(iClose);
  }
//+------------------------------------------------------------------+
//| 事件处理                                                           |
//+------------------------------------------------------------------+
void CRssReader::OnChangeListView(void)
  {
   int a=0,k=0,l=0;
   a=m_listview.Current()+m_shift;
   if(ArraySize(ChannelChildNodes)>a)
     {
      if(m_titleview.ItemsClear())
        {
         if(!FormatString(getTitle(ChannelChildNodes[a]),m_titleareaoutput,55))
           {
            return;
           }
         else
         if(ArraySize(m_titleareaoutput)>0)
           {
            for(l=0;l<ArraySize(m_titleareaoutput);l++)
              {
               m_titleview.AddItem(removeSpecialCharacters(m_titleareaoutput[l]));
              }
           }
        }
      if(m_textview.ItemsClear())
        {
         if(!FormatString(getDescription(ChannelChildNodes[a]),m_textareaoutput,35))
            return;
         else
         if(ArraySize(m_textareaoutput)>0)
           {
            for(k=0;k<ArraySize(m_textareaoutput);k++)
              {
               m_textview.AddItem(m_textareaoutput[k]);
              }
            m_textview.AddItem(" ");
            m_textview.AddItem("Date|"+getDate(ChannelChildNodes[a]));
           }
         else return;
        }
     }
  }
//+------------------------------------------------------------------+
//| 事件处理                                                           |
//+------------------------------------------------------------------+
void CRssReader::OnObjectEdit(void)
  {
   string f=m_edit.Text();
   if(StringLen(f)>0)
     {
      if(ArraySize(ChannelChildNodes)<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(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;
        }
      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(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;
        }
     }
   else return;
  }
//+------------------------------------------------------------------+
//| 事件处理  更新程序对话框                                            |
//+------------------------------------------------------------------+   
void CRssReader::OnClickButton1(void)
  {
   if(ArraySize(ChannelChildNodes)<1)
     {
      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(int i=0;i<20;i++)
           {
            if(!m_listview.AddItem(" "))
               Print("error adding to listview");
           }
         m_listview.VScrolled(true);
         for(int i=0;i<1;i++)
           {
            m_textview.AddItem(" ");
           }
         m_textview.VScrolled(true);
         for(int i=0;i<2;i++)
           {
            m_titleview.AddItem(" ");
           }
         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(int i=0;i<20;i++)
           {
            if(!m_listview.AddItem(" "))
               Print("error adding to listview");
           }
         m_listview.VScrolled(true);
         for(int i=0;i<1;i++)
           {
            m_textview.AddItem(" ");
           }
         m_textview.VScrolled(true);
         for(int i=0;i<2;i++)
           {
            m_titleview.AddItem(" ");
           }
         return;
        }
     }
  }
//+------------------------------------------------------------------+
//| 事件处理  更新当前源                                               |
//+------------------------------------------------------------------+ 
void CRssReader::OnClickButton2(void)
  {
   string f=m_rssurl;
   if(ArraySize(ChannelChildNodes)<1)
      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(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代码中了。


2.11. EA代码

EA没有输入参数,因为程序设计为完全互动的。

首先我们声明一个全局变量,它是CRssReader类的实例。在OnInit()函数中,我们调用Create()方法来初始化应用程序对话框。如果成功,一个父类的方法Run()将被调用。

OnDeinit()函数中,父类的Destroy()方法被调用来删除整个应用并且将EA从图表上移除。

OnChartEvent()函数包含对CRssReader类的父类方法的调用,它将处理所有事件。

//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函数                                                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   ExtDialog.ChartEvent(id,lparam,dparam,sparam);
  }

编译代码后程序就可以使用了。

当RssReader.mq5 EA加载到图表上,一个空白的对话框出现:

图 2. RssReader EA程序空白对话框截图

图 2. RssReader EA程序空白对话框截图

输入网址,RSS内容将在应用程序对话框中加载,如下图所示:

图 3. RssReader EA 在终端中运行

图 3. RssReader EA 在终端中运行

我使用大量的RSS源测试此程序。我观察到的仅有问题是,有时会显示出我不想要的字符,绝大多数时候RSS文档包含的字符都能在HTML文档中找到。

我还发现当程序运行时改变图表周期导致EA重新初始化,会使得程序的空间不能正确的绘制。

我没能过改正这个问题,因此我建议在RSS阅读器程序运行时不要改变图表的时间框架。


总结

至此,我们使用面向对象的编程技术,完成了创建完整的MetaTrader 5互动RSS阅读器的程序。

我相信会有更多的特性加入到程序中,并且用户界面也有更多的组织方式。我希望那些更好的应用程序GUI设计技巧的加入来进一步优化此应用程序。

P.S. 请注意这里提供下载的asyxml.mqh文件和Code Base中的不是同一个,它包含了文本已经提及的改动。所有需要的包含文件都在RssReader.zip文件中。


本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/1589

附加的文件 |
easyxml.mqh (25.66 KB)
easyxmlnode.mqh (7.67 KB)
ListViewArea.mqh (19.89 KB)
TextArea.mqh (13.47 KB)
TitleArea.mqh (13.56 KB)
RssReader.mq5 (27.83 KB)
RssReader.zip (20.49 KB)
模糊逻辑介绍 模糊逻辑介绍
模糊逻辑扩展了我们的数理逻辑和集合论的界限。本文揭示了模糊逻辑的基本原理, 同时描述使用马丹尼型和关野型的两种推理系统。提供的例程将描述如何使用 MQL5 版本的模糊库来实现这两种类型的系统。
用随机森林预测趋势 用随机森林预测趋势
本文使用Rattle包自动进行模式识别,来预测外汇市场的多头和空头。本文对初学者和有经验的交易者都适用。
在 MetaTrader 5 里使用 HedgeTerminal (对冲终端) 面板进行双向交易和仓位对冲, 第二部分 在 MetaTrader 5 里使用 HedgeTerminal (对冲终端) 面板进行双向交易和仓位对冲, 第二部分
本文描述了一种新的方法来进行仓位对冲, 并在 MetaTrader 4 和 MetaTrader 5 的用户之间就此事的争辩划清界线。这是: "在 MetaTrader 5 里使用 HedgeTerminal (对冲终端) 面板进行双向交易和仓位对冲" 第一部分的延续。在第二部分里, 我们讨论自定义 EA 与 HedgeTerminalAPI 的集成, 其作为特别的可视化程序库, 设计用于在一个舒适的软件环境里作为工具进行便利的双向交易仓位管理。
研究CCanvas类如何绘制透明的图形对象 研究CCanvas类如何绘制透明的图形对象
你是不是想要更加好看的移动平均线?你想要在终端中绘制更加漂亮的而不是简单的实心矩形吗?终端中能够绘制出更有吸引力的图形。这可以通过CCanvas类来实现,该类用于创建自定义图形对象。用这个类你能够实现透明化,混合色以及通过重叠和混合颜色产生透明的效果。