下载MetaTrader 5

图形界面 X: 标准图表控件 (集成编译 4)

12 十二月 2016, 07:27
Anatoli Kazharski
0
3 856

内容


概论

在首篇文章 图形界面 I: 库结构准备 (第一章) 里详细解释了库的用途。您可在每章结尾处找到包含文章列表的链接。从那里, 您还可以下载当前最新开发阶段的完整版库文件。这些文件必须放置在与文档位置相同的目录中。

我们来研究另一个控件, 它不能被遗留在正在开发的库文件之外。当启动交易终端时, 会为用户打开价格图表。拥有一款轻松管理图表的工具将会更便利。前一篇名为 MQL5 酷宝书: 在单一窗口内监视多个时间帧 的文章里已经展示了此类工具的一个可能变体。这一次, 我们将编写一个用于创建控件的类, 它可轻松简单地用在自定义 MQL 应用程序的图形界面中。与以上链接中提供的前一版本不同, 这次的实现将允许水平滚动子图表的内容, 如同在常规主图表窗口中。

此外, 我们将继续优化库代码以降低 CPU 负载。更多细节会在文章中进一步提供。

 

开发创建标准图表控件的类

在开始开发用于创建标准图表控件的 CStandardChart 类之前, 应将拥有额外属性 (参阅以下所列代码) 的 CSubChart 基类 添加到 Object.mqh 文件中。对于所有类型的图形对象, 应该在创建使用它们的库控件之前完成。类 CSubChart 的基类来自标准库 — CChartObjectSubChart。 

//+------------------------------------------------------------------+
//|                                                      Objects.mqh |
//|                                 版权所有 2015, MetaQuotes 软件公司|
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
...
//--- 文件内的类列表, 用于快速导航 (Alt+G)
...
class CSubChart;
//+------------------------------------------------------------------+
//| 具有额外属性的 Subchart 对象类                                    |
//+------------------------------------------------------------------+
class CSubChart : public CChartObjectSubChart
  {
protected:
   int               m_x;
   int               m_y;
   int               m_x2;
   int               m_y2;
   int               m_x_gap;
   int               m_y_gap;
   int               m_x_size;
   int               m_y_size;
   bool              m_mouse_focus;
   //---
public:
                     CSubChart(void);
                    ~CSubChart(void);
   //--- 坐标
   int               X(void)                      { return(m_x);           }
   void              X(const int x)               { m_x=x;                 }
   int               Y(void)                      { return(m_y);           }
   void              Y(const int y)               { m_y=y;                 }
   int               X2(void)                     { return(m_x+m_x_size);  }
   int               Y2(void)                     { return(m_y+m_y_size);  }
   //--- 自边缘点的边距 (xy)
   int               XGap(void)                   { return(m_x_gap);       }
   void              XGap(const int x_gap)        { m_x_gap=x_gap;         }
   int               YGap(void)                   { return(m_y_gap);       }
   void              YGap(const int y_gap)        { m_y_gap=y_gap;         }
   //--- 大小
   int               XSize(void)                  { return(m_x_size);      }
   void              XSize(const int x_size)      { m_x_size=x_size;       }
   int               YSize(void)                  { return(m_y_size);      }
   void              YSize(const int y_size)      { m_y_size=y_size;       }
   //--- 焦点
   bool              MouseFocus(void)             { return(m_mouse_focus); }
   void              MouseFocus(const bool focus) { m_mouse_focus=focus;   }
  };
//+------------------------------------------------------------------+
//| 构造器                                                           |
//+------------------------------------------------------------------+
CSubChart::CSubChart(void) : m_x(0),
                             m_y(0),
                             m_x2(0),
                             m_y2(0),
                             m_x_gap(0),
                             m_y_gap(0),
                             m_x_size(0),
                             m_y_size(0),
                             m_mouse_focus(false)
  {
  }
//+------------------------------------------------------------------+
//| 析构器                                                           |
//+------------------------------------------------------------------+
CSubChart::~CSubChart(void)
  {
  }

CChartObjectSubChart 包含用来创建子图表的方法, 以及修改最常用图表属性的方法。列表包括属性赋值和取值的方法: 

  • 坐标和维度
  • 品名, 时间帧和尺度
  • 显示价格和时间尺度。  
//+------------------------------------------------------------------+
//|                                          ChartObjectSubChart.mqh |
//|                            版权所有 2009-2013, MetaQuotes 软件公司|
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "ChartObject.mqh"
//+------------------------------------------------------------------+
//| 类 CChartObjectSubChart。                                        |
//| 目的: 图表对象 "SubChart" 的类。                                  |
//|          衍生自 CChartObject 类。                                |
//+------------------------------------------------------------------+
class CChartObjectSubChart : public CChartObject
  {
public:
                     CChartObjectSubChart(void);
                    ~CChartObjectSubChart(void);
   //--- 访问对象属性的方法
   int               X_Distance(void) const;
   bool              X_Distance(const int X) const;
   int               Y_Distance(void) const;
   bool              Y_Distance(const int Y) const;
   ENUM_BASE_CORNER  Corner(void) const;
   bool              Corner(const ENUM_BASE_CORNER corner) const;
   int               X_Size(void) const;
   bool              X_Size(const int size) const;
   int               Y_Size(void) const;
   bool              Y_Size(const int size) const;
   string            Symbol(void) const;
   bool              Symbol(const string symbol) const;
   int               Period(void) const;
   bool              Period(const int period) const;
   int               Scale(void) const;
   bool              Scale(const int scale) const;
   bool              DateScale(void) const;
   bool              DateScale(const bool scale) const;
   bool              PriceScale(void) const;
   bool              PriceScale(const bool scale) const;
   //--- 锁定更改时间/价格坐标
   bool              Time(const datetime time) const { return(false); }
   bool              Price(const double price) const { return(false); }
   //--- 创建对象的方法
   bool              Create(long chart_id,const string name,const int window,
                            const int X,const int Y,const int sizeX,const int sizeY);
   //--- 识别对象的方法
   virtual int       Type(void) const { return(OBJ_CHART); }
   //--- 操纵文件的方法
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
  };

