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

Anatoli Kazharski | 21 十月, 2016


目录


简介

第一篇文章, 图形界面 I: 库结构的准备工作 (第一章) 详细考虑了这个库的目标. 在每章末尾会有第一部分文章的完整链接列表. 在那里您可以下载当前开发阶段的库的完整版本. 文件必须按照它们在档案中的位置放到相同目录中.

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

从这个版本开始,开发库将只针对MetaTrader 5平台进行开发,这是因为在MetaTrader 4中有某些本质的结构差异和限制,然而,如果有紧急需要支持更早平台的开发库,可以在自由职业服务部分发布一个请求,寻找作者或者任何其他的开发人员,他们将很愿意并且能够做这些工作。 

 

更新

1. 直到现在,在前面文章中的MQL应用程序展示的都是如何在指标的子窗口中实现图形界面,当然,这种方法对一些简单的应用程序来说是足够了,但是如果可以在子窗口中为EA交易类型的程序创建图形界面就更好了,那样的话,就可能创建功能全面地交易面板,完全独立于主图表窗口了。在这种模式下工作将很简单: 价格图表和任何图表上的重要数据将会在应用程序的图形界面中保持一直打开。之后会介绍如何在简单快速开发库中实现此思路,

所要做的就是通过在MQL应用程序中包含一个占位指标(SubWindow.ex5)来建立"子窗口中的EA交易"模式,占位指标只是一个子窗口,而没有任何计算功能。但是因为现在没有办法通过MQL的方法知道指标是否包含在资源中,我们临时把EXPERT_IN_SUBWINDOW常量ID加到Defines.mqh文件中,它是为了当尝试取得指标的句柄而指标不在指定目录中时,消除记录中的错误,

默认值是false, 意思是这种模式是禁用的,而图形界面将在主图表窗口中创建 (参见以下代码)。

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- "子窗口中的EA交易" 模式
#define EXPERT_IN_SUBWINDOW false
...

如果需要在子窗口中创建图形界面,就要把数值设为true,并且在MQL应用程序的主文件中以资源形式包含占位指标。 

//+------------------------------------------------------------------+
//|                                                TestLibrary01.mq5 |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2016, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
//--- 为 "子窗口中的EA交易" 模式包含指标
#resource \\Indicators\\SubWindow.ex5
...

下面,我们讨论SubWindow.ex5指标,看它是如何与EA相关联的。SubWindow.ex5 指标的完整代码在下面提供。程序的属性指定了该指标是图表上的子窗口,缓冲区数量和序列是0,而垂直尺度上的最大值和最小值也是0, 

指标和EA交易之间会交换信息,问题是,指标"不知道"它在EA交易中选择后是使用哪种模式来绘制图形界面,它需要有消息传来,看开发人员是否决定使子窗口的高度固定,以及窗口高度的像素点数。为创建此消息,需要另一个ON_SUBWINDOW_CHANGE_HEIGHT事件 ID,这个ID也应该放到Defines.mqh文件中,这样就可以在使用开发库来创建图形界面的应用程序中使用了。 

为了了解什么时候EA需要生成事件来改变子窗口的高度: 在成功初始化指标后,在第一次自动调用::OnCalculate()函数(当prev_calculated参数为0), EA交易将会传递此消息。为了确定消息是来自SubWindow.ex5指标,除了ON_SUBWINDOW_CHANGE_HEIGHT事件ID,应用程序的名称将以字符串型(string)参数(sparam)的形式发送,此消息将在CWindow类中的处理函数中跟踪,如果在那里所有条件都匹配,指标就会发送回应,使用的是相同的(1)ID(ON_SUBWINDOW_CHANGE_HEIGHT), (2) 高度值是事件的长整型(long)参数(lparam) 以及(3)EA交易的名称。当收到这个消息后,它将在::OnChartEvent()函数中处理,在检查名称之后, 子窗口的高度会根据传入的数值进行设置。  

//+------------------------------------------------------------------+
//|                                                    SubWindow.mq5 |
//|                        Copyright 2016, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2016, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property indicator_separate_window
#property indicator_plots   0
#property indicator_buffers 0
#property indicator_minimum 0.0
#property indicator_maximum 0.0
//--- 程序名称
#define PROGRAM_NAME ::MQLInfoString(MQL_PROGRAM_NAME)
//--- 用于修改EA交易子窗口高度的事件ID
#define ON_SUBWINDOW_CHANGE_HEIGHT (25)
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                      |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- 指标的短名称
   ::IndicatorSetString(INDICATOR_SHORTNAME,PROGRAM_NAME);
//--- 初始化成功
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 去初始化                                                    |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//| 自定义指标迭代函数                              |
//+------------------------------------------------------------------+
int OnCalculate(const int    rates_total,
                const int    prev_calculated,
                const int    begin,
                const double &price[])
  {
//--- 如果初始化成功
   if(prev_calculated<1)
      //--- 向EA交易发送消息并取得子窗口的大小
      ::EventChartCustom(0,ON_SUBWINDOW_CHANGE_HEIGHT,0,0.0,PROGRAM_NAME);
u//---
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| ChartEvent 函数                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int    id,
                  const long   &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- 处理改变EA交易子窗口高度的事件
   if(id==CHARTEVENT_CUSTOM+ON_SUBWINDOW_CHANGE_HEIGHT)
     {
      //--- 只接收来自指定EA交易名称的消息
      if(sparam==PROGRAM_NAME)
         return;
      //--- 改变子窗口高度
      ::IndicatorSetInteger(INDICATOR_HEIGHT,(int)lparam);
     }
  }
//+------------------------------------------------------------------+

