带有图形用户界面的通用震荡指标

Dmitry Fedoseev | 18 一月, 2017


目录

简介

一个用于交易系统的合适指标通常是通过在图表上使用各种参数监视各种指标后选出来的,如果您通过导航窗口来拖拽测试每个指标,并且每次都通过指标的属性窗口来改变它的参数,这个过程将花费很多时间,有一种方法可以加速这个过程。

它包含了创建一个图形界面来直接从图表上访问,使得用户快速修改指标参数并马上看到新的结果。可以通过在一个带有图形界面的通用指标中组合各种指标来实现指标的快速切换。

分析问题

创建一个通用指标的任务并不是很难,它需要一点面向对象的编程: 一个基类和一些相同类型的子类。

每个特定指标的参数将通过子类的构造函数来传递。在这种情况下,当创建一个对象时,MetaEditor 会有一个参数列表的提示,它将非常有助于开发过程(图1)

 
图 1. 当创建一个对象时构造函数参数的提示

主要的困难将在实际使用这样的指标时出现,不同的震荡指标有不同的外部参数设置,如果我们为每个震荡指标独立出参数并且为它们使用不同的前缀, 我们将能够人工使用这些指标,但是它将不适合用在iCustom()或者IndicatorCreate()函数中,因为参数太多了。对于 IndicatorCreate(),传入参数的数量限制是256,而对于 iCustom() 是64。这个值还包含了通用的参数,例如交易品种和指标名称,所以实际可用的参数数量甚至比它还少。我们可以也使用少一些的通用参数集,但是指标在这种情况下使用将会不方便: 我们将需要检查引用来指导对于特定的指标来说使用哪些参数,

图形界面可以解决这个问题: 它的对话框可以使用特定的控件来用于选定的指标。我们也应该提供功能来使用 iCustom() 或者 IndicatorCreate() 来调用指标, 这样指标属性窗口将有少量的通用外部参数。

参数集

让我们定义所需的最小外部参数集合,在终端中查看震荡指标的列表: 主菜单 - 插入 - 指标 - 震荡指标, 把它们加到表格中。

表格 1. 终端中所有的震荡指标

函数名称缓冲区参数
iATR平均真实范围1. 线形1. int ma_period — 平均周期数
iBearsPower空头力度1. 柱形1. int ma_period — 平均周期数
iBullsPower多头力度1. 线形1. int ma_period — 平均周期数
iCCI商品通道指数1. 线形1. int ma_period — 平均周期数
2. ENUM_APPLIED_PRICE applied_price — 价格类型
iChaikin蔡金(Chaikin)震荡指标1. 线形1. int fast_ma_period — 快速平均周期数  
2. int slow_ma_period — 慢速平均周期数
3. ENUM_MA_METHOD ma_method — 平滑类型
4. ENUM_APPLIED_VOLUME applied_volume — 使用的交易量 
iDeMarkerDeM指标1. 线形1. int ma_period — 平均周期数
iForce强力指数1. 线形1. int ma_period — 平均周期数
2. ENUM_MA_METHOD ma_method — 平滑类型 
3. ENUM_APPLIED_VOLUME applied_volume — 用于计算的交易量类型 
iMomentum动量1. 线形1. int mom_period — 平均周期数
2. ENUM_APPLIED_PRICE applied_price — 价格类型
iMACD移动平均汇总/分离指标(MACD)1. 柱形
2. 线形
1. int fast_ema_period — 快速移动平均周期数
2. int slow_ema_period — 慢速移动平均周期数
3. int signal_period — 差别平均周期数
4. ENUM_APPLIED_PRICE  applied_price — 价格类型
iOsMA移动平均震荡指标(OsMA, MACD 柱形图)1. 柱形1. int fast_ema_period — 快速移动平均周期数
2. int slow_ema_period — 慢速移动平均周期数
3. int signal_period — 差别平均周期数
4. ENUM_APPLIED_PRICE  applied_price — 价格类型
iRSI相对强度指数1. 线形1. int ma_period — 平均周期数
2. ENUM_APPLIED_PRICE applied_price — 价格类型
iRVI相对动量指数1. 线形
2. 线形
1. int ma_period — 平均周期数
iStochastic随机震荡指标1. 线形
2. 线形
1. int Kperiod — 用于计算的柱数
2. int Dperiod — 首要平滑周期数
3. int slowing — 最终平滑周期数
4. ENUM_MA_METHOD ma_method — 平滑类型
5. ENUM_STO_PRICE price_field — 随机震荡计算方法 
iTriX三重指数平均线(TRIX)1. 线形1. int ma_period — 平均周期数
2. ENUM_APPLIED_PRICE applied_price — 价格类型
iWPR威廉姆斯百分比范围1. 线形1. int calc_period — 平均周期数

根据参数列,我们创建一个含有所有参数类型的列表,然后确定它们的最大数量。

表格 2. 参数的类型和数量 

类型数量
int3
ENUM_APPLIED_PRICE1
ENUM_MA_METHOD1
ENUM_APPLIED_VOLUME1
ENUM_STO_PRICE1
从缓冲区列,您可以看到可以使用两个指标缓冲区,并且不同的指标可以有不同的绘图方式。当然,我们可以把它们都画成线形,但是它们其中的一些通常显示为柱形图,并且终端也允许这样,所以我们会试着提供功能在更换指标时修改对应的绘图类型选项。另外,我们需要提供绘制水平线的功能,因为有些指标需要加上它们 (RSI, CCI, 等等.)

计划

一个大的通用任务能够分成小的独立任务的数量越多,它的实现就会越简单方便,所以,我们的工作将包括三个阶段:

  1. 为通用震荡指标创建类,并创建不包含GUI(图形用户界面)的震荡指标。
  2. 创建用于GUI的类。
  3. 把通用震荡指标和图形界面整合到一起。 

其中重要的一点是,您应当注意包括默认设置,我们应该提供功能来同时使用图形界面或者属性窗口来配置指标的参数(为了使得通用指标有最大的灵活性),当在属性窗口中配置参数时,它包含了一小组通用参数,我们需要保证,所有的默认设置的参数能够提供指标的自然外观。

考虑不同震荡指标的默认值。例如, 随机震荡指标的周期数为 5, 3, 3 (第一个参数比第二个大), 而 MACD 使用的是 12, 26, 9 (第一个参数比第二个小),MACD 的第一个参数意思是快速移动平均的周期数, 而第二个参数是慢速移动平均的周期数,所以第一个参数必须小于第二个。对于蔡金震荡指标,第一个参数和第二个的比例很重要 (也使用了快速和慢速移动平均的周期数),而对于随机震荡指标,这个比例就没有那么重要,并且它对应了价格变化,可以是任何数值。如果我们把MACD的第一个参数设置得大于第二个,指标的方向就会和价格变化相反 (在设置默认参数时我们应当记住这一点)。