现在我们可以利用 CStandardChart 类来创建 StandardChart.mqh 文件, 此处所有库控件的标准方法可以在基础内容中指定, 如下代码所示。由于控件具有水平滚动模式, 这需要一个鼠标光标的图标以便告知用户滚动模式被启用, 并且子图表中的数据将随着鼠标光标的水平移动而移动。若要更改图标, 包含 Pointer.mqh 文件, 它所包括的 CPointer 类已在之前的文章 图形界面 VIII: 树形视图控件 (第二章) 里讨论。作为鼠标指针的图标, 我们将使用主图表水平滚动激活的图标 (具有白色轮廓的双侧黑色箭头)。此图标的两个版本 (黑色和蓝色箭头) 附于本文末尾。 

相应地, 鼠标指针枚举 (ENUM_MOUSE_POINTER) 补充另一个标识符 (MP_X_SCROLL): 

//+------------------------------------------------------------------+
//| 指针类型枚举                                                      |
//+------------------------------------------------------------------+
enum ENUM_MOUSE_POINTER
  {
   MP_CUSTOM     =0,
   MP_X_RESIZE   =1,
   MP_Y_RESIZE   =2,
   MP_XY1_RESIZE =3,
   MP_XY2_RESIZE =4,
   MP_X_SCROLL   =5
  };

此外, 有必要在 Pointer.mqh 文件里 包含此光标类型的图标资源, 且在 CPointer::SetPointerBmp() 方法的 switch 结构里应 扩展其它 case

//+------------------------------------------------------------------+
//|                                                      Pointer.mqh |
//|                                 版权所有 2015, MetaQuotes 软件公司|
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
//--- 资源
...
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll.bmp"
#resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll_blue.bmp"
//+------------------------------------------------------------------+
//| 基于光标类型设置光标图像                                           |
//+------------------------------------------------------------------+
void CPointer::SetPointerBmp(void)
  {
   switch(m_type)
     {
      case MP_X_RESIZE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_blue.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs.bmp";
         break;
      case MP_Y_RESIZE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_blue.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs.bmp";
         break;
      case MP_XY1_RESIZE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_xy1_rs_blue.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_xy1_rs.bmp";
         break;
      case MP_XY2_RESIZE :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_xy2_rs_blue.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_xy2_rs.bmp";
         break;
      case MP_X_SCROLL :
         m_file_on  ="Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll_blue.bmp";
         m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll.bmp";
         break;
     }
//--- 如果指定自定义类型 (MP_CUSTOM)
   if(m_file_on=="" || m_file_off=="")
      ::Print(__FUNCTION__," > 必须为此光标设置全部图像!");
  }

应当注意的是 Moving() 方法也可按照两种模式使用, 这可以通过 方法的第三个参数 来设置。此参数的省缺值为 false, 这意味着只有在控件被附加到表单且当前处于移动模式的情况下才可移动。 

//+------------------------------------------------------------------+
//|                                                StandardChart.mqh |
//|                                 版权所有 2015, MetaQuotes 软件公司|
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Element.mqh"
#include "Window.mqh"
#include "Pointer.mqh"
//+------------------------------------------------------------------+
//| 创建标准图表的类                                                  |
//+------------------------------------------------------------------+
class CStandardChart : public CElement
  {
private:
   //--- 指向元素被附加表单的指针
   CWindow          *m_wnd;
   //---
public:
   //--- 保存表单指针
   void              WindowPointer(CWindow &object)          { m_wnd=::GetPointer(object); }
   //---
public:
   //--- 图表事件处理器
   virtual void      OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   //--- 移动元素
   virtual void      Moving(const int x,const int y,const bool moving_mode=false);
   //--- (1) 显示, (2) 隐藏, (3) 重置, (4) 删除
   virtual void      Show(void);
   virtual void      Hide(void);
   virtual void      Reset(void);
   virtual void      Delete(void);
   //--- (1) 设置, (2) 重置鼠标左键按下时的优先权
   virtual void      SetZorders(void);
   virtual void      ResetZorders(void);
   //---
private:
   //--- 更改窗口右边缘的宽度
   virtual void      ChangeWidthByRightWindowSide(void);
   //--- 更改窗口底部边缘的高度
   virtual void      ChangeHeightByBottomWindowSide(void);
  };
//+------------------------------------------------------------------+
//| 构造器                                                           |
//+------------------------------------------------------------------+
CStandardChart::CStandardChart(void)
  {
//--- 在基类中保存元素类的名字
   CElement::ClassName(CLASS_NAME);
  }
//+------------------------------------------------------------------+
//| 析构器                                                           |
//+------------------------------------------------------------------+
CStandardChart::~CStandardChart(void)
  {
  }

如果 Moving() 方法的第三个参数设为 true, 则控件将被调用的方法强制移动, 无论表单是否处于移动模式。在某些情况下, 这可明显降低 CPU 负载。 

为了查看表单是否处于移动模式, 已将方法 ClampingAreaMouse() 添加到 CWindow 类。它返回鼠标左键按下时的区域: 

//+------------------------------------------------------------------+
//| 创建控件表单的类                                                  |
//+------------------------------------------------------------------+
class CWindow : public CElement
  {
public:
   //--- 返回鼠标左键按下时的区域
   ENUM_MOUSE_STATE  ClampingAreaMouse(void)                           const { return(m_clamping_area_mouse);          }
  };

方法 CWindow::ClampingAreaMouse() 仅能通过所依附的表单指针访问。若要全部按照上述工作, 应在每个控件的 Moving() 方法里插入一段代码, 如下所示清单 (参阅黄色高亮片段)。作为例程, 来自 CSimpleButton (较短版本) 类的方法作为展示。 