CWindow类的事件处理函数中要加入一段代码,如下方所示,当以ON_SUBWINDOW_CHANGE_HEIGHT为ID的事件到来时,需要经过多项检查。程序在以下状况下会退出:

  • 消息是由EA交易向指标发送的;
  • 程序不是一个EA交易;
  • EA交易子窗口固定高度模式没有设定。 
//+------------------------------------------------------------------+
//| 图表事件处理函数                            |
//+------------------------------------------------------------------+
void CWindow::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- 处理改变EA交易子窗口高度的事件
   if(id==CHARTEVENT_CUSTOM+ON_SUBWINDOW_CHANGE_HEIGHT)
     {
      //--- 如果消息是来自EA交易,退出
      if(sparam==PROGRAM_NAME)
         return;
      //--- 如果程序不是EA交易,退出
      if(CElement::ProgramType()!=PROGRAM_EXPERT)
         return;
      //--- 如果没有设置固定子窗口高度模式,退出
      if(!m_height_subwindow_mode)
         return;
      //--- 计算并设置子窗口的高度
      m_subwindow_height=(m_is_minimized)? m_caption_height+3 : m_bg_full_height+3;
      ChangeSubwindowHeight(m_subwindow_height);
      return;
     }
  }

如果所有的检查都通过了,就考虑窗口的当前状态(最小化/最大化)计算子窗口的高度。随后,调用CWindow::ChangeSubwindowHeight()方法,这个方法也有少许改动 (参见下方的代码),它的目的是,如果程序类型是EA交易,那么就SubWindow.ex5指标生成消息。 

//+------------------------------------------------------------------+
//| 改变指标子窗口的高度                          |
//+------------------------------------------------------------------+
void CWindow::ChangeSubwindowHeight(const int height)
  {
//--- 如果图形界面不在子窗口中或者程序是脚本类型
   if(CElement::m_subwin<=0 || CElement::m_program_type==PROGRAM_SCRIPT)
      return;
//--- 如果子窗口高度需要改变
   if(height>0)
     {
      //--- 如果程序是指标类型
      if(CElement::m_program_type==PROGRAM_INDICATOR)
        {
         if(!::IndicatorSetInteger(INDICATOR_HEIGHT,height))
            ::Print(__FUNCTION__," > 修改指标子窗口高度失败!错误编号: ",::GetLastError());
        }
      //--- 如果程序是EA交易类型
      else
        {
         //--- 发送消息给 SubWindow.ex5 指标,通知它窗口高度必须改变
         ::EventChartCustom(m_chart_id,ON_SUBWINDOW_CHANGE_HEIGHT,(long)height,0,PROGRAM_NAME);
        }
     }
  }

开发库的引擎(CWndEvents类)也需要增加内容以用来在开发EA交易类型的MQL图形界面程序时确定,检查和调整子窗口编号。"子窗口EA交易"模式的代码应该加到CWndEvents::DetermineSubwindow()方法中,下面的代码展示了这个方法的精简版本。进入代码段执行的条件是程序类型为"EA交易",下面要做的是检查是否启用了"子窗口EA交易"的模式。如果启用,就取得SubWindow.ex5指标的句柄,如果没有问题,则首先取得图表窗口中当前子窗口的数量,取得的值就是SubWindow.ex5指标子窗口的编号,也就是通过::ChartIndicatorAdd()设定的。然后,如果在设置子窗口时没有出错,它就保存 (1)EA交易子窗口的编号, (2) 当前子窗口的数量 以及(3)SubWindow.ex5指标的短名称。  

//+------------------------------------------------------------------+
//| 用于事件处理的类                        |
//+------------------------------------------------------------------+
class CWndEvents : public CWndContainer
  {
protected:
   //--- EA交易子窗口的句柄
   int               m_subwindow_handle;
   //--- EA交易子窗口的名称
   string            m_subwindow_shortname;
   //--- 在设置EA子窗口后子窗口的数量
   int               m_subwindows_total;
  };
//+------------------------------------------------------------------+
//| 识别子窗口编号                               |
//+------------------------------------------------------------------+
void CWndEvents::DetermineSubwindow(void)
  {
//--- 如果程序类型是脚本,退出
//--- 重置最后的错误代码

//--- 如果程序类型是"EA交易"
   if(PROGRAM_TYPE==PROGRAM_EXPERT)
     {
      //--- 如果EA的图形界面要求放在主窗口中,退出
      if(!EXPERT_IN_SUBWINDOW)
         return;
      //--- 取得占位指标(空白子窗口)的句柄
      m_subwindow_handle=iCustom(Symbol(),Period(),"::Indicators\\SubWindow.ex5");
      //--- 如果没有这样的指标,在记录中写错误报告
      if(m_subwindow_handle==INVALID_HANDLE)
         ::Print(__FUNCTION__," > 在以下目录获取指标句柄出错 ::Indicators\\SubWindow.ex5 !");
      //--- 如果取得了句柄,则指标存在,在应用程序中以资源形式包含,
      // 而这表明应用程序的图形界面必须放在子窗口中。
      else
        {
         //--- 取得图表中子窗口的数量
         int subwindows_total=(int)::ChartGetInteger(m_chart_id,CHART_WINDOWS_TOTAL);
         //--- 为EA交易的图形界面设置子窗口
         if(::ChartIndicatorAdd(m_chart_id,subwindows_total,m_subwindow_handle))
           {
            //--- 保存子窗口编号和图表上当前子窗口的数量
            m_subwin           =subwindows_total;
            m_subwindows_total =subwindows_total+1;
            //--- 取得和保存EA交易子窗口的短名称
            m_subwindow_shortname=::ChartIndicatorName(m_chart_id,m_subwin,0);
           }
         //--- 如果子窗口没有设置
         else
            ::Print(__FUNCTION__," > 设置EA交易子窗口错误!错误编号: ",::GetLastError());
        }
      u//---
      return;
     }
//--- 识别指标窗口
//--- 如果识别窗口编号错误,退出
//--- 如果这不是图表的主窗口
...
  }