当使用图形界面时,指标应该使用常用的默认参数集来开始运行: MACD 使用周期数为 12, 26, 9, 随机震荡使用的周期数为 5, 3, 3, 等等。另外,最好可以使新选择的指标可以选择从默认参数开始运行或者以与之前指标相同的参数开始运行。例如,我们分析 RSI 和 CCI, 并且我们想看到不同的指标在使用相同参数时线形的变化,所以我们在实现类的时候要提供这种功能。

创建指标的基类

让我们在 Include 文件夹下创建一个新的文件夹 'UniOsc' ,所有增加的指标文件都位于这个新文件夹之中。使用的震荡指标集合在表格1种已经定义过,让我们创建一个对应的枚举来选择震荡指标类型,除了指标文件,我们可能在其他地方也要使用这个枚举,所以我们把它加到独立的文件 UniOscDefines.mqh 中(在文件夹 'UniOsc'下): 

enum EOscUnyType{
   OscUni_ATR,
   OscUni_BearsPower,
   OscUni_BullsPower,  
   OscUni_CCI,
   OscUni_Chaikin,
   OscUni_DeMarker,
   OscUni_Force,
   OscUni_Momentum,
   OscUni_MACD,
   OscUni_OsMA,
   OscUni_RSI,
   OscUni_RVI,
   OscUni_Stochastic,
   OscUni_TriX,
   OscUni_WPR
};

这个文件将不再加入其他内容。

让我们为指标文件创建 "CUniOsc.mqh",并在其中写上 COscUni 类的模板:

class COscUni{
   protected:
    
   public:

};

'protected' 部分是由模板确定的,因为有些类成员需要被保护,但是还是应当可以在子类中访问 ( 'private' 部分的成员是被保护的,不能在子类中找到 )。

基类的主要方法是对应着指标的 OnCalculate() 函数的,让我们称它为 Calculate(),这个方法的前两个参数对应着 OnCalculate() 的相应参数: rates_total (柱的总数) 和 prew_calculate (已经计算过的柱数),不需要把数组传给 Calculate() 方法了, 因为使用了另外指标的数据。但是我们需要传入两个指标缓冲区,它们会填入数据。即使在使用只含一个缓冲区的指标时,我们将需要控制第二个缓冲区,所以在任何情况下都会给 Calculate() 方法传入两个缓冲区。Calculate() 的代码将依赖于使用的震荡指标类型: 它可以是含有一个或者两个缓冲区。所以,Calculate() 方法将会是虚函数:

virtual int Calculate( const int rates_total,
               const int prev_calculated,
               double & buffer0[],
               double & buffer1[]
){
   return(rates_total);
}

当载入不同类型的指标时,我们将需要一个变量来保存指标的句柄。我们在 protected 部分声明它,

另外,我们还需要更多的变量来用于不同的缓冲区显示属性。这些属性将在载入每个指标时确定,也就是这些变量将在子类中设置:

int m_handle;           // 指标句柄
int m_bufferscnt;       // 使用的缓冲区数量
string m_name;          // 指标名称      
string m_label1;        // 缓冲区1的名称    
string m_label2;        // 缓冲区2的名称
int m_drawtype1;        // 缓冲区1的绘图类型    
int m_drawtype2;        // 缓冲区2的绘图类型      
string m_help;          // 指标参数的提示
int m_digits;           // 指标值中小数点位数
int m_levels_total;     // 水平总数
double m_level_value[]; // 水平数值的数组

我们需要检查指标是否被成功载入,所以我们需要对应的方法来检查指标的句柄:

bool CheckHandle(){
   return(m_handle!=INVALID_HANDLE);
}

如果我们通过图形界面修改震荡指标,我们需要确定指标是否已经完成了计算,这可以通过使用 BarsCalculated() 函数来做到, 调用需要指标的句柄,所以,我们加上一个方法来取得句柄:  

int Handle(){
    return(m_handle);
}

在类的构造函数中我们需要初始化句柄,在析构函数中要检查句柄,如有必要则调用 IndicatorRelease() :

void COscUni(){
   m_handle=INVALID_HANDLE;
}

void ~COscUni(){
   if(m_handle!=INVALID_HANDLE){
      IndicatorRelease(m_handle);
   }
} 

让我们提供对决定各种指标显示的变量的访问,并创建方法来取得它们的数值:

string Name(){ // 震荡指标的名称
   return(m_name);
}    
  
int BuffersCount(){ // 震荡指标缓冲区的数量
   return(m_bufferscnt);
}

string Label1(){ // 第一个缓冲区的名称
   return(m_label1);
}

string Label2(){ // 第二个缓冲区的名称
   return(m_label2);
}

int DrawType1(){ // 第一个缓冲区的绘图类型
   return(m_drawtype1);
}

int DrawType2(){ // 第二个缓冲区的绘图类型
   return(m_drawtype2);
}  

string Help(){ // 使用参数的提示
   return(m_help);
}

int Digits(){ // 指标值的小数点位数
   return(m_digits);
}

int LevelsTotal(){ // 指标水平的数量
   return(m_levels_total);
}

double LevelValue(int index){ // 根据指定的索引取得水平的数值
   return(m_level_value[index]);
}

所有这些方法都返回对应变量的值,并且变量的值应当在震荡指标的子类中赋值。  

'Calculate(计算)'的子类

让我们创建两个子类: 用于一个缓冲区的指标和两个缓冲区的指标。对于一个缓冲区的指标:

class COscUni_Calculate1:public COscUni{
   public:
      void COscUni_Calculate1(){
         m_bufferscnt=1;
      }
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & buffer0[],
                     double & buffer1[]
      ){
        
         int cnt,start;
        
         if(prev_calculated==0){
            cnt=rates_total;
            start=0;
         }
         else{
            cnt=rates_total-prev_calculated+1;
            start=prev_calculated-1;
         }  
        
         if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
            return(0);
         }
        
         for(int i=start;i<rates_total;i++){
            buffer1[i]=EMPTY_VALUE;
         }        
        
         return(rates_total);
      }
};

让我们讨论这个类,这个类的构造函数是 COscUni_Calculate1, 缓冲区的数量 (在本例中是 1) 是在构造函数中设置的。需要复制的缓冲区元件数量('cnt'变量)和我们需要清空的第二个缓冲区的起始柱的索引('start'变量)就在 Calculate() 方法中计算, 它依赖于 rates_total 和 prev_calculate 变量的值。如果数据的复制失败(当调用 CopyBuffer()),方法会返回0,这样可以在下一个订单时刻的最开始进行全部计算,在方法的最后返回 rates_total。

用于含有两个缓冲区的指标的类:

class COscUni_Calculate2:public COscUni{
   public:
      void COscUni_Calculate2(){
         m_bufferscnt=2;
      }  
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & buffer0[],
                     double & buffer1[]
      ){
         int cnt;
         if(prev_calculated==0){
            cnt=rates_total;
         }
         else{
            cnt=rates_total-prev_calculated+1;
         }          
         if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
            return(0);
         }
         if(CopyBuffer(m_handle,1,0,cnt,buffer1)<=0){
            return(0);
         }
         return(rates_total);
      }
};

这个类比单个缓冲区指标的类还要更简单,在 Calculate() 方法的最开始计算需要复制的元件数量('cnt'变量),然后复制缓冲区。 

指标的子类

现在我们创建用于震荡指标的子类。这些将使 COscUni_Calculate1 或者 COscUni_Calculate2 的子类,所有这些类都将只有一个构造函数。对应着震荡指标的参数和一些额外参数都将会传给每个类的构造函数,额外的参数将会决定如何使用传入构造函数的参数或者用于设置默认值 ('use_default' 变量)。第二个参数 keep_previous 决定了是设置所有指标参数的默认值还是只设置那些还没有被使用的参数。

列表中第一个指标是 ATR, 让我们先为它开始写一个子类。首先,我们使用类的模板:

class COscUni_ATR:public COscUni_Calculate1{
   public:
   void COscUni_ATR(bool use_default,bool keep_previous,int & ma_period){

   }
};

请注意,ma_period 参数是通过引用传入的,是为了当设置指标的默认参数时可以在通用震荡指标中访问这些参数值。

在构造函数中写下代码:

if(use_default){
   if(keep_previous){
      if(ma_period==-1)ma_period=14;
   }
   else{
      ma_period=14;
   }      
}  

如果 use_default=true, 在这部分代码中会设置默认值。如果 keep_previous=true, 则只会在参数等于 -1 时,也就是之前没有被使用过时,才设置默认值。所以,在通用震荡指标的初始化过程中,我们需要把所有参数的值设为 -1 。

现在让我们分析在子类的构造函数中最重要的代码行,它包含了指标的载入:

m_handle=iATR(Symbol(),Period(),ma_period);

加上几行代码来设置显示参数:

m_name=StringFormat("ATR(%i)",ma_period); // 指标名称
m_label1="ATR"; // 缓冲区名称
m_drawtype1=DRAW_LINE;   // 绘制类型
m_help=StringFormat("ma_period - Period1(%i)",ma_period); // 提示  
m_digits=_Digits+1; // 指标值的小数点位数
m_levels_total=0; // 水平的数量   

让我们分析在一个更加复杂的指标 MACD 中子类的一些创建步骤,创建原则是一样的,尽管在这种情况下需要更多的代码。所以,让我们考虑分段,设置默认参数:

if(use_default){
   if(keep_previous){
      if(fast_ema_period==-1)fast_ema_period=12;
      if(slow_ema_period==-1)slow_ema_period=26;
      if(signal_period==-1)signal_period=9;
      if(applied_price==-1)applied_price=PRICE_CLOSE;            
   }
   else{
      fast_ema_period=12;
      slow_ema_period=26;
      signal_period=9;
      applied_price=PRICE_CLOSE;
   }      
}

设置显示参数:

m_handle=iMACD(Symbol(),
               Period(),
               fast_ema_period,
               slow_ema_period,
               signal_period,
               (ENUM_APPLIED_PRICE)applied_price);

m_name=StringFormat( "iMACD(%i,%i,%i,%s)",
                     fast_ema_period,
                     slow_ema_period,
                     signal_period,
                     EnumToString((ENUM_APPLIED_PRICE)applied_price));
                    
m_label1="Main";
m_label2="Signal";      
m_drawtype1=DRAW_HISTOGRAM;            
m_drawtype2=DRAW_LINE;

m_help=StringFormat( "fast_ema_period - Period1(%i), "+
                     "slow_ema_period - Period2(%i), "+
                     "signal_period - Period3(%i), "+
                     "applied_price - Price(%s)",
                     fast_ema_period,
                     slow_ema_period,
                     signal_period,
                     EnumToString((ENUM_APPLIED_PRICE)applied_price));  
                    
m_digits=_Digits+1;

构造函数的参数:

void COscUni_MACD(bool use_default,
                  bool keep_previous,
                  int & fast_ema_period,
                  int & slow_ema_period,
                  int & signal_period,
                  long & applied_price
){

请注意,所使用的 applied_price 在标准的 ENUM_APPLIED_PRICE 枚举中声明为长整形(long),这使得可以把这个变量设为 -1 以指示该参数还没有被使用。

让我们看一下用于RSI指标的类的另一端代码,它包含了设置水平的代码部分: 

m_levels_total=3;
ArrayResize(m_level_value,3);
m_level_value[0]=30;
m_level_value[1]=50;
m_level_value[2]=70;

它设置水平的数量,修改数组的大小并填充水平的数值。

我不会在这里描述其他的震荡指标类是如何创建的,文章的附件中包含了完整可用的震荡指标集(CUniOsc.mqh 文件)。

创建一个通用的震荡指标(开始)

震荡指标类已经准备好了,我们就可以创建一个通用的震荡指标,尽管现在还没有震荡指标的图形界面。

创建一个新的指标,也就是 "iUniOsc",然后,在指标创建向导中选择函数类型 OnCalculate(...open,high,low,close), 创建一个外部变量 (这样可以更容易发现在哪里放置外部变量) 和两个线类型的缓冲区。

在外部变量之前,我们需要包含含有枚举和震荡指标类的文件: 

#include <UniOsc/UniOscDefines.mqh>
#include <UniOsc/CUniOsc.mqh>

创建一个外部变量用于选择震荡指标类型:

input EOscUnyType          Type        =  OscUni_ATR;

UseDefault 和 KeepPrevious 变量:

input bool                 UseDefault  =  true;
input bool                 KeepPrev    =  true;

用于震荡指标参数的通用变量:

input int                  Period1     =  14;
input int                  Period2     =  14;
input int                  Period3     =  14;
input ENUM_MA_METHOD       MaMethod    =  MODE_EMA;
input ENUM_APPLIED_PRICE   Price       =  PRICE_CLOSE;  
input ENUM_APPLIED_VOLUME  Volume      =  VOLUME_TICK;  
input ENUM_STO_PRICE       StPrice     =  STO_LOWHIGH;

有些指标画一条线,其他的画两条线。第一个缓冲区有时候显示为线,有时候画成柱形图。我们将画明亮的线形,而柱形图是灰色的,所以我们创建三个用于颜色的变量:

input color                ColorLine1  =  clrLightSeaGreen;
input color                ColorLine2  =  clrRed;
input color                ColorHisto  =  clrGray;

因为我们将要创建一个GUI,它将可以使得不重新启动指标就能改变震荡指标的类型和参数,所以让我们创建 Type 变量的副本和用于指标参数的变量: 

int                  _Period1;
int                  _Period2;
int                  _Period3;
long                 _MaMethod;
long                 _Price;  
long                 _Volume;  
long                 _StPrice;
EOscUnyType          _Type;

让我们声明一个指针变量用于通用震荡指标对象:

COscUni * osc;

还有更多一些变量要声明:

string ProgName;
string ShortName;

这些变量将用于生成指标的名称来显示在子窗口的左上角。 

现在我们将在 OnInit() 函数的末尾加上代码, 但是首先我们需要做些准备。我们还要根据 UseDefault 和 KeepPrevious 的值来准备震荡指标的参数 (并给 _Type 变量赋值), 把它写成一个函数,这样使代码结构更好。

void PrepareParameters(){

   _Type=Type;

   if(UseDefault && KeepPrev){
      _Period1=-1;
      _Period2=-1;
      _Period3=-1;
      _MaMethod=-1;
      _Volume=-1;
      _Price=-1;  
      _StPrice=-1;
   }
   else{  
      _Period1=Period1;
      _Period2=Period2;
      _Period3=Period3;
      _MaMethod=MaMethod;
      _Volume=Volume;
      _Price=Price;  
      _StPrice=StPrice;
   }
}

如果使用了 UseDefault 和 KeepPrevious, 所有的变量都赋值为 -1, 这样我们在类的构造函数中就可以看到我们没有使用过的变量,然后只把它们设为默认值。来自属性窗口的数值将会在其他条件下赋值,这些数值将会根据设置赋值,或者它们可以在对象创建的时候使用默认值替代,  

在准备完参数以后,再载入选中的震荡指标。载入代码也写成一个函数:

void LoadOscillator(){
   switch(_Type){
      case OscUni_ATR:
         osc=new COscUni_ATR(UseDefault,KeepPrev,_Period1);
      break;
      case OscUni_BearsPower:
         osc=new COscUni_BearsPower(UseDefault,KeepPrev,_Period1);
      break;
      case OscUni_BullsPower:
         osc=new COscUni_BullsPower(UseDefault,KeepPrev,_Period1);
      break;      
      ...
   }  
}

在载入震荡指标之后,我们需要检查句柄:

if(!osc.CheckHandle()){
   Alert("指标载入错误 "+osc.Name());
   return(INIT_FAILED);
}

如果成功载入,再通过对应的对象方法来接收绘制风格,这部分代码也实现为一个函数:

void SetStyles(){  

   // 设置风格
   if(osc.BuffersCount()==2){
      PlotIndexSetInteger(0,PLOT_DRAW_TYPE,osc.DrawType1());
      PlotIndexSetInteger(1,PLOT_DRAW_TYPE,osc.DrawType2());
      PlotIndexSetInteger(0,PLOT_SHOW_DATA,true);
      PlotIndexSetInteger(1,PLOT_SHOW_DATA,true);
      PlotIndexSetString(0,PLOT_LABEL,osc.Label1());
      PlotIndexSetString(1,PLOT_LABEL,osc.Label2());
      if(osc.DrawType1()==DRAW_HISTOGRAM){
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorHisto);
      }
      else{
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorLine1);        
      }
      PlotIndexSetInteger(1,PLOT_LINE_COLOR,ColorLine2);  
   }
   else{
      PlotIndexSetInteger(0,PLOT_DRAW_TYPE,osc.DrawType1());
      PlotIndexSetInteger(1,PLOT_DRAW_TYPE,DRAW_NONE);  
      PlotIndexSetInteger(0,PLOT_SHOW_DATA,true);
      PlotIndexSetInteger(1,PLOT_SHOW_DATA,false);  
      PlotIndexSetString(0,PLOT_LABEL,osc.Label1());
      PlotIndexSetString(1,PLOT_LABEL,"");
      if(osc.DrawType1()==DRAW_HISTOGRAM){
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorHisto);
      }
      else{
         PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorLine1);        
      }        
   }
  
   // 设置小数位数
   IndicatorSetInteger(INDICATOR_DIGITS,osc.Digits());

   // 设置水平
   int levels=osc.LevelsTotal();
   IndicatorSetInteger(INDICATOR_LEVELS,levels);
   for(int i=0;i<levels;i++){
      IndicatorSetDouble(INDICATOR_LEVELVALUE,i,osc.LevelValue(i));
   }

}    

首先,根据震荡指标的缓冲区数量,实现从两种可用的风格设置选项中选择一个,如果第一个缓冲区是一个柱形图,就设置对应的缓冲区类型,然后设置指标数值的小数点位数,最后设置水平。

这里是 OnInit() 的完整代码, 它包含了对所有新创建的函数的调用:

int OnInit(){

   SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA);
   SetIndexBuffer(1,Label2Buffer,INDICATOR_DATA);

   PrepareParameters();

   LoadOscillator();
  
   if(!osc.CheckHandle()){
      Alert("载入指标时出错 "+osc.Name());
      return(INIT_FAILED);
   }

   SetStyles();
  
   Print("参数对应: "+osc.Help());
  
   ShortName=ProgName+": "+osc.Name();  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
  
   return(INIT_SUCCEEDED);
}

请注意,函数的末尾调用了 Print 函数, 它包含了属性窗口中使用的参数的提示,并且设置了短的指标名称。

现在我们创建通用震荡指标项目的第一步就完成了,也就是说,我们已经准备好了使用之前创建类的指标。下一步我们创建一个GUI类。

本文的附件中有一个准备好的指标叫做 iUniOsc (晚些时候将稍微修改一些指标代码,这样它就与当前阶段的指标有少许不同)。 

图形界面的创建计划

为了创建一个图形界面,我们可以使用图形对象,包括"输入栏位(entry field)"来用于输入数字值,以及几个按钮(buttons)来用于枚举类型参数(下拉列表)。然而,这将是一种不同的方法。您可以找到各种不同的MQL5开发库来创建图形界面,这些开发库可以创建标准控件,例如对话框,带有调节按钮的输入栏位,下拉列表,等等。终端中也包含了一系列用于创建面板和对话框的标准类,"文章"部分有很多系列文章,与创建图形界面有关。

有一系列三篇文章(文章 1, 文章 2, 文章 3)描述了创建图形界面的简单而快速的方法。除了理论知识之外,这些文章创建了一个开发库,这个库可以操作图形对象并创建图形界面。所有以上这些选择都有它们的优点和缺点,当写这篇文章时都已经考虑了它们,最终,我选择了上面的最后一个选项 (incGUI 开发库)。

MetaTrader 5 一直在活跃开发和提高,所以这个库的一些控件有可能会变得过时(例如,滚动条), 但是它们还是可以使用的。为了开始使用这个开发库,下载位于 "自定义图形化控件. 第三部分. 用于 MetaTrader 5 的表单"的附件, 解压缩,并把 incGUI_v3.mqh 文件复制到终端数据文件目录的 Include 文件夹下。