//+------------------------------------------------------------------+
//| 控件移动                                                         |
//+------------------------------------------------------------------+
void CSimpleButton::Moving(const int x,const int y,const bool moving_mode=false)
  {
//--- 如果控件隐藏则离开
   if(!CElement::IsVisible())
      return;
//--- 如果将管理委派给窗口, 则确定其位置
   if(!moving_mode)
      if(m_wnd.ClampingAreaMouse()!=PRESSED_INSIDE_HEADER)
         return;
//--- 如果锚定在右侧
//--- 如果锚定在左侧
//--- 如果锚定在底部
//--- 如果锚定在顶部
//--- 更新图形对象坐标
   m_button.X_Distance(m_button.X());
   m_button.Y_Distance(m_button.Y());
  }

使用 Moving() 方法的例程如以下清单所示, 它展示了 CSimpleButton::Show() 方法的代码。在此情况下, 应强制更新控件坐标, 因此, true 作为第三个参数传递。库中所有使用 Moving() 方法的类均进行了适当的修改。  

//+------------------------------------------------------------------+
//| 显示按钮                                                         |
//+------------------------------------------------------------------+
void CSimpleButton::Show(void)
  {
//--- 如果元素可见则离开
   if(CElement::IsVisible())
      return;
//--- 令所有对象可见
   m_button.Timeframes(OBJ_ALL_PERIODS);
//--- 可见状态
   CElement::IsVisible(true);
//--- 更新对象位置
   Moving(m_wnd.X(),m_wnd.Y(),true);
  }

已开发库的优化将在本文后面讨论, 而现在, 将研究标准图表控件。

让我们在一行中创建放置子图表的数组。为此, 必须为表示图表的对象, 以及某些属性声明动态数组, 例如: (1) 图表标识符, (2) 品名和 (3) 时间帧。在创建标准图表控件之前, 有必要使用 CStandardChart::AddSubChart() 方法, 此处应传递图表品名和时间帧。方法伊始, 使用传递值将元素添加到数组并初始化它们之前, 使用 CStandardChart::CheckSymbol() 方法。  

class CStandardChart : public CElement
  {
private:
   //--- 创建元素的对对象
   CSubChart         m_sub_chart[];
   //--- 图表属性
   long              m_sub_chart_id[];
   string            m_sub_chart_symbol[];
   ENUM_TIMEFRAMES   m_sub_chart_tf[];
   //---
public:
   //--- 在创建之前添加指定属性的图表
   void              AddSubChart(const string symbol,const ENUM_TIMEFRAMES tf);
   //---
private:
   //--- 检查品名
   bool              CheckSymbol(const string symbol);
  };

方法 CStandardChart::CheckSymbol() 首先检查指定品名在市场观察窗口里是否可用。如果品名未发现, 则它尝试在常用列表里搜索此品名。如果品名已发现, 则它必须添加到市场观察。否则, 将无法使用此品名创建子图表 (将创建与主图表相同品名的子图表来代替)。 

若成功, CStandardChart::CheckSymbol() 方法返回 true。如果未找到指定的品名, 则方法将返回 false, 且子窗口不会被添加 (数组大小相同), 有关消息将会显示在日志中。  

//+------------------------------------------------------------------+
//| 添加图表                                                         |
//+------------------------------------------------------------------+
void CStandardChart::AddSubChart(const string symbol,const ENUM_TIMEFRAMES tf)
  {
//--- 检查品名是否在服务器上可用
   if(!CheckSymbol(symbol))
     {
      ::Print(__FUNCTION__," > 品名 "+symbol+" 在服务器上不可用!");
      return;
     }
//--- 数组增加一个元素大小
   int array_size=::ArraySize(m_sub_chart);
   int new_size=array_size+1;
   ::ArrayResize(m_sub_chart,new_size);
   ::ArrayResize(m_sub_chart_id,new_size);
   ::ArrayResize(m_sub_chart_symbol,new_size);
   ::ArrayResize(m_sub_chart_tf,new_size);
//--- 保存传递的参数值
   m_sub_chart_symbol[array_size] =symbol;
   m_sub_chart_tf[array_size]     =tf;
  }
//+------------------------------------------------------------------+
//| 检查品种可用性                                                    |
//+------------------------------------------------------------------+
bool CStandardChart::CheckSymbol(const string symbol)
  {
   bool flag=false;
//--- 检查市场观察内的品名
   int symbols_total=::SymbolsTotal(true);
   for(int i=0; i<symbols_total; i++)
     {
      //--- 如果品名可用, 停止循环
      if(::SymbolName(i,true)==symbol)
        {
         flag=true;
         break;
        }
     }
//--- 如果品名在市场观察窗口内不可用
   if(!flag)
     {
      //--- ... 尝试在常用列表里查找它
      symbols_total=::SymbolsTotal(false);
      for(int i=0; i<symbols_total; i++)
        {
         //--- 如果品名可用
         if(::SymbolName(i,false)==symbol)
           {
            //--- ... 将其添加到市场观察窗口并停止循环
            ::SymbolSelect(symbol,true);
            flag=true;
            break;
           }
        }
     }
//--- 返回搜索结果
   return(flag);
  }

创建标准图表控件需要三个方法: 一个主要的 public 方法和两个 private 方法, 其中之一是在水平滚动模式下 涉及鼠标光标图标。公有方法 CStandardChart::SubChartsTotal() 已添加到类中作为检索子图表数量的辅助方法。 

class CStandardChart : public CElement
  {
private:
   //--- 创建元素的对对象
   CSubChart         m_sub_chart[];
   CPointer          m_x_scroll;
   //---
public:
   //--- 创建标准图表的方法
   bool              CreateStandardChart(const long chart_id,const int subwin,const int x,const int y);
   //---
private:
   bool              CreateSubChart(void);
   bool              CreateXScrollPointer(void);
   //---
public:
   //--- 返回图表数组的大小
   int               SubChartsTotal(void)              const { return(::ArraySize(m_sub_chart)); }
  };