还需要确认的是,当在图表子窗口中增加或者删除其它指标时,EA子窗口的编号要进行调整,以便其图形界面正确运行。另外,让我们考虑,如果用户删除了EA交易的子窗口,EA交易也要删除,并且在"工具箱"窗口的"专家"页面的记录中留下消息,指出从图表上删除的原因。为此我们实现了 CWndEvents::CheckExpertSubwindowNumber() 方法。在程序类型为"EA交易"时会调用此方法。如果检查通过,就会计算图表窗口中子窗口的数量,如果发现子窗口数量没有改变,程序就退出此方法。 

然后就需要在循环中找到EA交易的子窗口,如果它存在 - 检查它的子窗口编号是否已经改变。子窗口编号可能会因为增加或者删除了位于独立子窗口中的指标引起。如果子窗口编号已经改变,就需要保存它的编号并在主窗口的所有控件中更新它的值。 

如果子窗口没有找到,就只有一个原因: 它已经被删除了。遇到这种情况, EA交易也需要从图表上删除。 

class CWndEvents : public CWndContainer
  {
private:
   //--- 检查和更新EA交易子窗口的编号
   void              CheckExpertSubwindowNumber(void);
  };
//+------------------------------------------------------------------+
//| 检查和更新EA交易子窗口的编号                   |
//+------------------------------------------------------------------+
void CWndEvents::CheckExpertSubwindowNumber(void)
  {
//--- 如果不是EA交易,退出
   if(PROGRAM_TYPE!=PROGRAM_EXPERT)
      return;
//--- 取得图表上子窗口的数量
   int subwindows_total=(int)::ChartGetInteger(m_chart_id,CHART_WINDOWS_TOTAL);
//--- 如果子窗口数量和指标数量没有改变,退出
   if(subwindows_total==m_subwindows_total)
      return;
//--- 保存当前的子窗口数量
   m_subwindows_total=subwindows_total;
//--- 用于检查EA交易子窗口是否存在
   bool is_subwindow=false;
//--- 寻找EA交易的子窗口
   for(int sw=0; sw<subwindows_total; sw++)
     {
      //--- 如果找到了EA交易的子窗口,退出循环
      if(is_subwindow)
         break;
      //--- 该窗口/子窗口中的指标数量
      int indicators_total=::ChartIndicatorsTotal(m_chart_id,sw);
      //--- 在窗口中迭代所有指标 
      for(int i=0; i<indicators_total; i++)
        {
         //--- 取得指标的短名称
         string indicator_name=::ChartIndicatorName(m_chart_id,sw,i);
         //--- 如果这不是EA交易的子窗口,就查看下一个
         if(indicator_name!=m_subwindow_shortname)
            continue;
         //--- 标记此EA有子窗口
         is_subwindow=true;
         //--- 如果子窗口编号改变,则 
         //  需要在主表单的所有控件中保存新的编号
         if(sw!=m_subwin)
           {
            //--- 保存子窗口编号
            m_subwin=sw;
            //--- 在界面中主表单的所有控件中保存
            int elements_total=CWndContainer::ElementsTotal(0);
            for(int e=0; e<elements_total; e++)
               m_wnd[0].m_elements[e].SubwindowNumber(m_subwin);
           }
         u//---
         break;
        }
     }
//--- 如果没有找到EA交易子窗口,删除EA交易
   if(!is_subwindow)
     {
      ::Print(__FUNCTION__," > 删除了EA交易的子窗口导致需要删除EA交易!");
      //--- 在图表中删除EA交易
      ::ExpertRemove();
     }
  }

 

2. 前面版本的开发库引入了自动改变表单宽度的功能,让我们为高度加入类似的特性。把 ON_WINDOW_CHANGE_SIZE ID用于改变表单大小的事件并不适合解决这个任务,因为修改宽度和高度将作为相互独立的事件来处理。所以,在 Defines.mqh 文件中将要有两个独立的ID,而不是ON_WINDOW_CHANGE_SIZE, 如下面的代码所示。在其它的库文件中已经进行了相关的ID替换。  

#define ON_WINDOW_CHANGE_XSIZE     (3)  // 在X轴上改变了窗口的大小
#define ON_WINDOW_CHANGE_YSIZE     (4)  // 在Y轴上改变了窗口的大小

已经加上了 CWindow::ChangeWindowHeight() 方法用于改变表单的高度。当在改变表单大小后调用此方法,会在最后生成有关操作的消息。 

//+------------------------------------------------------------------+
//| 用于创建控件的表单类                           |
//+------------------------------------------------------------------+
class CWindow : public CElement
  {
public:
   //--- 管理大小
   void              ChangeWindowHeight(const int height);
  };
//+------------------------------------------------------------------+
//| 改变窗口的高度                          |
//+------------------------------------------------------------------+
void CWindow::ChangeWindowHeight(const int height)
  {
//--- 如果高度没有改变,退出
   if(height==m_bg.YSize())
      return;
//--- 如果窗口是最小化的,退出
   if(m_is_minimized)
      return;
//--- 更新背景的高度
   CElement::YSize(height);
   m_bg.YSize(height);
   m_bg.Y_Size(height);
   m_bg_full_height=height;
//--- 生成窗口大小已经改变的消息
   ::EventChartCustom(m_chart_id,ON_WINDOW_CHANGE_YSIZE,(long)CElement::Id(),0,"");
  }