表单类

图形界面的创建将在一个单独文件 "UniOscGUI.mqh" 中实现。首先我们需要包含开发库:

#include <IncGUI_v3.mqh>

编译它。现在,在编译的时候将会出现一些警告信息。增强的编译器发现了这些代码中的问题,并且允许改正它们,改正过后的 "inc_GUI_v4" 文件在文章的附件中。我们包含 IncGUI_v4.mqh 而不是 IncGUI_v4.mqh 以及 UniOscDefines.mqh。 

#include <IncGUI_v4.mqh>
#include <UniOsc/UniOscDefines.mqh>

让我们把 iUniOsc 复制一份命名为 iUniOscGUI。随后,iUniOsc 指标就可以编辑,隐藏 UseDefault 和 KeepPrev 参数。它们在没有GUI的指标中是没有意义的,但是我们需要把它们设为 false:

bool                 UseDefault  =  false;
bool                 KeepPrev    =  false;

随后 iUniOsc 指标就全部完成了。

让我们继续操作 iUniOscGUI 指标,在其中包含 UniOscGUI.mqh 文件。我们需要一共包含三个文件:

#include <UniOsc/UniOscDefines.mqh>
#include <UniOsc/CUniOsc.mqh>
#include <UniOsc/UniOscGUI.mqh>

在编译了指标之后,您可以检查代码并马上在图表上看到 GUI 了。直到现在,所有的工作都是在 UniOscGUI.mqh 文件中进行的, 

GUI 将以对话框的形式展现; 在上面的部分有震荡指标的下拉列表, 下面是每个震荡指标对应的一些控件。所以,在文件中我们将有一个类用于创建表单,还有一组类 (父类和几个子类)用于创建表单上的控件。

让我们从表单开始,详细的按步骤描述创建表单的过程在文章 "自定义图形控件. 第三部分. MetaTrader 5 的表单". 这里我们针对我们特定的任务进行这个过程。

1. 首先,我们需要把 CFormTemplate 类从 IncGUI_v4.mqh 文件复制到 UniOscGUI.mqh, 并且把它重命名为 CUniOscForm。

2. 设置属性。这是通过 CUniOscForm 类中的 MainProperties() 方法来实现的。让我们设置如下的属性:

void MainProperties(){
      m_Name         =  "UniOscForm";
      m_Width        =  FORM_WIDTH;
      m_Height       =  150;
      m_Type         =  0;
      m_Caption      =  "UniOsc";
      m_Movable      =  true;
      m_Resizable    =  true;
      m_CloseButton  =  true;
}

请注意,m_Heigh 变量设为 FORM_WIDTH。在最后一步,我们将需要找到控件正确的大小和形状,所以,让我们在文件的开头加上以下常数:

#define FORM_WIDTH 210        // 表单宽度
#define SPIN_BOX_WIDTH 110    // 调节按钮的宽度
#define COMBO_BOX_WIDTH 110   // 下拉列表的宽度

随后,表单就可以在指标中使用了。然后,我们在指标中声明一个外部变量 UseGUI,默认值为 'true' (在属性窗口的开始):

input bool                 UseGUI      =  true;

在外部变量之后,我们需要声明一个表单类的指针:

CUniOscForm * frm;

如果 UseGUI = true, 我们在指标的 OnInit() 中创建一个对象,并且通过调用设置属性的方法来进行准备:

frm=new CUniOscForm();  // 创建一个对象
frm.Init();             // 初始化
frm.SetSubWindow(0);    // 创建一个显示表单的子窗口
frm.SetPos(10,30);      // 设置表单的初始位置
frm.Show();             // 使表单可见

在 OnDeinit() 函数中, 我们隐藏表单并删除对象:

if(CheckPointer(frm)==POINTER_DYNAMIC){
   frm.Hide();
   delete(frm);
}

从 OnChartEvent() 函数中调用 Event() 方法:

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   frm.Event(id,lparam,dparam,sparam);  
}

现在,如果您在图表上附加指标,您就会看到表单 (图 2).


图 2. 在图表上运行 iUniOscGUI 指标后创建一个表单 


表单上的所有按钮都是有效的: 表单可以通过左上角的按钮移动 (点击按钮,然后指向一个新的位置再点击), 它也可以被最小化(右上角有一个长方形按钮)。点击有交叉的按钮可以关闭表单,在这种情况下指标应该从图表上删除。指标可以通过使用 ChartIndicatorDelete() 函数来删除。为了使用这个功能,您需要知道指标子窗口的索引,您可以使用 ChartWindowFind() 函数来得到, 它需要指标的短名称。

当点击了表单的关闭按钮后,Event() 方法返回1。检查返回值并且如有必要从表单上删除指标:

int win=ChartWindowFind(0,ShortName);  // 寻找子窗口
ChartIndicatorDelete(0,win,ShortName); // 删除指标
ChartRedraw();                         // 加快图表的重绘

现在,点击关闭表单的按钮就会从图表上删除指标了。  

让我们在图表上加上主控件: 用于选择振荡指标类型的下拉列表。它可以使用 CComBox 类来创建,我们在 CUniOscForm 类中加入一些代码。声明用于对象的变量:

CComBox m_cmb_main;

然后在 OnInitEvent() 方法中调用类的 Init() 方法:

m_cmb_main.Init("cb_main",100," 选择震荡指标");

要向方法中传入控件的名称 (用于图形对象名称的前缀), 控件的宽度以及一个标签, 

在 OnShowEvent() 方法中调用 Show() 方法:

m_cmb_main.Show(aLeft+10,aTop+10);

这里要指定表单中控件的位置坐标 (在表单空间的左上角有10个像素的缩进), 

在 OnHideEvent() 中调用 Hide() 方法:

m_cmb_main.Hide();

在主列表中选择有变化后应该载入另一个指标,这可以通过指标文件中方便地做到, 所以震荡指标列表的 Event() 方法应该在指标的 OnChartEvent() 函数中调用,而不是在表单的 EventsHandler() 方法中调用。并且应该处理这个事件:

int me=frm.m_cmb_main.Event(id,lparam,dparam,sparam);
if(me==1){
   Alert(frm.m_cmb_main.SelectedText());
}  

图表事件的标准参数会传给这个方法,然后当方法返回1时,会打开一个消息框。

列表中应该使用选项填充,有几种实现方法:

  • 一切都可以在表单的 OnInitEvent() 方法中完成;
  • 表单中可以额外加一个方法,然后它可以从指标的 Init() 方法后调用;
  • 列表的方法可以直接从指标中访问。

让我们使用第三个选项,它只需要较少的方法。首先,我们要在指标中创建一个震荡指标类型的数组:

EOscUniType osctype[]={
   OscUni_ATR,
   OscUni_BearsPower,
   OscUni_BullsPower,  
   OscUni_CCI,
   OscUni_Chaikin,
   OscUni_DeMarker,
   OscUni_Force,
   OscUni_Momentum,
   OscUni_MACD,
   OscUni_OsMA,
   OscUni_RSI,
   OscUni_RVI,
   OscUni_Stochastic,
   OscUni_TriX,
   OscUni_WPR
};

然后,在指标中调用了 frm.Init() 之后,我们填充列表并设置默认选项:

for(int i=0;i<ArraySize(osctype);i++){
   frm.m_cmb_main.AddItem(EnumToString(osctype[i]));
}
frm.m_cmb_main.SetSelectedIndex(0);

在这个阶段可以做一项检查,在表单上应该显示一个震荡指标类型的下拉列表,当选择有变化时,应该显示对应的消息框 (图 3):

 
图 3. 含有震荡指标列表的表单以及在选择另一个项目后显示消息框 

表单上的控件

在文章的开始,我们定义了根据类型的外部参数的最大数量 (三个参数用于输入数值型变量,4个参数用于标准的枚举),为了输入数值型数值,我们将使用 incGUI 开发库中的 CSpinInputBox 元件 (一种带有按钮的输入栏位),CComBox 元件 (下拉列表) 将用于标准的枚举。

在图形界面类文件的开始,我们使用数值声明标准枚举的数组:

ENUM_APPLIED_PRICE e_price[]={   PRICE_CLOSE,
                                 PRICE_OPEN,
                                 PRICE_HIGH,
                                 PRICE_LOW,
                                 PRICE_MEDIAN,
                                 PRICE_TYPICAL,
                                 PRICE_WEIGHTED
};

ENUM_MA_METHOD e_method[]={MODE_SMA,MODE_EMA,MODE_SMMA,MODE_LWMA};

ENUM_APPLIED_VOLUME e_volume[]={VOLUME_TICK,VOLUME_REAL};

ENUM_STO_PRICE e_sto_price[]={STO_LOWHIGH,STO_CLOSECLOSE};

现在,在表单类中,我们声明控件变量 (三个变量用于 CSpinInputBox,而四个变量用于 CComBox): 

CSpinInputBox m_value1;
CSpinInputBox m_value2;      
CSpinInputBox m_value3;
      
CComBox m_price;
CComBox m_method;
CComBox m_volume
CComBox m_sto_price;

在表单类的 OnInitEvent() 方法中,我们初始化下拉列表 (CComBox 类对象) 并且使用之前声明的数组来填充它们:

m_price.Init("price",COMBO_BOX_WIDTH," price");
m_method.Init("method",COMBO_BOX_WIDTH," method");
m_volume.Init("volume",COMBO_BOX_WIDTH," volume");
m_sto_price.Init("sto_price",COMBO_BOX_WIDTH," price");              

for(int i=0;i<ArraySize(e_price);i++){
   m_price.AddItem(EnumToString(e_price[i]));            
}
for(int i=0;i<ArraySize(e_method);i++){
   m_method.AddItem(EnumToString(e_method[i]));              
}            
for(int i=0;i<ArraySize(e_volume);i++){
   m_volume.AddItem(EnumToString(e_volume[i]));            
}            
for(int i=0;i<ArraySize(e_sto_price);i++){
   m_sto_price.AddItem(EnumToString(e_sto_price[i]));            
}

因为显示不同指标的控件集合也是不同的,让我们创建类(基类和子类)来构建集合。基类是 CUniOscControls, 这里是它的模板:

class CUniOscControls{
   protected:
      CSpinInputBox * m_value1;
      CSpinInputBox * m_value2;      
      CSpinInputBox * m_value3;
      CComBox * m_price;
      CComBox * m_method;
      CComBox * m_volume;
      CComBox * m_sto_price;
   public:
   void SetPointers(CSpinInputBox & value1,
                        CSpinInputBox & value2,      
                        CSpinInputBox & value3,
                        CComBox & price,
                        CComBox & method,
                        CComBox & volume,
                        CComBox & sto_price){
       ...
   }
   void Hide(){
       ...
   }
   int Event(int id,long lparam,double dparam,string sparam){
      ...
      return(0);
   }
   virtual void InitControls(){
   }  
   virtual void Show(int x,int y){
   }  
   virtual int FormHeight(){
      return(0);
   }
};

在使用这个类对象的开始将会调用 SetPointers() 方法,指向所有控件的指针会传入此方法,并且会在方法中把它们保存到类的变量中: 

void SetPointers(CSpinInputBox & value1,
                     CSpinInputBox & value2,      
                     CSpinInputBox & value3,
                     CComBox & price,
                     CComBox & method,
                     CComBox & volume,
                     CComBox & sto_price){
   m_value1=GetPointer(value1);
   m_value2=GetPointer(value2);      
   m_value3=GetPointer(value3);            
   m_price=GetPointer(price);
   m_method=GetPointer(method);
   m_volume=GetPointer(volume);
   m_sto_price=GetPointer(sto_price);
}

这些指针是用于隐藏所有控件的 (Hide() 方法):

void Hide(){
   m_value1.Hide();
   m_value2.Hide();
   m_value3.Hide();
   m_price.Hide();
   m_method.Hide();
   m_volume.Hide();
   m_sto_price.Hide();
}

它们的事件需要处理 (Event() 方法):

int Event(int id,long lparam,double dparam,string sparam){
   int e1=m_value1.Event(id,lparam,dparam,sparam);
   int e2=m_value2.Event(id,lparam,dparam,sparam);
   int e3=m_value3.Event(id,lparam,dparam,sparam);
   int e4=m_price.Event(id,lparam,dparam,sparam);
   int e5=m_method.Event(id,lparam,dparam,sparam);
   int e6=m_volume.Event(id,lparam,dparam,sparam);
   int e7=m_sto_price.Event(id,lparam,dparam,sparam);
   if(e1!=0 || e2!=0 || e3!=0 || e4!=0 || e5!=0 ||e6!=0 || e7!=0){
      return(1);
   }
   return(0);
}

其它的方法是虚拟的,每个震荡指标将在子类中有它们特定的代码。Show() 方法将用于显示控件;FormHeight() 将返回表单的高度;InitControls() 方法只允许修改控件旁边的文字 (图 4).


图 4. 对于不同震荡指标在控件旁边显示不同的文字 

事实上,来自 incGUI 开发库的控件只有所需的最小方法集,而没有用于修改文字的方法。类的设计是,如果有需要修改文字,就通过调用 Init() 方法,因为文字的改变是使用 Init() 完成的, 该方法就称为 InitControls()。  

考虑一些子类,它们中最简单的就是用于 ATR 指标的,而最难的是用于随机震荡指标的。

对于 ATR:

class CUniOscControls_ATR:public CUniOscControls{
   void InitControls(){
      m_value1.Init("value1",SPIN_BOX_WIDTH,1," ma_period");
   }
   void Show(int x,int y){
      m_value1.Show(x,y);
   }  
   int FormHeight(){
      return(70);
   }  
};

控件的 Init() 在 InitControls() 中调用,它最重要的特性(我们问什么必须准备这个虚方法)是传递文字 "ma_period",它将会在控件的旁边显示。

在表单类的 Show() 方法中,调用 CUniOscControls 类的 Show() 方法,在调用中,需要指定左上角第一个控件单位的坐标。FormHeight() 方法会简单返回一个数值。

对于随机震荡指标:

class CUniOscControls_Stochastic:public CUniOscControls{
   void InitControls(){
      m_value1.Init("value1",SPIN_BOX_WIDTH,1," Kperiod");
      m_value2.Init("value2",SPIN_BOX_WIDTH,1," Dperiod");  
      m_value3.Init("value3",SPIN_BOX_WIDTH,1," slowing");          
   }
   void Show(int x,int y){
      m_value1.Show(x,y);
      m_value2.Show(x,y+20);      
      m_value3.Show(x,y+40);
      m_method.Show(x,y+60);      
      m_sto_price.Show(x,y+80);
   }
   int FormHeight(){
      return(150);
   }    
};

每个控件的坐标在 Show() 方法中计算, 其余的部分应该很清楚。

最终,让我们看一下控件是怎样加到表单上的,在表单类中,声明一个控件类的指针:

CUniOscControls * m_controls;

在析构函数中删除对象:

void ~CUniOscForm(){
   delete(m_controls);
}

在表单类中加上 SetType() 方法,该方法将在指定所使用的震荡指标时调用。 

      void SetType(long type){
         if(CheckPointer(m_controls)==POINTER_DYNAMIC){
            delete(m_controls);
            m_controls=NULL;
         }
        
         switch((EOscUniType)type){
            case OscUni_ATR:
               m_controls=new CUniOscControls_ATR();
            break;
            case OscUni_BearsPower:
               m_controls=new CUniOscControls_BearsPower();
            break;
            case OscUni_BullsPower:
               m_controls=new CUniOscControls_BullsPower();
            break;
            case OscUni_CCI:
               m_controls=new CUniOscControls_CCI();
            break;
            case OscUni_Chaikin:
               m_controls=new CUniOscControls_Chaikin();
            break;
            case OscUni_DeMarker:
               m_controls=new CUniOscControls_DeMarker();
            break;
            case OscUni_Force:
               m_controls=new CUniOscControls_Force();
            break;
            case OscUni_Momentum:
               m_controls=new CUniOscControls_Momentum();
            break;
            case OscUni_MACD:
               m_controls=new CUniOscControls_MACD();
            break;
            case OscUni_OsMA:
               m_controls=new CUniOscControls_OsMA();
            break;
            case OscUni_RSI:
               m_controls=new CUniOscControls_RSI();
            break;
            case OscUni_RVI:
               m_controls=new CUniOscControls_RVI();
            break;
            case OscUni_Stochastic:
               m_controls=new CUniOscControls_Stochastic();
            break;
            case OscUni_TriX:
               m_controls=new CUniOscControls_TriX();
            break;
            case OscUni_WPR:
               m_controls=new CUniOscControls_WPR();
            break;
         }
        
         m_controls.SetPointers(m_value1,m_value2,m_value3,m_price,m_method,m_volume,m_sto_price);
         m_controls.InitControls();
        
         m_value1.SetReadOnly(false);
         m_value2.SetReadOnly(false);
         m_value3.SetReadOnly(false);
        
         m_value1.SetMinValue(1);        
         m_value2.SetMinValue(1);
         m_value3.SetMinValue(1);
        
         m_Height=m_controls.FormHeight();        
        
      }  

如果有一个对象,它应当在方法的开始就删除,然后,会根据指标的类型载入对应的类,在最后会调用 SetPointers() 和 InitControls()。然后进行一些额外的操作: 对于 SpinBox 对象, 需要启用可以使用键盘来输入数值 (调用 ReadOnly() 方法), 设置最小值 (调用 SetMinValue()), 以及用于 m_Height 的表单高度最小值。

在 OnShowEvent() 和 OnHideEvent() 方法中应该调用对应的 m_controls 对象的方法:

void OnShowEvent(int aLeft, int aTop){
   m_cmb_main.Show(aLeft+10,aTop+10);
   m_controls.Show(aLeft+10,aTop+10+20);
}
void OnHideEvent(){
   m_cmb_main.Hide();            
   m_controls.Hide();          
}  

现在,我们需要“激活”m_controls 对象的事件,在 OnChartEvent() 函数中加入 Event() 调用:

int ce=frm.m_controls.Event(id,lparam,dparam,sparam);

在指标的 OnInit() 中加入对表单的 SetType() 方法的调用 (在调用 SetSelectedIndex()方法之后):

frm.SetType(_Type);

在载入震荡指标之后,它参数的数值应该在表单上显示出来,所以我们在表单类中加入 SetValues() 方法:

void SetValues(int period1,
               int period2,
               int period3,
               long method,
               long price,
               long volume,  
               long sto_price  
){
  
   m_value1.SetValue(period1);
   m_value2.SetValue(period2);      
   m_value3.SetValue(period3);
  
   for(int i=0;i<ArraySize(e_price);i++){
      if(price==e_price[i]){
         m_price.SetSelectedIndex(i);
         break;
      }
   }
  
   for(int i=0;i<ArraySize(e_method);i++){
      if(method==e_method[i]){
         m_method.SetSelectedIndex(i);
         break;
      }
   }            

   for(int i=0;i<ArraySize(e_volume);i++){
      if(volume==e_volume[i]){
         m_volume.SetSelectedIndex(i);
         break;
      }
   }
  
   for(int i=0;i<ArraySize(e_sto_price);i++){
      if(sto_price==e_sto_price[i]){
         m_sto_price.SetSelectedIndex(i);
         break;
      }
   }

}      

在 SetValues() 方法中, 会设置 SpinBox 控件的数值,而对于枚举,是枚举值在枚举中的对应索引。在调用 SetType() 之后要调用 SetValues() 方法:

frm.SetValues(_Period1,_Period2,_Period3,_MaMethod,_Price,_Volume,_StPrice);

在这里,我们可以认为 GUI 已经全部完成了 (图 5), 但是指标还不知道如何回应它。


图 5. 用于 ATR 指标的控件窗口 

完成通用震荡指标

震荡指标类已经准备好了,GUI classes 也准备好了,所以现在我们需要整合它们。 