我们来研究创建子图表的 CStandardChart::CreateSubCharts() 方法。在方法伊始, 创建控件之前要检查添加到数组的图表数量。如果尚未添加, 则程序简单地退出方法并在日志中打印相关消息。

如果已添加图表, 则计算每个对象的宽度。控件的总宽度必须由用户在所开发的 MQL 应用程序的自定义类中定义。在需要创建多个图表的情况下, 为了获得每个对象的宽度, 将控件总宽度除以图表的数量就足够了。

之后在循环中创建对象。这已考虑了控制定位 (锚点指向表单一侧)。此话题已在之前的文章中详细描述过, 因此这里不再赘述。 

在创建子图表之后, 所得到的图表已创建的标识符保存到数组, 其属性已设置并保存。 

//+------------------------------------------------------------------+
//| 创建图表                                                          |
//+------------------------------------------------------------------+
bool CStandardChart::CreateSubCharts(void)
  {
//--- 得到图表数量
   int sub_charts_total=SubChartsTotal();
//--- 如果在群中没有图表, 报告
   if(sub_charts_total<1)
     {
      ::Print(__FUNCTION__," > 此方法未被调用, "
              "如果群内至少包含一个图表!使用 CStandardChart::AddSubChart() 方法");
      return(false);
     }
//--- 计算坐标和大小
   int x=m_x;
   int x_size=(sub_charts_total>1)? m_x_size/sub_charts_total : m_x_size;
//--- 创建指定数量的图表
   for(int i=0; i<sub_charts_total; i++)
     {
      //--- 形成对象名
      string name=CElement::ProgramName()+"_sub_chart_"+(string)i+"__"+(string)CElement::Id();
      //--- 计算 X 坐标
      x=(i>0)?(m_anchor_right_window_side)? x-x_size+1 :  x+x_size-1 : x;
      //--- 调整最后图表的宽度
      if(i+1>=sub_charts_total)
         x_size=m_x_size-(x_size*(sub_charts_total-1)-(sub_charts_total-1));
      //--- 设置按钮
      if(!m_sub_chart[i].Create(m_chart_id,name,m_subwin,x,m_y,x_size,m_y_size))
         return(false);
      //--- 得到并保存已创建图表的标识符
      m_sub_chart_id[i]=m_sub_chart[i].GetInteger(OBJPROP_CHART_ID);
      //--- 设置属性
      m_sub_chart[i].Symbol(m_sub_chart_symbol[i]);
      m_sub_chart[i].Period(m_sub_chart_tf[i]);
      m_sub_chart[i].Z_Order(m_zorder);
      m_sub_chart[i].Tooltip("\n");
      //--- 保存大小
      m_sub_chart[i].XSize(x_size);
      m_sub_chart[i].YSize(m_y_size);
      //--- 自边缘对其
      m_sub_chart[i].XGap((m_anchor_right_window_side)? x : x-m_wnd.X());
      m_sub_chart[i].YGap((m_anchor_bottom_window_side)? m_y : m_y-m_wnd.Y());
      //--- 保存对象指针
      CElement::AddToArray(m_sub_chart[i]);
     }
//---
   return(true);
  }

在标准图表控件创建后, 可随时更改其所包含的子图表的任何属性。这可通过 CStandardChart::GetSubChartPointer() 方法的协助下获取指针来完成。如果偶然传递了不正确的索引, 将会 调整 以防止超出数组范围。 

class CStandardChart : public CElement
  {
public:
   //--- 返回指定索引处的子图表指针
   CSubChart        *GetSubChartPointer(const uint index);
  };
//+------------------------------------------------------------------+
//| 返回指定索引处的图表指针                                           |
//+------------------------------------------------------------------+
CSubChart *CStandardChart::GetSubChartPointer(const uint index)
  {
   uint array_size=::ArraySize(m_sub_chart);
//--- 如果无图表, 报告
   if(array_size<1)
     {
      ::Print(__FUNCTION__," > 此方法未被调用, "
              "如果群内包含至少一个图表!");
     }
//--- 若超界则调整
   uint i=(index>=array_size)? array_size-1 : index;
//--- 返回指针
   return(::GetPointer(m_sub_chart[i]));
  }

鼠标光标的图标仅当子图表启用数据水平滚动模式时才会创建。. 它应当在创建控件之前使用 CStandardChart::XScrollMode() 方法 启用。  

class CStandardChart : public CElement
  {
private:
   //--- 水平滚动模式
   bool              m_x_scroll_mode;
   //---
public:
   //--- 水平滚动模式
   void              XScrollMode(const bool mode) { m_x_scroll_mode=mode; }
  };
//+------------------------------------------------------------------+
//| 创建水平滚动光标                                                  |
//+------------------------------------------------------------------+
bool CStandardChart::CreateXScrollPointer(void)
  {
//--- 离开, 若无需水平滚动
   if(!m_x_scroll_mode)
      return(true);
//--- 设置属性
   m_x_scroll.XGap(0);
   m_x_scroll.YGap(-20);
   m_x_scroll.Id(CElement::Id());
   m_x_scroll.Type(MP_X_SCROLL);
//--- 创建一个元素
   if(!m_x_scroll.CreatePointer(m_chart_id,m_subwin))
      return(false);
//---
   return(true);
  }

总结上述。如果启用水平滚动模式, 则其操作使用 CStandardChart::HorizontalScroll() 方法, 当 CHARTEVENT_MOUSE_MOVE 事件被触发时它将在事件处理器中调用。按下鼠标左键的情况下, 根据它是刚被按下还是在水平滚动的过程中重复调用方法, 该方法计算鼠标光标自按压点 (以像素为单位) 覆盖的距离。此处: 

  • 表单被锁定。
  • 计算鼠标光标的图标坐标。
  • 显示图标。