为了使表单高度自动变化,用户在开发MQL应用程序的时候应该使用CElement::AutoYResizeMode()方法设置对应的模式: 

//+------------------------------------------------------------------+
//| 控件基类                            |
//+------------------------------------------------------------------+
class CElement
  {
protected:
   //--- 控件自动改变大小的模式
   bool              m_auto_yresize_mode;
   u//---
public:
   //--- (1) 控件自动改变高度的模式
   bool              AutoYResizeMode(void)                     const { return(m_auto_yresize_mode);          }
   void              AutoYResizeMode(const bool flag)                { m_auto_yresize_mode=flag;             }
  };

现在,如果表单自动改变大小的模式被启用,如果图表窗口大小有改变,表单的事件处理函数会根据CHARTEVENT_CHART_CHANGE事件调整表单的大小。这对指标子窗口和图表窗口都适用。可以启用其中一种模式,也可以同时启用两种模式。 

//--- 图表属性改变事件
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      //--- 如果按钮被松开
      if(m_clamping_area_mouse==NOT_PRESSED)
        {
         //--- 取得窗口大小
         SetWindowProperties();
         //--- 调整坐标
         UpdateWindowXY(m_x,m_y);
        }
      //--- 如果启用了自动改变宽度的模式
      if(CElement::AutoXResizeMode())
         ChangeWindowWidth(m_chart.WidthInPixels()-2);
      //--- 如果启用了自动改变高度的模式
      if(CElement::AutoYResizeMode())
         ChangeWindowHeight(m_chart.HeightInPixels(m_subwin)-3);
      u//---
      return;
     }

 

3. 自动改变高度的模式也适用于开发库中的所有控件。但是在当前的版本中,它只使用于以下列表中的控件:

  • CTabs – 简单页面。
  • CIconTabs – 带有图标的页面。
  • CCanvasTable – 绘制型表格。
  • CLineGraph – 线形图表。

与之前在CElement类中加入CElement::ChangeWidthByRightWindowSide()虚拟方法用于改变控件的宽度一样,让我们加上对应的方法用于自动高度调节。另外,让我们创建方法设置距离表单底边的距离,就像之前加的当修改宽度时设置和表单右侧边缘距离一样。 

class CElement
  {
protected:
   //--- 在自动改变控件宽度/高度的模式下和表单右侧/底部边界之间的距离
   int               m_auto_xresize_right_offset;
   int               m_auto_yresize_bottom_offset;
   u//---
public:
   //--- 获取/设置与表单底部边界的距离
   int               AutoYResizeBottomOffset(void)             const { return(m_auto_yresize_bottom_offset); }
   void              AutoYResizeBottomOffset(const int offset)       { m_auto_yresize_bottom_offset=offset;  }
   u//---
public:
   //--- 根据窗口的底部边界改变高度
   virtual void      ChangeHeightByBottomWindowSide(void) {}
  };

每个控件都有自己的ChangeWidthByRightWindowSide()方法的实现, 它会考虑到自身的特性和模式,作为实例,下面显示了CCanvasTable类中这个方法的代码: 

//+------------------------------------------------------------------+
//| 根据窗口底部边界改变高度                           |
//+------------------------------------------------------------------+
void CCanvasTable::ChangeHeightByBottomWindowSide(void)
  {
//--- 如果启用了表单底部锚点的模式,退出  
   if(m_anchor_bottom_window_side)
     return;
//--- 坐标
   int y=0;
//--- 大小
   int x_size=(m_auto_xresize_mode)? m_wnd.X2()-m_area.X()-m_auto_xresize_right_offset : m_x_size;
   int y_size=m_wnd.Y2()-m_area.Y()-m_auto_yresize_bottom_offset;
//--- 如果大小小于指定值,退出
   if(y_size<60)
     return;
//--- 设置表格背景新的大小
   ChangeMainSize(x_size,y_size);
//--- 计算表格大小
   CalculateTableSize();
//--- 检查滚动条是否存在
   bool is_scrollh=!(m_table_visible_x_size>=m_table_x_size);
   bool is_scrollv=!(m_table_visible_y_size>=m_table_y_size);
//--- 相对于滚动条存在时的距离
   int offset=(is_scrollh || (!is_scrollh && !is_scrollv))? 0 : 2;
//--- 计算和设置水平滚动条新的坐标
   y=m_area.Y2()-m_scrollh.ScrollWidth();
   m_scrollh.YDistance(y);
//--- 为新的大小初始化水平滚动条
   m_scrollh.Reinit(m_table_x_size,m_table_visible_x_size);
//--- 计算和设置垂直滚动条的高度
   m_scrollv.Reinit(m_table_y_size,m_table_visible_y_size-offset);
   m_scrollv.ChangeYSize((is_scrollh)? m_table_visible_y_size+2 : m_table_visible_y_size);
//--- 如果不需要垂直滚动条
   if(!is_scrollv)
     {
      //--- 隐藏垂直滚动条
      m_scrollv.Hide();
      //--- 改变水平滚动条的宽度
      m_scrollh.ChangeXSize(m_area.XSize());
     }
   else
     {
      //--- 显示垂直滚动条
      if(CElement::IsVisible())
         m_scrollv.Show();
      //--- 计算和改变水平滚动条的宽度
      m_scrollh.ChangeXSize(m_area.XSize()-m_scrollv.ScrollWidth()+1);
     }
//--- 管理水平滚动条是否可见
   if(CElement::IsVisible())
     {
      if(!is_scrollh) m_scrollh.Hide();
      else m_scrollh.Show();
     }
//--- 改变表格的大小
   ChangeTableSize(m_table_x_size,m_table_y_size,m_table_visible_x_size,m_table_visible_y_size-offset);
//--- 绘制表格
   DrawTable();
//--- 更新对象的位置
   Moving(m_wnd.X(),m_wnd.Y());
  }