在本阶段,OnChatEvent() 函数看起来如下:

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{

   int e=frm.Event(id,lparam,dparam,sparam);  
   if(e==1){
      int win=ChartWindowFind(0,ShortName);
      ChartIndicatorDelete(0,win,ShortName);
      ChartRedraw();
   }
  
   int me=frm.m_cmb_main.Event(id,lparam,dparam,sparam);
  
   int ce=frm.m_controls.Event(id,lparam,dparam,sparam);

}

我们需要处理指标改变的事件 (me 变量), 以及它参数改变的事件 (ce 变量).

改变指标:

if(me==1){

   // 指标重新载入
  
   _Type=osctype[frm.m_cmb_main.SelectedIndex()]; // 新的类型
  
   delete(osc); // 删除旧对象
   LoadOscillator(); // 载入新的指标
  
   if(!osc.CheckHandle()){
      Alert("载入指标时出错 "+osc.Name());
   }
  
   SetStyles(); // 设置风格
  
   // 设置短名称
   ShortName=ProgName+": "+osc.Name();  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

   // 刷新表单

   frm.SetType(osctype[frm.m_cmb_main.SelectedIndex()]); // 设置类型
   frm.SetValues(_Period1,_Period2,_Period3,_MaMethod,_Price,_Volume,_StPrice); // 设置数值
   frm.Refresh(); // 刷新表单
  
   // 指标重新计算
   EventSetMillisecondTimer(100);

}  

让我们详细检查代码,当从主列表中选择一个指标时,Event() 方法会返回 1,在这种情况下,_Type 变量会被赋予一个新的类型数值, 旧的对象被删除,新的对象被载入,并且会设置风格和短名称。在载入指标的结尾会刷新表单的外观: 设置参数和类型,然后调用 Refresh() 方法来根据新的参数改变表单的外观。在最后会启动计时器 (晚些时候会讨论),

让我们探讨参数被改变部分的代码: 

if(ce==1){
  
   if((int)frm.m_value1.Value()>0){
      _Period1=(int)frm.m_value1.Value();
   }
   if((int)frm.m_value2.Value()>0){
      _Period2=(int)frm.m_value2.Value();
   }
   if((int)frm.m_value3.Value()>0){
      _Period3=(int)frm.m_value3.Value();
   }      
   if(frm.m_method.SelectedIndex()!=-1){
      _MaMethod=e_method[frm.m_method.SelectedIndex()];
   }
   if(frm.m_price.SelectedIndex()!=-1){
      _Price=e_price[frm.m_price.SelectedIndex()];
   }
   if(frm.m_volume.SelectedIndex()!=-1){
      _Volume=e_volume[frm.m_volume.SelectedIndex()];
   }
   if(frm.m_sto_price.SelectedIndex()!=-1){
      _StPrice=e_sto_price[frm.m_sto_price.SelectedIndex()];
   }
  
   delete(osc);
   LoadOscillator();
   if(!osc.CheckHandle()){
      Alert("Error while loading indicator "+osc.Name());
   }  
  
   ShortName=ProgName+": "+osc.Name();  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
  
   EventSetMillisecondTimer(100);
    
}

当参数被改变时,控件类的 Event() 方法返回,. 在这种情况下,所有的变量在确认后都会被赋予新的数值,SpinBox 控件的数值必须大于0,而下拉列表的值不能等于 -1,选择另一个指标的代码都很类似。

关于计时器。指标的计算需要一些时间,所以,会启用一个计时器,并且在它的函数中会使用 BarsCalculated() 来按时检查看指标是否已经准备好,如果返回值大于0,意思就是指标的计算已经完成,而会调用 osc 对象的 Calculate() 方法:

void OnTimer(){
   if(BarsCalculated(osc.Handle())>0){
      if(osc.Calculate(Bars(Symbol(),Period()),0,Label1Buffer,Label2Buffer)!=0){
         ChartRedraw();    
         EventKillTimer();
      }
   }
}

柱数会作为第一个参数传给 Calculate(), 而第二个参数是 0, 它会启用完整的指标重新计算。在图表重绘之后 (ChartRedaraw()),计时器就会关闭。 

现在指标应该可以回应 GUI 了,这意味着指标就要完成了,

加上一点结束步骤: 让我们提供功能来使指标不使用 GUI 也可以工作。为此,加上一个外部变量 UseGUI:

input bool                 UseGUI      =  true;

OnInit() 部分的代码只在 UseGUI 变量被启用的时候才创建表单:

if(UseGUI){
   frm=new CUniOscForm();
   frm.Init();
   int ind=0;
  
   for(int i=0;i<ArraySize(osctype);i++){
      frm.m_cmb_main.AddItem(EnumToString(osctype[i]));
      if(osctype[i]==_Type){
         ind=i;
      }
   }
  
   frm.m_cmb_main.SetSelectedIndex(ind);      
   frm.SetType(_Type);
   frm.SetValues(_Period1,_Period2,_Period3,_MaMethod,_Price,_Volume,_StPrice);
  
   frm.SetSubWindow(0);
   frm.SetPos(10,30);
   frm.Show();
}

还有另外一个小的结束步骤,incGUI 开发库支持修改控件的颜色配置,让我们使用这个功能,

在外部参数后加入以下代码:

enum eColorScheme{
   DefaultScheme=0,
   YellowBrownScheme=1,
   BlueScheme=2,
   GreenScheme=3,
   YellowBlackScheme=4,
   LimeBlackScheme=5,
   AquaBlackScheme=6
};

input eColorScheme ColorScheme=DefaultScheme;

这些代码在指标的属性窗口中加入一个下拉列表用来选择颜色设置,在 OnInit() 函数的开头加上一行:

ClrScheme.SetScheme(ColorScheme);

现在 iUniOscGUI 指标已经全部完成了,而图形界面还可以使用不同的颜色(图 6).

 
图 6. iUniOscGUI 指标的不同 GUI 颜色设置 

结论

最后得到的指标使我们不仅可以比较不同的指标,还可以使您观察到指标的外部参数的影响,指标的外观在您修改它的参数时会立即改变,这种效果在使用属性窗口中不能得到,所以您就无法得到指标参数影响外观的直观印象。

附件

  • UniOscDefines.mqh: 此文件包含了震荡指标类型的枚举;
  • CUniOsc.mqh: 通用震荡指标类;
  • iUniOsc.mq5: 不带有GUI的通用震荡指标;
  • UniOscGUI.mqh: 用于创建震荡指标图形界面的类; 
  • iUniOscGUI.mq5: 带有GUI的通用震荡指标; 
  • IncGUI_v4.mqh: 用于操作图形对象和创建图形界面的开发库;对于库的版本可能有些容易混淆,有两个版本3的文件是同名的: 在文章中以及在代码库中(其中有更新过的类,用于创建表格的 CTable). 除了做过修改,IncGUI_v4 文件还包含了一个用于创建表格的新类 (在代码库中有的)。