在子图表内的平移数据值可为负数, 因为偏移相对于最后一根柱线执行 - ::ChartNavigate() 方法的 CHART_END 值 (第二个参数) 来自 ENUM_CHART_POSITION 枚举。如果平移值为正数, 程序退出方法。如果检查通过, 则保存当前平移值用于下一次迭代。然后, 所有子图表的 "自动滚动" (CHART_AUTOSCROLL) 和 "图表自由边界平移" (CHART_SHIFT) 被禁用, 平移会根据计算值执行。

如果松开鼠标左键, 表单将被解锁, 并且水平滚动过程的指示鼠标光标的图标将被隐藏。之后, 程序退出该方法。 

class CStandardChart : public CElement
  {
private:
   //--- 有关图表水平滚动的变量
   int               m_prev_x;
   int               m_new_x_point;
   int               m_prev_new_x_point;
   //---
private:
   //--- 水平滚动
   void              HorizontalScroll(void);
  };
//+------------------------------------------------------------------+
//| 图表水平滚动                                                      |
//+------------------------------------------------------------------+
void CStandardChart::HorizontalScroll(void)
  {
//--- 离开, 如果图表水平滚动被禁用
   if(!m_x_scroll_mode)
      return;
//--- 如果鼠标按钮被按下
   if(m_mouse.LeftButtonState())
     {
      //--- 保存当前光标的 X 坐标
      if(m_prev_x==0)
        {
         m_prev_x      =m_mouse.X()+m_prev_new_x_point;
         m_new_x_point =m_prev_new_x_point;
        }
      else
         m_new_x_point=m_prev_x-m_mouse.X();
      //--- 阻塞表单
      if(!m_wnd.IsLocked())
        {
         m_wnd.IsLocked(true);
         m_wnd.IdActivatedElement(CElement::Id());
        }
      //--- 更新指针坐标, 并令其可见
      int l_x=m_mouse.X()-m_x_scroll.XGap();
      int l_y=m_mouse.Y()-m_x_scroll.YGap();
      m_x_scroll.Moving(l_x,l_y);
      //--- 显示指针
      m_x_scroll.Show();
      //--- 设置可视标志
      m_x_scroll.IsVisible(true);
     }
   else
     {
      m_prev_x=0;
      //--- 解锁表单
      if(m_wnd.IdActivatedElement()==CElement::Id())
        {
         m_wnd.IsLocked(false);
         m_wnd.IdActivatedElement(WRONG_VALUE);
        }
      //--- 隐藏指针
      m_x_scroll.Hide();
      //--- 设置可视标志
      m_x_scroll.IsVisible(false);
      return;
     }
//--- 离开, 如果为正数值
   if(m_new_x_point>0)
      return;
//--- 保存当前位置
   m_prev_new_x_point=m_new_x_point;
//--- 应用于所有图表
   int symbols_total=SubChartsTotal();
//--- 禁用自动滚动和自右边界平移
   for(int i=0; i<symbols_total; i++)
     {
      if(::ChartGetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL))
         ::ChartSetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL,false);
      if(::ChartGetInteger(m_sub_chart_id[i],CHART_SHIFT))
         ::ChartSetInteger(m_sub_chart_id[i],CHART_SHIFT,false);
     }
//--- 重置最后的错误
   ::ResetLastError();
//--- 平移图表
   for(int i=0; i<symbols_total; i++)
      if(!::ChartNavigate(m_sub_chart_id[i],CHART_END,m_new_x_point))
         ::Print(__FUNCTION__," > 错误: ",::GetLastError());
  }

方法 CStandardChart::ZeroHorizontalScrollVariables() 将用于重置子图表数据水平滚动模式的辅助变量。它也可能需要以编程方式跳转到到最后一根柱线。为达此目的, 使用了 CStandardChart::ResetCharts() 公有方法。 

class CStandardChart : public CElement
  {
public:
   //--- 重置图表
   void              ResetCharts(void);
   //---
private:
   //--- 重置水平滚动变量
   void              ZeroHorizontalScrollVariables(void);
  };
//+------------------------------------------------------------------+
//| 重置图表                                                         |
//+------------------------------------------------------------------+
void CStandardChart::ResetCharts(void)
  {
   int sub_charts_total=SubChartsTotal();
   for(int i=0; i<sub_charts_total; i++)
      ::ChartNavigate(m_sub_chart_id[i],CHART_END);
//--- 重置用于图表水平滚动的辅助变量
   ZeroHorizontalScrollVariables();
  }
//+------------------------------------------------------------------+
//| 重置水平滚动变量                                                  |
//+------------------------------------------------------------------+
void CStandardChart::ZeroHorizontalScrollVariables(void)
  {
   m_prev_x           =0;
   m_new_x_point      =0;
   m_prev_new_x_point =0;
  }

也可能需要跟踪 "标准图表" 控件上子图表的鼠标左键按下事件。因此, 在 Defines.mqh 文件里添加新的 ON_CLICK_SUB_CHART 标识符: 

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                                 版权所有 2015, MetaQuotes 软件公司|
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
...
//--- 事件标识符
...
#define ON_CLICK_SUB_CHART         (28) // 点击子图表

为了检测在子图表上点击, 实现 CStandardChart::OnClickSubChart() 方法。如果检查名字和标识符成功 (参见以下清单), 则它 生成一条消息 带有 (1) ON_CLICK_SUB_CHART 事件标识符, (2) 控件标识符, (3) 子图表索引, 以及 (4) 品名。 

class CStandardChart : public CElement
  {
private:
   //--- 处理子图表按下
   bool              OnClickSubChart(const string clicked_object);
  };
//+------------------------------------------------------------------+
//| 处理按钮按下                                                      |
//+------------------------------------------------------------------+
bool CStandardChart::OnClickSubChart(const string clicked_object)
  {
//--- 离开, 如果并非在菜单项上按下
   if(::StringFind(clicked_object,CElement::ProgramName()+"_sub_chart_",0)<0)
      return(false);
//--- 从对象名得到标识符和索引
   int id=CElement::IdFromObjectName(clicked_object);
//--- 离开, 如果标识符不匹配
   if(id!=CElement::Id())
      return(false);
//--- 得到索引
   int group_index=CElement::IndexFromObjectName(clicked_object);
//--- 发送一条有关信号
   ::EventChartCustom(m_chart_id,ON_CLICK_SUB_CHART,CElement::Id(),group_index,m_sub_chart_symbol[group_index]);
   return(true);
  }