为了它能够正确工作,需要在CWndEvents类中做一些增加和改变,因为改变大小的事件(宽度和高度)现在是使用不同的ID生成的,就需要两个独立方法来处理它们。 

//+------------------------------------------------------------------+
//| 用于事件处理的类                        |
//+------------------------------------------------------------------+
class CWndEvents : public CWndContainer
  {
private:
   //--- 处理改变窗口大小
   bool              OnWindowChangeXSize(void);
   bool              OnWindowChangeYSize(void);
  };
//+------------------------------------------------------------------+
//| ON_WINDOW_CHANGE_XSIZE 事件                   |
//+------------------------------------------------------------------+
bool CWndEvents::OnWindowChangeXSize(void)
  {
//--- 如果信号是 "改变控件大小"
   if(m_id!=CHARTEVENT_CUSTOM+ON_WINDOW_CHANGE_XSIZE)
      return(false);
//--- 活动窗口的索引
   int awi=m_active_window_index;
//--- 如果窗口ID匹配
   if(m_lparam!=m_windows[awi].Id())
      return(true);
//--- 改变所有控件的大小,表单除外
   int elements_total=CWndContainer::ElementsTotal(awi);
   for(int e=0; e<elements_total; e++)
     {
      //--- 如果它是窗口,就转到下一个
      if(m_wnd[awi].m_elements[e].ClassName()=="CWindow")
         continue;
      //--- 如果启用了模式,调整宽度
      if(m_wnd[awi].m_elements[e].AutoXResizeMode())
         m_wnd[awi].m_elements[e].ChangeWidthByRightWindowSide();
     }
u//---
   return(true);
  }
//+------------------------------------------------------------------+
//| ON_WINDOW_CHANGE_YSIZE 事件                    |
//+------------------------------------------------------------------+
bool CWndEvents::OnWindowChangeYSize(void)
  {
//--- 如果信号是 "改变控件大小"
   if(m_id!=CHARTEVENT_CUSTOM+ON_WINDOW_CHANGE_YSIZE)
      return(false);
//--- 活动窗口的索引
   int awi=m_active_window_index;
//--- 如果窗口ID匹配
   if(m_lparam!=m_windows[awi].Id())
      return(true);
//--- 改变所有控件的大小,表单除外
   int elements_total=CWndContainer::ElementsTotal(awi);
   for(int e=0; e<elements_total; e++)
     {
      //--- 如果它是窗口,就转到下一个
      if(m_wnd[awi].m_elements[e].ClassName()=="CWindow")
         continue;
      //--- 如果启用了模式,调整高度
      if(m_wnd[awi].m_elements[e].AutoYResizeMode())
         m_wnd[awi].m_elements[e].ChangeHeightByBottomWindowSide();
     }
u//---
   return(true);
  }

CWndEvents::OnWindowChangeXSize() 和 CWndEvents::OnWindowChangeYSize() 方法是在用于处理自定义事件的通用CWndEvents::ChartEventCustom()方法中调用的,下面的代码展示了这个方法的精简版本。 

//+------------------------------------------------------------------+
//| CHARTEVENT_CUSTOM 事件                        |
//+------------------------------------------------------------------+
void CWndEvents::ChartEventCustom(void)
  {
//--- 如果信号是最小化表单
//--- 如果信号是最大化表单

//--- 如果信号是在X轴上改变控件的大小
   if(OnWindowChangeXSize())
      return;
//--- 如果信号是在Y轴上改变控件的大小
   if(OnWindowChangeYSize())
      return;

//--- 如果信号是需要隐藏菜单项下的上下文菜单
//--- 如果信号是隐藏所有上下文菜单

//--- 如果信号是打开对话框窗口
//--- 如果信号是关闭一个对话框窗口
//--- 如果信号是清除指定表单上所有元件的颜色
//--- 如果信号是重设鼠标左键点击优先级
//--- 如果信号是恢复鼠标左键点击优先级
  }

现在,当改变图表窗口的大小时,如果启用了改变表单和控件大小的模式,它们将自动改变大小。 

下面的屏幕截图显示了在一个MQL应用程序图形界面中的例子,它是在图表主窗口中创建的。对于应用程序窗口(CWindow), 设置了根据图表窗口自动调节宽度和高度的模式,对于 «主菜单» (CMenuBar) 和 «状态栏» (CStatusBar) 控件,设置了根据MQL应用程序窗口,自动调节宽度和高度的模式,对于«页面» (CTabs) 和 «绘制型表格» (CCanvasTable) 控件,指定了根据MQL应用程序底部边界自动调整宽度和高度的模式。

图 1. 终端窗口大小的最小值。MQL应用程序启用了自动调整大小模式的图形界面。 

图 1. 终端窗口大小的最小值。MQL应用程序启用了自动调整大小模式的图形界面。


如果终端窗口的大小改变,而以上所说的模式被启用,MQL应用程序的图形界面也会对应地变化。

图 2. 当终端窗口大小改变,MQL应用程序的图形界面也会调整大小。 

图 2. 当终端窗口大小改变,MQL应用程序的图形界面也会调整大小。


4. 当设计表单大小可变的图形界面时,经常需要使控件位于应用程序窗口的右侧或者底部,之前,有个选项可以简单指定控件相对于表单左上角的锚点的坐标,现在,有四个选项用于给控件相对于表单定位:

  • 左上;
  • 右上;
  • 右下;
  • 左下。