假设您需要另一种方式来导航子图表, 类似于终端实现的主图表方式。如果在 MetaTrader 终端里按下 «空格» 或 «回车» 键, 则会激活图表左下角的一个编辑框 (参阅以下的屏幕截图)。这是一种命令行, 在其中输入日期即可在图表上跳转到该日期。此命令行也可用来更改图表的品名和时间帧。  

 图例. 1. 在图表左下角的命令行。

图例. 1. 在图表左下角的命令行。


顺便说一下, 在交易终端的最新版 里已添加了管理命令行的新功能。

8. MQL5: 新属性 CHART_QUICK_NAVIGATION 可以启用/禁用图表上的快速柱线导航。若您需要修改以及访问属性状态, 使用 ChartSetIntegerChartGetInteger 函数。

按下回车或空格打开导航栏。它可令您快速移动到图表上的指定日期, 以及切换品名和时间帧。若您的 MQL5 程序处理回车和空格按键动作, 请禁用 CHART_QUICK_NAVIGATION 属性, 以免终端拦截这些事件。快速导航栏仍然可以通过双击打开。

… 

在图形界面中, 一切皆可更方便及更容易地完成。在 Easy And Fast 库中以及包含了日历控件 (CCalendar 类)。主图表和子图表的导航可以通过在日历中简单地选择日期来实现。我们将所有事情简化为一个具有单一参数的方法。此参数的值将是图表需要平移的日期。此方法将命名为 CStandardChart::SubChartNavigate(), 以下的代码清单展示了此方法的当前版本。

主图表的 "自动滚动" 和 "自图表右边界平移" 模式在方法伊始即被禁止。然后, 如果传递给方法的变量大于当日的开始, 只需跳转到最后一个根柱线并退出该方法。如果日期较小, 则必须计算向左平移的柱线数量。首先, 进行主图表的计算:

  • 获取自当日开始直到指定日期为止的当前品种与时间帧的可用柱线总数。
  • 获取图表上可见的柱线数量。
  • 获取自当日开始的柱线数量 + 两根柱线作为一个额外的缩进。
  • 计算自最后一根柱线平移的柱线数量。

之后, 主图表向左平移, 并针对子图表重复一切。 

class CStandardChart : public CElement
  {
public:
   //--- 跳转到指定日期
   void              SubChartNavigate(const datetime date);
  };
//+------------------------------------------------------------------+
//| 跳转到指定日期                                                    |
//+------------------------------------------------------------------+
void CStandardChart::SubChartNavigate(const datetime date)
  {
//--- (1) 当前图表日期, 及 (2) 从日历中选择的新日期
   datetime current_date  =::StringToTime(::TimeToString(::TimeCurrent(),TIME_DATE));
   datetime selected_date =date;
//--- 禁用自动滚动和自右边界平移
   ::ChartSetInteger(m_chart_id,CHART_AUTOSCROLL,false);
   ::ChartSetInteger(m_chart_id,CHART_SHIFT,false);
//--- 如果从日历中选择的日期大于当前日期
   if(selected_date>=current_date)
     {
      //--- 在所有图表中跳转到当前日期
      ::ChartNavigate(m_chart_id,CHART_END);
      ResetCharts();
      return;
     }
//--- 获取自指定日期的柱线数量
   int  bars_total    =::Bars(::Symbol(),::Period(),selected_date,current_date);
   int  visible_bars  =(int)::ChartGetInteger(m_chart_id,CHART_VISIBLE_BARS);
   long seconds_today =::TimeCurrent()-current_date;
   int  bars_today    =int(seconds_today/::PeriodSeconds())+2;
//--- 设置所有图表的距右边界的缩进
   m_prev_new_x_point=m_new_x_point=-((bars_total-visible_bars)+bars_today);
   ::ChartNavigate(m_chart_id,CHART_END,m_new_x_point);
//---
   int sub_charts_total=SubChartsTotal();
   for(int i=0; i<sub_charts_total; i++)
     {
      //--- 禁用自动滚动和自右边界平移
      ::ChartSetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL,false);
      ::ChartSetInteger(m_sub_chart_id[i],CHART_SHIFT,false);
      //--- 获取自指定日期的柱线数量
      bars_total   =::Bars(m_sub_chart[i].Symbol(),(ENUM_TIMEFRAMES)m_sub_chart[i].Period(),selected_date,current_date);
      visible_bars =(int)::ChartGetInteger(m_sub_chart_id[i],CHART_VISIBLE_BARS);
      bars_today   =int(seconds_today/::PeriodSeconds((ENUM_TIMEFRAMES)m_sub_chart[i].Period()))+2;
      //--- 距图表右边界的缩进
      m_prev_new_x_point=m_new_x_point=-((bars_total-visible_bars)+bars_today);
      ::ChartNavigate(m_sub_chart_id[i],CHART_END,m_new_x_point);
     }
  }

用来创建标准图表控件的 CStandardChart 类已完成。现在我们来编写一个应用程序, 看看它是如何工作的。 


测试控件的应用

出于测试目的, 您可以使用之前一篇文章中的智能交易系统。除主菜单, 状态栏和选项卡之外, 删除所有控件。令每个选项卡对应单独一组子图表。每一组都将包含某种货币; 因此, 每个选项卡将具有相应的描述:

  • 第一个选项卡 - EUR (欧元)。
  • 第二个选项卡 - GBP (英镑)。
  • 第三个选项卡 - AUD (澳元)。
  • 第四个选项卡 - CAD (加币)。
  • 第五个选项卡 - JPY (日元)。