为了实现这个想法,让我们在控件基类(CElement)中增加两个方法来设置/获取控件位置在表单右侧和底部的模式: 

class CElement
  {
protected:
   //--- 控件锚点在表单的右方和下方
   bool              m_anchor_right_window_side;
   bool              m_anchor_bottom_window_side;
   u//---
public:
   //--- (取得/设置)控件锚点的模式  窗口的(1)右侧 和 (2) 底部边界
   bool              AnchorRightWindowSide(void)               const { return(m_anchor_right_window_side);   }
   void              AnchorRightWindowSide(const bool flag)          { m_anchor_right_window_side=flag;      }
   bool              AnchorBottomWindowSide(void)              const { return(m_anchor_bottom_window_side);  }
   void              AnchorBottomWindowSide(const bool flag)         { m_anchor_bottom_window_side=flag;     }
  };

为了一切可以根据这些模式而工作,在所有的库控件的类里已经做了改动,作为示例,这里有用于为 «复选框» (CCheckBox) 控件创建文本标签的方法代码,注意为图形对象计算坐标和偏移的代码行。 

//+------------------------------------------------------------------+
//| 创建复选框的文字标签                                           |
//+------------------------------------------------------------------+
bool CCheckBox::CreateLabel(void)
  {
//--- 构造对象名称
   string name=CElement::ProgramName()+"_checkbox_lable_"+(string)CElement::Id();
//--- 坐标
   int x =(m_anchor_right_window_side)? m_x-m_label_x_gap : m_x+m_label_x_gap;
   int y =(m_anchor_bottom_window_side)? m_y-m_label_y_gap : m_y+m_label_y_gap;
//--- 根据状态的文字颜色
   color label_color=(m_check_button_state) ? m_label_color : m_label_color_off;
//--- 设置对象
   if(!m_label.Create(m_chart_id,name,m_subwin,x,y))
      return(false);
//--- 设置属性
   m_label.Description(m_label_text);
   m_label.Font(FONT);
   m_label.FontSize(FONT_SIZE);
   m_label.Color(label_color);
   m_label.Corner(m_corner);
   m_label.Anchor(m_anchor);
   m_label.Selectable(false);
   m_label.Z_Order(m_zorder);
   m_label.Tooltip("\n");
//--- 到边缘的距离
   m_label.XGap((m_anchor_right_window_side)? x : x-m_wnd.X());
   m_label.YGap((m_anchor_bottom_window_side)? y : y-m_wnd.Y());
//--- 初始化渐变色数组
   CElement::InitColorArray(label_color,m_label_color_hover,m_label_color_array);
//--- 保存对象指针
   CElement::AddToArray(m_label);
   return(true);
  }

在开发库中,所有控件的Moving()方法现在都要考虑到控件的位置模式,作为示例,下面的代码展示了CCheckBox::Moving()方法的代码: 

//+------------------------------------------------------------------+
//| 移动控件                                                     |
//+------------------------------------------------------------------+
void CCheckBox::Moving(const int x,const int y)
  {
//--- 如果元件是隐藏的, 退出
   if(!CElement::IsVisible())
      return;
//--- 如果锚点在右边
   if(m_anchor_right_window_side)
     {
      //--- 在元件栏位中保存坐标
      CElement::X(m_wnd.X2()-XGap());
      //--- 在对象栏位中保存坐标
      m_area.X(m_wnd.X2()-m_area.XGap());
      m_check.X(m_wnd.X2()-m_check.XGap());
      m_label.X(m_wnd.X2()-m_label.XGap());
     }
   else
     {
      //--- 在对象栏位中保存坐标
      CElement::X(x+XGap());
      //--- 在对象栏位中保存坐标
      m_area.X(x+m_area.XGap());
      m_check.X(x+m_check.XGap());
      m_label.X(x+m_label.XGap());
     }
//--- 如果锚点在底部
   if(m_anchor_bottom_window_side)
     {
      //--- 在元件栏位中保存坐标
      CElement::Y(m_wnd.Y2()-YGap());
      //--- 在对象栏位中保存坐标
      m_area.Y(m_wnd.Y2()-m_area.YGap());
      m_check.Y(m_wnd.Y2()-m_check.YGap());
      m_label.Y(m_wnd.Y2()-m_label.YGap());
     }
   else
     {
      //--- 在对象栏位中保存坐标
      CElement::Y(y+YGap());
      //--- 在对象栏位中保存坐标
      m_area.Y(y+m_area.YGap());
      m_check.Y(y+m_check.YGap());
      m_label.Y(y+m_label.YGap());
     }
//--- 更新图形对象的坐标
   m_area.X_Distance(m_area.X());
   m_area.Y_Distance(m_area.Y());
   m_check.X_Distance(m_check.X());
   m_check.Y_Distance(m_check.Y());
   m_label.X_Distance(m_label.X());
   m_label.Y_Distance(m_label.Y());
  }

为了更加清晰,下面的图概要显示了所有可能的位置模式组合以及控件的自动改变大小,这是一个抽象的例子,其中表单(参见第九列«结果»)使用黑色粗边框的白色长方形表示,并且大小为400 x 400像素点, 而作为控件 — 灰色长方形,大小为 200 x 200像素点,相对坐标和控件的大小在每个组合中都有显示,横线表示在那种情况下不一定要设置大小 (如果启用了自动改变大小的模式)。 

图 3. 表格列举了控件位置与自动改变大小的不同组合选项。 

图 3. 表格列举了控件位置与自动改变大小的不同组合选项。


5. 增加了ON_CLICK_TAB事件ID,用于在CTabsCIconTabs类中生成切换页面事件。 

//+------------------------------------------------------------------+
//|                                                      Defines.mqh |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
...
#define ON_CLICK_TAB               (27) // 切换页面

使用ON_CLICK_TAB为ID的事件现在可以在自定义类的处理函数中跟踪,这使得可以更好地管理图形界面的外观。 


6. 在所有控件的Show()方法中加入了坐标的强制更新,作为示例,下面的代码展示了CSimpleButton 类中的这个方法 (用黄色突出显示的代码行):

//+------------------------------------------------------------------+
//| 显示按钮                                                 |
//+------------------------------------------------------------------+
void CSimpleButton::Show(void)
  {
//--- 如果元件已经可见, 退出
   if(CElement::IsVisible())
      return;
//--- 使得所有对象可见
   m_button.Timeframes(OBJ_ALL_PERIODS);
//--- 可见状态
   CElement::IsVisible(true);
//--- 更新对象的位置
   Moving(m_wnd.X(),m_wnd.Y());
  }

在某些情况下,经常使用的MQL应用程序图形界面可能会引起它的控件位置不正确,现在这个问题解决了。


7. 在CWindow类中加入了用于取得按钮指针的方法: 

//+------------------------------------------------------------------+
//| 用于创建控件的表单类                           |
//+------------------------------------------------------------------+
class CWindow : public CElement
  {
public:
   //--- 返回表单按钮指针的方法
   CBmpLabel        *GetCloseButtonPointer(void)                             { return(::GetPointer(m_button_close));   }
   CBmpLabel        *GetRollUpButtonPointer(void)                            { return(::GetPointer(m_button_unroll));  }
   CBmpLabel        *GetUnrollButtonPointer(void)                            { return(::GetPointer(m_button_rollup));  }
   CBmpLabel        *GetTooltipButtonPointer(void)                           { return(::GetPointer(m_button_tooltip)); }
  };

这样,开发库的用户现在能够在MQL应用程序中图形界面已经创建后的生命周期中,也可以改变这些图形对象的属性了。比如,如果按钮的工具提示预先定义了默认值,现在它们可以被独立设置,这对创建多语言的MQL应用程序很有用。


8. CTable 类中的修正。在创建控件的主方法中加入了一项检查 (参见下面的代码),可见部分不再需要是通用的,现在,如果用户在设置表格属性时出了错,这些数值会自动修正。 

//+------------------------------------------------------------------+
//| 创建编辑框型表格                                             |
//+------------------------------------------------------------------+
bool CTable::CreateTable(const long chart_id,const int subwin,const int x,const int y)
  {
//--- 如果没有表单指针
   if(!CElement::CheckWindowPointer(::CheckPointer(m_wnd)))
      return(false);
//--- 可见部分不再需要通用
   m_visible_rows_total    =(m_visible_rows_total>m_rows_total)? m_rows_total : m_visible_rows_total;
   m_visible_columns_total =(m_visible_columns_total>m_columns_total)? m_columns_total : m_visible_columns_total;
//--- 初始化变量
   m_id       =m_wnd.LastId()+1;
   m_chart_id =chart_id;
   m_subwin   =subwin;
   m_x        =x;
   m_y        =y;
   m_x_size   =(m_x_size<1 || m_auto_xresize_mode)? (m_anchor_right_window_side)? m_wnd.X2()-m_x+m_x_size-(m_wnd.X2()-m_x)+1-m_auto_xresize_right_offset : m_wnd.X2()-m_x-m_auto_xresize_right_offset : m_x_size;
   m_y_size   =m_row_y_size*m_visible_rows_total-(m_visible_rows_total-1)+2;
//--- 到边缘的距离
   CElement::XGap((m_anchor_right_window_side)? m_x : m_x-m_wnd.X());
   CElement::YGap((m_anchor_bottom_window_side)? m_y : m_y-m_wnd.Y());
//--- 创建表格
   if(!CreateArea())
      return(false);
   if(!CreateCells())
      return(false);
   if(!CreateScrollV())
      return(false);
   if(!CreateScrollH())
      return(false);
//--- 如果窗口是对话框或者是最小化状态,隐藏元件
   if(m_wnd.WindowType()==W_DIALOG || m_wnd.IsMinimized())
      Hide();
u//---
   return(true);
  }


用于测试更新的应用程序

为了测试,让我们稍微改一下前面文章中的MQL应用程序,来使得它可以用于演示本文中的内容。在“SubWindow”指标的子窗口中创建EA交易。图形界面中主窗口的大小将根据子窗口的大小自动调整。子窗口的高度将可以人工修改,为此,当调用CWindow::RollUpSubwindowMode()方法时,应该传入'false'(以下代码中用绿色突出显示的部分)。 

下面的代码页演示了如何在应用程序图形界面的主窗口中访问和管理按钮的属性。在这种情况下,示例展示了设置工具提示

//+------------------------------------------------------------------+
//| 创建控件表单                                                 |
//+------------------------------------------------------------------+
bool CProgram::CreateWindow(const string caption_text)
  {
//--- 在窗口数组中加入窗口指针
   CWndContainer::AddWindow(m_window);
//--- 坐标
   int x=1;
   int y=1;
//--- 属性
   m_window.Movable(false);
   m_window.UseRollButton();
   m_window.AutoXResizeMode(true);
   m_window.AutoYResizeMode(true);
   m_window.RollUpSubwindowMode(false,false);
//--- 创建表单
   if(!m_window.CreateWindow(m_chart_id,m_subwin,caption_text,x,y))
      return(false);
//--- 设置工具提示
   m_window.GetCloseButtonPointer().Tooltip("Close program");
   m_window.GetUnrollButtonPointer().Tooltip("Unroll");
   m_window.GetRollUpButtonPointer().Tooltip("Roll up");
   return(true);
  }