子图表将严格位于选项卡的工作区域中, 并且每当表单调整大小时, 它们将自动调整大小。选项卡中工作区域的右边界距表单右边缘的缩进量始终为 173 像素。此空间将填充用于设置属性的控件, 譬如:

  • 显示时间刻度 (日期时间)。
  • 显示价格刻度 (价格刻度)。
  • 改变图表时间帧 (时间帧)。
  • 通过日历进行图表数据导航。

作为示例, 显示用于创建单个标准图表 (CStandardChart) 控件的代码就足矣。请记住, 默认情况下图表的水平滚动为禁用, 以防万一需要, 可使用 CStandardChart::XScrollMode() 方法。方法 CStandardChart::AddSubChart() 用来在组中添加图表。

//+------------------------------------------------------------------+
//| 创建应用程序的类                                                  |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
protected:
   //--- 标准图表
   CStandardChart    m_sub_chart1;
   //---
protected:
   //--- 标准图表
   bool              CreateSubChart1(const int x_gap,const int y_gap);
  };
//+------------------------------------------------------------------+
//| 创建标准图表 1                                                    |
//+------------------------------------------------------------------+
bool CProgram::CreateSubChart1(const int x_gap,const int y_gap)
  {
//--- 保存窗口指针
   m_sub_chart1.WindowPointer(m_window);
//--- 加载第一个选卡
   m_tabs.AddToElementsArray(0,m_sub_chart1);
//--- 坐标
   int x=m_window.X()+x_gap;
   int y=m_window.Y()+y_gap;
//--- 创建之前设置属性
   m_sub_chart1.XSize(600);
   m_sub_chart1.YSize(200);
   m_sub_chart1.AutoXResizeMode(true);
   m_sub_chart1.AutoYResizeMode(true);
   m_sub_chart1.AutoXResizeRightOffset(175);
   m_sub_chart1.AutoYResizeBottomOffset(25);
   m_sub_chart1.XScrollMode(true);
//--- 添加图表
   m_sub_chart1.AddSubChart("EURUSD",PERIOD_D1);
   m_sub_chart1.AddSubChart("EURGBP",PERIOD_D1);
   m_sub_chart1.AddSubChart("EURAUD",PERIOD_D1);
//--- 创建控件
   if(!m_sub_chart1.CreateStandardChart(m_chart_id,m_subwin,x,y))
      return(false);
//--- 将对象添加到对象组的公共数组
   CWndContainer::AddToElementsArray(0,m_sub_chart1);
   return(true);
  }

以下的屏幕截图显示了最终结果。在这个示例中, 子图表数据可以水平地滚动, 类似于主图表那样的实现方式。此外, 子图表之间的导航通过日历操纵, 包括快速日期跳转。

 图例. 2. 标准图表控件测试。

图例. 2. 标准图表控件测试。


本文介绍的测试应用程序可从以下链接下载, 以供进一步学习。 

 

优化库引擎的定时器和事件处理器

早前, Easy And Fast 库仅在 Windows 7 x64 操作系统上进行过测试。之后升级到 Windows 10 x64, 发现 CPU 使用率显著增长。当与图形界面没有交互时, 即使在待命模式下, 库进程消耗的 CPU 资源高达 10%。以下的屏幕截图显示了在图表上加载测试 MQL 应用程序前后 CPU 的资源消耗。

图例. 3. 在图表上加载测试 MQL 应用程序之前 CPU 的资源消耗。

图例. 3. 在图表上加载测试 MQL 应用程序之前 CPU 的资源消耗。


图例. 4. 在图表上加载测试 MQL 应用程序之后 CPU 的资源消耗。

图例. 4. 在图表上加载测试 MQL 应用程序之后 CPU 的资源消耗。


事实证明, 问题出在库引擎的定时器中, 在此 图表每隔 16ms 刷新一次, 如以下代码所示:

//+------------------------------------------------------------------+
//| 定时器                                                           |
//+------------------------------------------------------------------+
void CWndEvents::OnTimerEvent(void)
  {
//--- 离开, 如果数组为空  
   if(CWndContainer::WindowsTotal()<1)
      return;
//--- 定期检查所有元素事件
   CheckElementsEventsTimer();
//--- 重绘图表
   m_chart.Redraw();
  }

如果我们在图表区域中添加鼠标光标移动, 并主动与 MQL 应用程序的图形界面进行交互, 则 CPU 消耗会进一步增加。任务是减少库引擎定时器的操作, 并在每次鼠标移动事件触发时消除图表重绘。如何做到这一点?

CWndEvents::ChartEventMouseMove() 方法中删除负责图表重绘 (红色高亮) 的那一行: 

//+------------------------------------------------------------------+
//| CHARTEVENT 鼠标移动事件                                           |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventMouseMove(void)
  {
//--- 离开, 如果这不是鼠标移动事件
   if(m_id!=CHARTEVENT_MOUSE_MOVE)
      return;
//--- 移动窗口
   MovingWindow();
//--- 设置图表状态
   SetChartState();
//--- 重绘图表
   m_chart.Redraw();
  }

对于库引擎定时器, 它的当前目的是当鼠标悬停时改变控件的颜色, 并在不同控件件执行快速转发 (列表, 表, 日历等), 因此, 它必须持续工作。为了节省资源, 当鼠标光标开始移动时定时器将被激活, 且一旦移动结束, 其操作将在短暂停顿后被阻塞。 

为了实现这个想法, 有必要在 CMouse 类中添加一些东西。附加内容包括一个系统定时器调用计数器, 以及 CMouse::GapBetweenCalls() 方法, 它返回调用鼠标移动事件之间的差值。 

class CMouse
  {
private:
   //--- 调用计数器
   ulong             m_call_counter;
   //---
public:   
   //--- 返回 (1) 最后调用期间保存的计数值, 及 (2) 调用鼠标移动事件处理器之间的差值
   ulong             CallCounter(void)     const { return(m_call_counter);                  }
   ulong             GapBetweenCalls(void) const { return(::GetTickCount()-m_call_counter); }
  };