在第一个页面,所有的控件都以表单的右侧为锚点 (参见下面的屏幕截图)。如果表单的宽度改变,它们与右侧边界的距离是不变的。 

 图 4. 第一个页面的控件以表单右侧为锚点。

图 4. 第一个页面的控件以表单右侧为锚点。


下面的代码是创建"简单按钮"(CSimpleButton)控件的示例代码。为了按照右侧边界固定控件,只要调用AnchorRightWindowSide() 方法, 并把它传给数值 true。 

//+------------------------------------------------------------------+
//| 创建简单按钮 1                           |
//+------------------------------------------------------------------+
bool CProgram::CreateSimpleButton1(const int x_gap,const int y_gap,string button_text)
  {
//--- 保存窗口指针
   m_simple_button1.WindowPointer(m_window);
//--- 附加到第一个页面
   m_tabs.AddToElementsArray(0,m_simple_button1);
//--- 坐标
   int x=m_window.X()+x_gap;
   int y=m_window.Y()+y_gap;
//--- 在创建之前设置属性
   m_simple_button1.ButtonXSize(140);
   m_simple_button1.BackColor(C'255,140,140');
   m_simple_button1.BackColorHover(C'255,180,180');
   m_simple_button1.BackColorPressed(C'255,120,120');
   m_simple_button1.AnchorRightWindowSide(true);
//--- 创建一个按钮
   if(!m_simple_button1.CreateSimpleButton(m_chart_id,m_subwin,button_text,x,y))
      return(false);
//--- 把元件指针加到库中
   CWndContainer::AddToElementsArray(0,m_simple_button1);
   return(true);
  }

第二个页面只有一个绘制型表格(CCanvasTable), 它允许当子窗口宽度和高度发生变化,它将会调整表单的大小。

 图 5. 调整到表单大小的绘制型表格。

图 5. 调整到表单大小的绘制型表格。


为了一切能够按预期进行,需要 使用 AutoXResizeMode() 和 AutoYResizeMode() 方法来启用模式。通过使用 AutoXResizeRightOffset() 和AutoYResizeBottomOffset() 方法,可能根据表单右侧和底部的控件调整距离控件右侧和底部边界的距离,在这种情况下,距离右侧边界的偏移是1个像素,而从底部看有25个像素点(参见下方代码)。 

//+------------------------------------------------------------------+
//| 创建一个绘制型表格                                          |
//+------------------------------------------------------------------+
bool CProgram::CreateCanvasTable(const int x_gap,const int y_gap)
  {
#define COLUMNS3_TOTAL 15
#define ROWS3_TOTAL    30
//--- 保存表单的指针
   m_canvas_table.WindowPointer(m_window);
//--- 附加到第二个页面
   m_tabs.AddToElementsArray(1,m_canvas_table);
//--- 坐标
   int x=m_window.X()+x_gap;
   int y=m_window.Y()+y_gap;
//--- 表格列宽度的数组
   int width[COLUMNS3_TOTAL];
   ::ArrayInitialize(width,70);
   width[0]=100;
   width[1]=90;
//--- 在列中文字对齐方式的数组
   ENUM_ALIGN_MODE align[COLUMNS3_TOTAL];
   ::ArrayInitialize(align,ALIGN_CENTER);
   align[0]=ALIGN_RIGHT;
   align[1]=ALIGN_RIGHT;
   align[2]=ALIGN_LEFT;
//--- 在创建之前设置属性
   m_canvas_table.XSize(400);
   m_canvas_table.YSize(200);
   m_canvas_table.TableSize(COLUMNS3_TOTAL,ROWS3_TOTAL);
   m_canvas_table.TextAlign(align);
   m_canvas_table.ColumnsWidth(width);
   m_canvas_table.GridColor(clrLightGray);
   m_canvas_table.AutoXResizeMode(true);
   m_canvas_table.AutoYResizeMode(true);
   m_canvas_table.AutoXResizeRightOffset(1);
   m_canvas_table.AutoYResizeBottomOffset(25);
//--- 发布表格数据
   for(int c=0; c<COLUMNS3_TOTAL; c++)
      for(int r=0; r<ROWS3_TOTAL; r++)
         m_canvas_table.SetValue(c,r,string(c)+":"+string(r));
//--- 创建控件
   if(!m_canvas_table.CreateTable(m_chart_id,m_subwin,x,y))
      return(false);
//--- 把对象加到对象组的通用数组中
   CWndContainer::AddToElementsArray(0,m_canvas_table);
   return(true);
  }

第三个页面上将会放置一个线形图表 (CLineGraph),另外也能够根据表单大小而自主进行修正。

图 6. «线形图形» 控件可以调整到表单的边上。 

图 6. «线形图形» 控件可以调整到表单的边上。


第四页和第五页演示了许多其他库控件展示的距离右侧的锚点。

图 7. 第四个页面上的控件。 

图 7. 第四个页面上的控件。


图 8. 第五个页面上的控件。 

图 8. 第五个页面上的控件。


本文中使用的测试程序可以使用下面的链接下载来做更多学习和研究。 

 

结论

当前阶段的图形界面开发库看起来如以下结构图所示。

图 9. 当前开发阶段的库结构。 

图 9. 当前开发阶段的库结构。


在下面的版本中,开发库会继续扩展,加入更多可能在开发MQL应用程序中需要使用的控件。已有的控件将继续提高并加入新的功能。

以下您可以下载第三版的简单快速开发库。如果您对使用这些文件中的资料有问题,可以参考系列文章中的详细描述或者在文章的留言处问问题。