//+------------------------------------------------------------------+
//| 构造器                                                           |
//+------------------------------------------------------------------+
CMouse::CMouse(void) : m_call_counter(::GetTickCount())
  {
  }

逻辑很简单。一旦鼠标光标开始移动, CMouse 类的事件处理器 保存系统定时器的当前值

//+------------------------------------------------------------------+
//| 鼠标光标移动事件处理器                                             |
//+------------------------------------------------------------------+
void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- 光标移动事件处理
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- 鼠标左键坐标和状态
      m_x                 =(int)lparam;
      m_y                 =(int)dparam;
      m_left_button_state =(bool)int(sparam);
      //--- 保存调用计数器值
      m_call_counter=::GetTickCount();
      //--- 获取光标位置
      if(!::ChartXYToTimePrice(0,m_x,m_y,m_subwin,m_time,m_level))
         return;
      //--- 获取相对的 Y 坐标
      if(m_subwin>0)
         m_y=m_y-m_chart.SubwindowY(m_subwin);
     }
  }

库引擎的定时器 (CWndEvents 类) 必须包含一个条件: 如果鼠标光标 500ms 未移动, 图表不应重绘。此时必须释放鼠标左键, 以避免控件的快速转发选项仅能工作 500ms 的情形。 

//+------------------------------------------------------------------+
//| 定时器                                                           |
//+------------------------------------------------------------------+
void CWndEvents::OnTimerEvent(void)
  {
//--- 离开, 如果鼠标光标处于静止状态 (调用之间的差值大于 500ms) 则释放鼠标左键。
   if(m_mouse.GapBetweenCalls()>500 && !m_mouse.LeftButtonState())
      return;
//--- 离开, 如果数组为空  
   if(CWndContainer::WindowsTotal()<1)
      return;
//--- 定期检查所有元素事件
   CheckElementsEventsTimer();
//--- 重绘图表
   m_chart.Redraw();
  }

问题解决了。鼠标光标移动时禁止重绘对于移动控件的表单质量没有影响, 因为定时器间隔 16ms 对于重绘足够了。问题已经通过最简单的方式解决, 但不是唯一可能的方式。库代码的优化将在本系列的后续文章中进一步讨论, 因为还有其它方法和选项可以帮助更有效地降低 CPU 消耗。


优化树形视图和文件导航控件

已发现当初始化包含大量元素的树形视图 (CTreeView) 时会花费很长时间。这也发生在文件导航器 (CFileNavigator) 中, 它利用了列表类型。为了解决这个问题, 当向数组中添加元素时, 需要指定数组的预留大小作为 ::ArrayResize() 函数的第三个参数。 

援引来自 ::ArrayResize() 函数的引用:

对于频繁的内存分配, 建议使用第三个参数设置预留大小以便减少物理内存分配的数量。所有 ArrayResize 的后续调用不会导致物理内存的重分配, 但仅能改变数组预留内存的第一维大小。应当记住, 第三个参数仅在物理内存分配期间使用...

为了比较, 以下是使用不同保留大小数组的树形视图的测试结果。测试的文件数超过 15 000

 图例. 5. 使用保留大小值的表单数组的测试结果。

图例. 5. 使用保留大小值的表单数组的测试结果。


将树形视图的数组保留大小设置为 10 000CTreeViewCFileNavigator 类已进行了相应地改变。

 

用于文件导航里的新文件夹和文件图标。

在文件导航器里 (CFileNavigator 类) 添加新的文件夹和文件图标, 其模拟的是 Windows 10 操作系统的文件导航器。它们的时尚设计更适合开发库的图形界面, 但如果需要, 也可以使用定制版本。

 图例. 6. 用于文件导航里的新文件夹和文件图标。

图例. 6. 用于文件导航里的新文件夹和文件图标。 

 

这些图像可在本文结尾处获得。

 

结论

当前开发阶段的创建图形界面的库, 如下图所示。

 图例. 6. 当前开发阶段的库结构

图例. 6. 当前开发阶段的库结构


下一篇关于图形界面的文章将继续 Easy And Fast 库的开发。该库将扩展其它控件, 在开发 MQL 应用程序时这可能是必要的。现有的控件将得到改进, 并补充新的功能。 

您可以从下面下载 Easy And Fast 库的第四版 (集成编译 4)。 如您有兴趣, 您可以通过文章评论, 或通过私信提出一些任务的解决方案来促进该项目的更快开发。

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/2763

附加的文件 |
MQL5 编程基础: 终端中的全局变量 MQL5 编程基础: 终端中的全局变量

终端中的全局变量为开发复杂而可靠的EA交易提供了一种方便的工具,如果您掌握了全局变量,您就会发现在使用MQL5开发EA交易的时候它们是必不可少的。

图形界面 X: 简单快速开发库的更新 (版本 3) 图形界面 X: 简单快速开发库的更新 (版本 3)

在本文中,我们介绍下个版本的简单快速开发库(版本 3),它修改了一些缺陷,并且加入了新的功能,文章中有更加详细的内容。

海龟汤和海龟汤升级版的改进 海龟汤和海龟汤升级版的改进

本文介绍了来自琳达.布拉福德.瑞斯克(Linda Bradford Raschke)和劳伦斯.A.康纳斯(Laurence A. Connors)的《华尔街智慧:高胜算短线交易策略(Street Smarts: High Probability Short-Term Trading Strategies)》一书的两个交易策略,‘海龟汤’和‘海龟汤升级版’的原则规范。在书中描述的策略非常流行,但是有必要知道的是,作者是基于15年到20年的市场行为来开发它们的。

交易员生存诀窍: 若干测试的比较报告 交易员生存诀窍: 若干测试的比较报告

本文应对在四种不同的金融工具上同时启动智能交易系统测试。四个测试报告的最终比较在表格中提供, 类似于在线商店中陈列商品。附送礼包是为每个品种自动创建分布图表。