A Universal Channel with the Graphical Interface

Dmitry Fedoseev | 5 April, 2017

Contents

Introduction

The previous article described the creation of a universal oscillator with the graphical interface. In that article, we created a very interesting, convenient and useful indicator that considerably simplifies and speeds up chart analysis. In addition to oscillators, there are other types of technical analysis indicators, which might be as interesting as oscillators. They include trend, volatility and volume indicators, as well as other indicators that can be divided into various categories. In this article, we will consider creation of a universal channel indicator.

My previous article about the universal oscillator was quite complex, and was intended for experienced programmers rather than beginners. Since the topic of this article is close to creation of a universal oscillator, we will not repeat general issues here. We will create a universal channel on the basis of the universal oscillator. Thus, even novice programmers can create their universal indicators by modifying the available material, without having to analyze all the nuances of creation of universal indicators with a graphical interface.

Despite the similarity of the universal indicators, there will be serious fundamental differences. All channel indicators are displayed as three lines, including central, top and bottom lines. The drawing principle of the central line is similar to a moving average, while the moving average indicator is mostly used for drawing channels. The top and bottom lines are located at equal distances from the central line. This distance can be determined in points, as percent of price (the Envelopes indicator), using a standard deviation value (Bollinger Bands), or an ATR value (Keltner channel). Hence, the channel indicator will be built using two independent blocks:

  1. Central line calculation block
  2. Channel width defining block (or drawing channel borders)

There are channels of a different type, such as Donchian channel (price channel). Usually it starts with the creation of border lines (price range), and after that the central line value is calculated (in the middle of the range). However, this channel can also be constructed by the above scheme: i.e. first we draw a central line, which is defined as the middle of the price range, and then we draw channel borders at a distance equal to the half of the price range. Of course there will be more calculations than the channel usually requires. However, since the main purpose of this article is to create a universal indicator, we can allow some exceptions, while this approach will increase the number of possible combinations of the central line and borders. For example, we will be able to create an indicator with the central line used in a price channel, with the borders located at a distance of standard deviation like in Bollinger bands etc. 

Types of the Central Line

Various moving averages will be used for the central line. Let's define their types and the number of parameters. All variants of the central line, which will be used in the indicator, are shown in Table 1.

Table 1. Types of the Central Line

Standard
function
Name Parameters
iAMA Adaptive Moving Average 1. int ama_period — AMA period 
2. int fast_ma_period — fast moving average period
3. int slow_ma_period — slow moving average period
4. int ama_shift — horizontal shift of the indicator
5. ENUM_APPLIED_PRICE  applied_price — price type of handle handle 
iDEMA Double Exponential Moving Average 1. int ma_period — averaging period
2. int ma_shift — horizontal shift of the indicator
3. ENUM_APPLIED_PRICE  applied_price — price type
iFrAMA Fractal Adaptive Moving Average 1. int ma_period — averaging period
2. int ma_shift — horizontal shift of the indicator
3. ENUM_APPLIED_PRICE  applied_price — price type
iMA Moving Average 1. int ma_period — averaging period
2. int ma_shift — horizontal shift of the indicator
3. ENUM_MA_METHOD ma_method — smoothing type
4. ENUM_APPLIED_PRICE applied_price — price type
iTEMA Triple Exponential Moving Average 1. int ma_period — averaging period
2. int ma_shift — horizontal shift of the indicator
3. ENUM_APPLIED_PRICE  applied_price — price type
iVIDyA Variable Index Dynamic Average 1. int cmo_period — the Chande Momentum period 
2. int ema_period — the smoothing factor period  
3. int ma_shift — horizontal shift of the indicator 
4. ENUM_APPLIED_PRICE  applied_price — price type
- central line of the price channel 1. int period

Based on the analysis of the "Parameters" column of Table 1, we obtain the required minimum set of parameters (Table 2).

Table 2. The universal set of parameters for the calculation of the central line of the channel 

Type Name
int period1
int period2
int period3
int shift
ENUM_MA_METHOD ma_method 
ENUM_APPLIED_PRICE  price
All parameters of the central line will be displayed with the "c_" prefix in the indicator properties window. 

Types of Borders

Now let us determine the channel border calculation variants (Table 3).

Table 3. Channel width calculation variants 

Standard
function
Name  Parameters
iATR Average True Range 1. int ma_period — averaging period
iStdDev  Standard Deviation 1. int ma_period — averaging period
2. int ma_shift — horizontal shift of the indicator
3. ENUM_MA_METHOD — smoothing type
4. ENUM_APPLIED_PRICE applied_price — price type 
in points  int width — width in points 
percentage (line in Envelopes)  double width — width as percentage of the price   
like in a price channel  double width — scale ratio relative to the actual width of the price channel

Based on the "Parameters" column of Table 3, we obtain a required set of parameters (Table 4).

Table 4. A universal set of parameters for calculating the channel width

Type Name
int period
int  shift 
ENUM_MA_METHOD  ma_method  
ENUM_APPLIED_PRICE  price
double  width  

We need the int variable for calculations in points, but it is not included in table 4, because we can use a double variable instead. This reduces the total number of variables in the properties window.

All border calculation parameters will be displayed with the "w_" prefix in the indicator properties window. 

Central Line Classes

The base class of the central line is similar to CUniOsc class described in Universal Oscillator with GUI in terms of the basic principle and set of methods. Therefore we will use this class and will slightly modify it.

In the MQL5/Include folder, we create the UniChannel folder, copy to it the CUniOsc.mqh file (from Include/UniOsc) and re-name it to CUniChannel.mqh. Leave in the file the base class (COscUni), the child class Calculate1 (its full name is COscUni_Calculate1) and its child class COscUni_ATR. Other classes should be deleted.

Let us rename the classes: replace "COscUni" fragment by "CChannelUni". In order to replace, use the editor function (Main menu — Edit — Find and replace — Replace). But do not use the "Replace all" button. Replace instances one by one to make sure that all replacements are correct.

The central line class always draws one solid line, therefore we will not need many of the base class methods. After deleting unnecessary methods, the following base class is left:

class CChannelUniWidth{
   protected:
      int m_handle;      // indicator handle
      string m_name;     // indicator name
      string m_label1;   // name of buffer 1      
      string m_help;     // a help on indicator parameters
      double m_width;    // channel width
   public:
  
      // constructor
      void CChannelUniWidth(){
         m_handle=INVALID_HANDLE;
      }
      
      // destructor
      void ~CChannelUniWidth(){
         if(m_handle!=INVALID_HANDLE){
            IndicatorRelease(m_handle);
         }
      }
  
      // the main method called from the OnCalculate() function of the indicator
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
         return(rates_total);
      }
      
      // getting the handle of the loaded indicator
      int Handle(){
         return(m_handle);
      }
      
      // handle checking method, which allows to check if the indicator has been loaded successfully  
      bool CheckHandle(){
         return(m_handle!=INVALID_HANDLE);
      }
      
      // getting the indicator name
      string Name(){
         return(m_name);
      }    
      // getting the label text for the buffers
      string Label1(){
         return(m_label1);
      }
      
      // getting the hint on parameters
      string Help(){
         return(m_help);
      }
};

Everything related to the second buffer can be deleted from the Calculate class. After that only one Calculate method will be left:

class CChannelUni_Calculate1:public CChannelUni{
   public:
      // the main method called from the OnCalculate() function of the indicator
      // The first two parameters are similar to the first two parameters 
      // of the OnCalculate() function of the indicator
      // The third parameter is the indicator buffer for the central line
      int Calculate( const int rates_total,    
                     const int prev_calculated,
                     double & buffer0[]
      ){
        
         // determining the number of copied elements
        
         int cnt;
        
         if(prev_calculated==0){
            cnt=rates_total;
         }
         else{
            cnt=rates_total-prev_calculated+1;
         }  
        
         // copying data to the indicator buffer
         if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
            return(0);
         }
        
         return(rates_total);
      }
    };

Let us write the child class using the iMA indicator. The CChannelUni_ATR class from the file should be renamed to CChannelUni_MA. Let us replace the called indicator and delete parts that we do not need. As a result we get the following class:

class CChannelUni_MA:public CChannelUni_Calculate1{
   public:
   // constructor
   // the first two parameters are the same for all child classes
   // then parameters of the loaded indicator 
   void CChannelUni_MA( bool use_default,
                        bool keep_previous,
                        int & ma_period,
                        int & ma_shift,
                        long & ma_method,
                        long & ma_price){
      if(use_default){ // use of default values is selected
         if(keep_previous){
            // no modification of previously used parameters
            if(ma_period==-1)ma_period=14;
            if(ma_shift==-1)ma_shift=0;
            if(ma_method==-1)ma_method=MODE_SMA;
            if(ma_price==-1)ma_price=PRICE_CLOSE;
         }
         else{
            ma_period=14;
            ma_shift=0;
            ma_method=MODE_SMA;
            ma_price=PRICE_CLOSE;            
         }      
      }    
      
      // loading the indicator
      m_handle=iMA(Symbol(),Period(),ma_period,ma_shift,(ENUM_MA_METHOD)ma_method,(ENUM_APPLIED_PRICE)ma_price);
      
      // forming a line with the indicator name
      m_name=StringFormat( "iMA(%i,%i,%s,%s)",
                           ma_period,
                           ma_shift,        
                           EnumToString((ENUM_MA_METHOD)ma_method),              
                           EnumToString((ENUM_APPLIED_PRICE)ma_price)
                        );
      
      // String for the buffer name
      m_label1=m_name;
      // hint on parameters
      m_help=StringFormat( "ma_period - c_Period1(%i), "+
                           "ma_shift - c_Shift(%i), "+
                           "ma_method - c_Method(%s)"+
                           "ma_price - c_Price(%s)",
                           ma_period,
                           ma_shift,
                           EnumToString((ENUM_MA_METHOD)ma_method),
                           EnumToString((ENUM_APPLIED_PRICE)ma_price)
                           );
   }
    };

Let us have a closer look at how strings are formed in variables m_name and mlabel1. Indicator name (the m_name variable) for indicators drawn in a sub-window is shown in the upper left corner of the sub-window. The channel will be displayed on the price chart, its name will not be visible, therefore we will assign to the m_label variable the same detailed name assigned to m_name variable, so that all parameters were shown in the pop-up help when we hover the mouse over the central channel. 

Classes for all other standard indicators are created similar to iMA. An exception is the price channel. Since the price channel is not included into the set of standard terminal indicators, we need to calculate it. Two options are possible:

  1. Creating a child class of Calculate type and performing calculations in it
  2. Creating an additional indicator and calling it through the iCustom function
Both options can be equally used. In the first case, there will be less files, on which the indicator created in the article would depend, but the same calculations need to be performed multiple times (first we determine the channel border in order to calculate the central line, and then we again determine the channel borders in order to determine the channel width). In the second case, calculations will not be repeated. In addition, we get an additional complete price channel indicator, which can be used independently.      

The below attachment contains the CUniChannel.mqh file with child classes for all other indicators and the iPriceChannel indicator. The data of the central line of the iPriceChannel indicator are located in buffer 0. If anyone needs to further modify the class for any other indicator, whose required data are located in a non-zero buffer, then it will be necessary to create another Calculate subclass or to create a variable for the buffer index in the base class and assign the desired value to it in the constructor of the subclass.   

Classes for calculating the width and drawing the channel

Let us again use CUniChannel as the basis of our base class. The indicator buffer with the values of the central line and two buffers for the channel borders that will be filled with values calculated in the method, will be passed to the Calculate class method. In contrast to CUniChannel, here we will have separate Calculate child classes for each border calculation option. These subclasses will load indicators, and the names of indicators and buffers will be formed in them. We also need to slightly revise the base class: to add a variable for the channel width - the value of the variable will be set through the child class constructor.

Let us save the CUniChannel.mqh filde as CUniChannelWidth.mqh and modify it. First we delete all child classes, leaving only the base class and Calculate. Rename CChannelUni to CChannelUniWidth (do not forget about the constructor, the destructor and the parent name in the child class, which also need to be changed). The resulting file is the following:

class CChannelUniWidth{
   protected:
      int m_handle;           // indicator handle
      string m_name;          // indicator name
      string m_label1;        // name of buffer 1      
      string m_help;          // a hint on indicator parameters
      double m_width;         // channel width
   public:
  
      // constructor
      void CChannelUniWidth(){
         m_handle=INVALID_HANDLE;
      }
      
      // destructor
      void ~CChannelUniWidth(){
         if(m_handle!=INVALID_HANDLE){
            IndicatorRelease(m_handle);
         }
      }
  
      // the main method called from the OnCalculate() function of the indicator
      virtual int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
         return(rates_total);
      }
      
      // getting the handle of the loaded indicator
      int Handle(){
         return(m_handle);
      }
      
      // handle checking method, which allows to check if the indicator has been loaded successfully  
      bool CheckHandle(){
         return(m_handle!=INVALID_HANDLE);
      }
      
      // getting the indicator name
      string Name(){
         return(m_name);
      }    
      // getting the label text for the buffers
      string Label1(){
         return(m_label1);
      }
      
      // getting the hint on parameters
      string Help(){
         return(m_help);
      }
};
  

Let us rename the CChannelUni_Calculate class to CChannelUni_Calculate_ATR and add a constructor to it. The constructor can be taken from the COscUni_ATR class of the universal oscillator, but we will need to rename it add the width parameter to it. What else needs to be changed: we need to add generation of the indicator and buffer names. Finally, the class for calculating the borders based on ATR is as follows:

class CChannelUni_Calculate_ATR:public CChannelUniWidth{
   public:
      // constructor
      // the first two parameters are standard for all child classes 
      // followed by parameters of the loaded indicator
      // the last parameter is the channel width
      void CChannelUni_Calculate_ATR(bool use_default,
                                     bool keep_previous,
                                     int & ma_period,
                                     double & ch_width){
         if(use_default){ // use of default values is selected
            if(keep_previous){ // no modification of previously used parameters
               if(ma_period==-1)ma_period=14;
               if(ch_width==-1)ch_width=2;
            }
            else{
               ma_period=14;
               ch_width=2;
            }      
         } 
         
         // saving width parameters for use in the calculation metho  
         m_width=ch_width; 
         // loading the indicator
         m_handle=iATR(Symbol(),Period(),ma_period);
         // forming a line with the indicator name
         m_name=StringFormat("ATR(%i)",ma_period);
         // string with the names of the buffers
         m_label1=m_name;
         // hint on parameters 
         m_help=StringFormat("ma_period - Period1(%i)",ma_period); // a hint   
      }   
      
      // the main method called from the OnCalculate() function of the indicator
      // the first two parameters are similar to the first two parameters
      // of OnCalculate() function
      // then indicator buffers are passed 
      int Calculate( const int rates_total,
                     const int prev_calculated,
                     double & bufferCentral[],
                     double & bufferUpper[],
                     double & bufferLower[],
      ){
      
         // defining the beginning of calculation
         
         int start;
         
         if(prev_calculated==0){
            start=0; 
         }
         else{ 
            start=prev_calculated-1;
         }  
         // the main loop of calculation and buffer filling 
         for(int i=start;i<rates_total;i++){
            
            // getting indicator data for the calculated bar
            double tmp[1];
            if(CopyBuffer(m_handle,0,rates_total-i-1,1,tmp)<=0){
               return(0);
            }
            
            // multiplying by the width parameter
            tmp[0]*=m_width;   
            // calculating the values of the upper and lower borders
            bufferUpper[i]=bufferCentral[i]+tmp[0];
            bufferLower[i]=bufferCentral[i]-tmp[0];
         }   
         
         return(rates_total);
      }
        };

Please note, the ATR indicator value of only one bar is copied within the main loop. This variant is much slower than copying a value series to a buffer. But this approach saves us one indicator buffer, while speed loss would only be visible when attaching an indicator to a chart manually. But a delay of a few tenths of a second is not essential for the user. In the strategy tester, a small number of bars is available on the chart at the beginning of testing, therefore the loss of time while copying data of each bar will not be noticeable.

Some of the channel width calculation options do not require additional indicators — e.g. if the width is set in points or for the Envelope channel. In this case we will assign 0 to the m_handle variable of the base class (it differs from the INVALID_HANDLE value).

The below attachment contains the CUniChannelWidth.mqh file with child classes for other channel calculation options.   

Creating a Universal Channel Indicator

Now that we have prepared the above classes, we can create a universal channel indicator, though without a graphical interface yet.

Let us create the new iUniChannel custom indicator in the editor. When creating an indicator in the MQL5 Wizard, select the following functions: OnCalculate(...,open,high,low,close), OnTimer, OnChartEvent, create three buffers of the Line type.

We need to create two enumerations to select the central line and the channel type. Enumerations will be located in the UniChannelDefines.mqh file. Creating enumerations in accordance with tables 1 and 3:

// enumeration of central line types
enum ECType{
   UniCh_C_AMA,
   UniCh_C_DEMA,
   UniCh_C_FrAMA,
   UniCh_C_MA,
   UniCh_C_TEMA,
   UniCh_C_VIDyA,
   UniCh_C_PrCh
};
// enumeration of border types
enum EWType{
   UniCh_W_ATR,
   UniCh_W_StdDev,
   UniCh_W_Points,
   UniCh_W_Percents,
   UniCh_W_PrCh
            };

The enumeration of central line types is called ECType, and the enumeration of channel width types is EWType. Connect a file with the enumerations and two previously created files with classes to the indicator:

#include <UniChannel/UniChannelDefines.mqh>
#include <UniChannel/CUniChannel.mqh>
#include <UniChannel/CUniChannelWidth.mqh>
    

Now we declare two external variables to select the central line type and the channel width, and variables for parameters in accordance with tables 2 and 4:

// central line parameters
input ECType               CentralType   =  UniCh_C_MA;
input int                  c_Period1     =  5;
input int                  c_Period2     =  10;
input int                  c_Period3     =  15;
input int                  c_Shift       =  0;
input ENUM_MA_METHOD       c_Method      =  MODE_SMA;
input ENUM_APPLIED_PRICE   c_Price       =  PRICE_CLOSE;
// border parameters
input EWType               WidthType     =  UniCh_W_StdDev;
input int                  w_Period      =  20;
input int                  w_Shift       =  0;
input ENUM_MA_METHOD       w_Method      =  MODE_SMA;
input ENUM_APPLIED_PRICE   w_Price       =  PRICE_CLOSE;
      input double               w_Width       =  2.0;

Let us declare two variables, which will be internal as for now, and will later be displayed in the properties window in the indicator version with a GUI:

bool                 UseDefault  =  false;
      bool                 KeepPrev    =  false;

The purpose of these variables is described in detail in the article about the universal oscillator: the UseDefault variable enables the mode, in which each newly selected indicator is loaded with default settings, and the KeepPrev variable enables the mode of preserving parameters when switching indicators. In the indicator version without a GUI, the indicator is loaded with parameters from the properties window, so the UseDefault value is false. KeepPrev is also set to false, since the GUI does not exist, and there is no indicator switch. 

Parameters need to be prepared during indicator initialization. Just like in the universal oscillator, we will prepare parameters in a separate function PrepareParameters(), but first we will create copies of all external variables:

ECType               _CentralType;
int                  _ma_Period1;
int                  _ma_Period2;
int                  _ma_Period3;
int                  _ma_Shift;
long                 _ma_Method;
long                 _ma_Price;
EWType               _WidthType;
int                  _w_Period;
int                  _w_Shift;
long                 _w_Method;
long                 _w_Price;
      double               _w_Width;

Then we write the parameter preparing function:

void PrepareParameters(){
   _CentralType=CentralType;
   _WidthType=WidthType;
  
   if(UseDefault && KeepPrev){
      _c_Period1=-1;
      _c_Period2=-1;
      _c_Period3=-1;
      _c_Shift=0;
      _c_Method=-1;
      _c_Price=-1;
      _w_Period=-1;
      _w_Shift=0;
      _w_Method=-1;
      _w_Price=-1;
      _w_Width=-1;
   }
   else{  
      _c_Period1=c_Period1;
      _c_Period2=c_Period2;
      _c_Period3=c_Period3;
      _c_Shift=c_Shift;
      _c_Method=c_Method;
      _c_Price=c_Price;
      _w_Period=w_Period;
      _w_Shift=w_Shift;
      _w_Method=w_Method;
      _w_Price=w_Price;
      _w_Width=w_Width;
   }
            }

Note that if the UseDefault && KeepPrev condition is met, all variables are set to -1, and 0 is assigned to the Shift variables, because the values ​​of these variables are not set from indicator objects, but are only set from the user interface (indicator properties window or GUI).   

After preparing parameters, we can create objects for calculating the central line and the channel. In the universal oscillator, the LoadOscillator() function was used for that purpose. Here we will have two functions: LoadCentral() and LoadWidth(), but first we need to declare pointers:

CChannelUni * central;
            CChannelUniWidth * width;

Some indicators have a parameter for horizontal shift, but some indicators do not have it, though all indicators can be shifted. Therefore we declare an additional variable shift0 with a value of 0, and will pass it in the class constructor. The shift will be performed by shifting indicator buffers.

The LoadCentral() function:

void LoadCentral(){
   switch(_CentralType){ // an appropriate class is created depending on the selected type
      case UniCh_C_AMA:
         central=new CChannelUni_AMA(  UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       _c_Period2,
                                       _c_Period3,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_DEMA:
         central=new CChannelUni_DEMA( UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_FrAMA:
         central=new CChannelUni_FrAMA(UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_MA:
         central=new CChannelUni_MA(   UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Method,
                                       _c_Price);
      break;
      case UniCh_C_TEMA:
         central=new CChannelUni_TEMA( UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_VIDyA:
         central=new CChannelUni_VIDyA(UseDefault,
                                       KeepPrev,
                                       _c_Period1,
                                       _c_Period2,
                                       shift0,
                                       _c_Price);
      break;
      case UniCh_C_PrCh:
         central=new CChannelUni_PriceChannel(  UseDefault,
                                                KeepPrev,
                                                _c_Period1);
      break;
   }
}
    

One of the channel width calculation options (the CChannelUni_Calculate_InPoints class) includes a parameter measured in points, and the class provides the possibility to adjust the parameter value in accordance with the number of decimal places in symbol quotes. For the adjustment function to operate, it is necessary to pass the parameter multiplier to the class constructor when creating an object. For 2- and 4-digit quotes, the multiplier will be equal to 1, for 3- and 5-digit quotes it is 10. In the external parameters, we declare Auto5Digits variable of the bool type:

input bool                 Auto5Digits   =  true;

If Auto5Digits is true, the parameter will be corrected, if false - the value will be used as is. Below Auto5Digits, we declare one more variable for the multiplier:

int mult;

At the beginning of the OnInit() function, we calculate the value of mult:

   if(Auto5Digits && (Digits()==3 || Digits()==5)){
      mult=10; // parameters in points will be multiplied by 10
   }
   else{
      mult=1; // parameters in points will not be corrected
         }

Now we write the LoadWidth() function:

void LoadWidth(){
   switch(_WidthType){ // an appropriate class is created depending on the selected type
      case UniCh_W_ATR:
         width=new CChannelUni_Calculate_ATR(UseDefault,KeepPrev,_w_Period,_w_Width);
      break;
      case UniCh_W_StdDev:
         width=new CChannelUni_Calculate_StdDev(UseDefault,KeepPrev,_w_Period,shift0,_w_Method,_w_Price,_w_Width);
      break;
      case UniCh_W_Points:
         width=new CChannelUni_Calculate_InPoints(UseDefault,KeepPrev,_w_Width,mult);
      break;
      case UniCh_W_Percents:
         width=new CChannelUni_Calculate_Envelopes(UseDefault,KeepPrev,_w_Width);
      break;
      case UniCh_W_PrCh:
         width=new CChannelUni_Calculate_PriceChannel(UseDefault,KeepPrev,_w_Period,_w_Width);
      break;
   }
            }

After creating each of the objects (central line and width), we need to check if they have been successfully created. If the objects are created, set the short name of the indicator and show a help info on parameters using the Print() function: 

Print("Central line parameters matching:",central.Help());
      Print("Width parameters matching:",width.Help());  

Labels and shift for buffers will be set using the SetStyles() function:

void SetStyles(){
   // buffer names
   PlotIndexSetString(0,PLOT_LABEL,"Central: "+central.Label1());
   PlotIndexSetString(1,PLOT_LABEL,"Upper: "+width.Label1());  
   PlotIndexSetString(2,PLOT_LABEL,"Lower: "+width.Label1());  
  
   // shift of buffers
   PlotIndexSetInteger(0,PLOT_SHIFT,_c_Shift);
   PlotIndexSetInteger(1,PLOT_SHIFT,_w_Shift);
   PlotIndexSetInteger(2,PLOT_SHIFT,_w_Shift);
            }

As a result, we get the following OnInit() function:

int OnInit(){
  
   // preparing a multiplier to adjust the parameter in points
   if(Auto5Digits && (Digits()==3 || Digits()==5)){
      mult=10;
   }
   else{
      mult=1;
   }
  
   // preparing parameters
   PrepareParameters();
  
   // loading the central line indicator
   LoadCentral();
  
   // checking if the central line has been loaded successfully 
   if(!central.CheckHandle()){
      Alert("Central line error "+central.Name());
      return(INIT_FAILED);
   }    
  
   // loading the width calculating indicator
   LoadWidth();
  
   // checking if the width calculating indicator has been loaded successfully
   if(!width.CheckHandle()){
      Alert("Width error "+width.Name());
      return(INIT_FAILED);
   }      
   // showing hint on parameters
   Print("Central line parameters matching: "+central.Help());
   Print("Width parameters matching: "+width.Help());  
  
   // setting the name
   ShortName="iUniChannel";  
   IndicatorSetString(INDICATOR_SHORTNAME,ShortName);  
  
   // a standard part of the OnInit function
   SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA);
   SetIndexBuffer(1,Label2Buffer,INDICATOR_DATA);
   SetIndexBuffer(2,Label3Buffer,INDICATOR_DATA);
  
   // setting labels and buffer shift
   SetStyles();
   return(INIT_SUCCEEDED);
            }

At this point, we can assume that the indicator without a graphical interface is complete. It can be used to test the operation of all classes and parameters, after which we can proceed to creating a graphical interface.

During testing, some inconveniences were revealed in the indicator operation. One of the disadvantages is the separate management of the central line period and width calculation period. Of course, such a control can widen the possibilities of the indicator, but in some cases a simultaneous control of the two periods using a single parameter may be needed. Let us make a minor change to make the channel width period equal to one of the three periods of the central line. One of the four available options will be selected using an enumeration (located in the UniChannelDefines.mqh file):

enum ELockTo{
   LockTo_Off,
   LockTo_Period1,
   LockTo_Period2,
   LockTo_Period3
            };

When LockTo_Off option is selected, periods are adjusted separately. In all other cases the value of the w_Period parameter is equal to the corresponding period of the central line. Let us declare a variable of type ELockTo immediately after the w_Period variable:

input ELockTo              w_LockPeriod  =  LockTo_Off;

We add the following code at the bottom of the PrepareParameters() function:

switch(w_LockPeriod){ // depending on the lock type
   case LockTo_Period1:
      _w_Period=_c_Period1;
   break;
   case LockTo_Period2:
      _w_Period=_c_Period2;      
   break;
   case LockTo_Period3:
      _w_Period=_c_Period3;      
   break;
            }

Another disadvantage is that the messages about the compliance of parameters are displayed in the "Experts" tab in one row, and part of it is not shown on narrow screens. Let us modify the code to display the information in a column. Instead of Print, we will use our own PrintColl() function. Two parameters are passed to this function: the caption and a help string. In this function, the help string is split into parts, which are printed separately:

void PrintColl(string caption,string message){
   Print(caption); // print the caption
   string res[];
   // splitting the message
   int cnt=StringSplit(message,',',res);
   // printing the message in parts
   for(int i=0;i<cnt;i++){
      StringTrimLeft(res[i]);
      Print(res[i]);
   }
            }

Accordingly, two help printing lines are changed in the OnInit() function:

PrintColl("Central line parameters matching:",central.Help());
            PrintColl("Width parameters matching:",width.Help());    

Now the indicator is completely ready, its file name in the attachment is "iUniChanhel". Now let us create the graphical interface.   

Creating Graphical Interface Classes

The graphical interface will be based on the graphical interface of the universal oscillator. Copy UniOsc/UniOscGUI.mqh to the UniChannel folder and change its name to UniChannelGUI.mqh. The graphical interface of the universal channel will differ much from the universal oscillator interface, so we well need to do much work here.

The main difference is that the in the universal channel requires an independent choice of two indicators (the central line and borders), so there should be two main indicator selection lists. The first list should be followed by controls to manage central line parameters. Then comes the second list and controls for border parameter. Hence, the second list does not have fixed coordinates, they must be calculated. In addition to the two type selection lists, the form should always have two input fields for offset values. Coordinates of the fields are also not fixed. Another important point is a list for selecting a variable corresponding to the w_LockPeriod parameter. In all cases where we want to display the w_Period parameter input field in the group of width controls, an additional drop-down list should be displayed.

First we make general changes in the UniChannelGUI.mqh file:

1. The path to the file with enumerations:

#include <UniOsc/UniOscDefines.mqh>

needs to be replaced by the following:

#include <UniChannel/UniChannelDefines.mqh>

2. Add an array with the ELockTo enumeration values:

ELockTo e_lockto[]={LockTo_Off,LockTo_Period1,LockTo_Period2,LockTo_Period3};
  

3. Delete arrays with the ENUM_APPLIED_VOLUME and ENUM_STO_PRICE enumerations.

We now proceed to change the CUniOscControls class.  

A class of central line controls

1. The CUniOscControls class is re-named to CUniChannelCentralControls.

2. Declaration of variables m_volume and m_sto_price should be deleted from the class. Accordingly, we delete everything associated with these controls from the SetPointers(), Hide(), and Events() methods.

3. Add the m_last_y variable, in which the Y coordinate of the last control from the group will be recorded. Add a method for receiving the value of this variable — GetLastY(). We no longer need the  FormHeight() method, so let us delete it. Instead, we need to add the ControlsCount() method that will return the number of controls in the child class. This number will be used to calculate the height of the form.

Here is the resulting parent class:

class CUniChannelCentralControls{
   protected:
      CSpinInputBox * m_value1; // for period 1
      CSpinInputBox * m_value2; // for period 2
      CSpinInputBox * m_value3; // for period 3
      CComBox * m_price;        // for the price
      CComBox * m_method;       // for the method
      int m_last_y;             // the Y coordinate of the last control
   public:
  
   // getting the Y coordinate of the last control
   int GetLastY(){
      return(m_last_y);
   }
  
   // method for passing object pointers to the object
   void SetPointers(CSpinInputBox & value1,
                        CSpinInputBox & value2,      
                        CSpinInputBox & value3,
                        CComBox & price,
                        CComBox & method){
      m_value1=GetPointer(value1);
      m_value2=GetPointer(value2);      
      m_value3=GetPointer(value3);            
      m_price=GetPointer(price);
      m_method=GetPointer(method);
   }
  
   // hiding the group of controls
   void Hide(){
      m_value1.Hide();
      m_value2.Hide();
      m_value3.Hide();
      m_price.Hide();
      m_method.Hide();
   }
  
   // event handling
   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);
      if(e1!=0 || e2!=0 || e3!=0 || e4!=0 || e5!=0){
         return(1);
      }
      return(0);
   }
  
   // methods for the initialization of controls (to change labels) 
   virtual void InitControls(){
   }  
  
   // displaying the group of controls
   virtual void Show(int x,int y){
   }  
  
   // getting the number of controls in the group
   virtual int ControlsCount(){
      return(0);
   }      
        };

Let us modify the child CUniOscControls_ATR class:

1. Change its name to  CUniChannelCentralControls_AMA, this will be a class for the AMA indicator.

2. In accordance with the "Parameters" column of Table 1, we initialize all controls in the InitControls() method, and in the Show() method we call the Show() methods of all controls. The value of the last control should be assigned to the m_last_y variable.

3. Now we delete the FormHeight() method and add ControlsCount() instead.

Here is the resulting class:

class CUniChannelCentralControls_AMA:public CUniChannelCentralControls{
   void InitControls(){
      // initialization of controls
      m_value1.Init("c_value1",SPIN_BOX_WIDTH,1," ama_period");
      m_value2.Init("c_value2",SPIN_BOX_WIDTH,1," fast_ma_period");      
      m_value3.Init("c_value3",SPIN_BOX_WIDTH,1," slow_ma_period");      
   }
  
   // display 
   void Show(int x,int y){
      m_value1.Show(x,y);
      y+=20;
      m_value2.Show(x,y);
      y+=20;
      m_value3.Show(x,y);
      y+=20;
      m_price.Show(x,y);
      m_last_y=y;
   }
  
   // getting the number of controls in the group
   int ControlsCount(){
      return(4);
   }
        };

Similarly, we create classes for the rest of indicators used for the central line, and delete all unnecessary oscillator child classes.

Classes for Width Calculation Controls

Based on the CUniChannelCentralControls class, we create a class for managing the channel width parameters. Make a copy of the CUniChannelCentralControls class and change its name to CUniChannelWidthControls. In this class, we need two input fields (period and width), two standard enumerations for the averaging type and price, and the w_LockPeriod parameter enumeration. As a result we get the following class:

class CUniChannelWidthControls{
   protected:
      CSpinInputBox * m_value1; // for the period
      CSpinInputBox * m_value2; // for the width
      CComBox * m_price;        // for the price
      CComBox * m_method;       // for the method
      CComBox * m_lockto;       // for the lock type
      int m_last_y;             // the Y coordinate of the last control 
   public:
  
   // getting the Y coordinate of the last control
   int GetLastY(){
      return(m_last_y);
   }
  
   // method for passing object pointers to the object
   void SetPointers(CSpinInputBox & value1,
                        CSpinInputBox & value2,      
                        CComBox & price,
                        CComBox & method,
                        CComBox & lockto){
      m_value1=GetPointer(value1);
      m_value2=GetPointer(value2);      
      m_price=GetPointer(price);
      m_method=GetPointer(method);
      m_lockto=GetPointer(lockto);      
   }
  
   // hiding the group of controls
   void Hide(){
      m_value1.Hide();
      m_value2.Hide();
      m_price.Hide();
      m_method.Hide();
   }
  
   // event handling
   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 e4=m_price.Event(id,lparam,dparam,sparam);
      int e5=m_method.Event(id,lparam,dparam,sparam);
      int e6=m_lockto.Event(id,lparam,dparam,sparam);      
      if(e1!=0 || e2!=0 || e4!=0 || e5!=0 || e6){
         return(1);
      }
      return(0);
   }
  
   // methods for the initialization of controls (to change labels) 
   virtual void InitControls(){
   }  
  
   // displaying the group of controls
   virtual void Show(int x,int y){
   }  
  
   // getting the number of controls in the group
   virtual int ControlsCount(){
      return(0);
   }    
        };

Let us create its child classes. The main difference from the central line class is that after the period input field we need to create a dropdown list for the w_LockPeriod parameter. Here is such class for width calculation using ATR:

class CUniChannelWidthControls_ATR:public CUniChannelWidthControls{
   void InitControls(){
      // Control initialization
      m_value1.Init("w_value1",SPIN_BOX_WIDTH,1," period");
   }
  
   // Displaying the group of controls
   void Show(int x,int y){
      m_value1.Show(x,y);
      y+=20;
      m_lockto.Show(x,y);
      m_last_y=y;
   }  
  
   // Getting the number of controls in the group
   int ControlsCount(){
      return(2);
   }    
        };

Classes for all other variants of channel width calculation are similar to the above one. 

Now the UniChannelGUI.mqh file contains two basic classes of controls, their child classes and a form class. The latter needs to be adjusted. Due to the large size, working with the file might be inconvenient, therefore we will prepare classes of controls in other files. Let us create the UniChannel/CUniChannelCentralControls.mqh file and move the CUniChannelCentralControls class and all its child classes to this file. We also include additional files to it: 

#include <IncGUI_v4.mqh>
    #include <UniChannel/UniChannelDefines.mqh>

Definitions of constants FORM_WIDTH, SPIN_BOX_WIDTH, COMBO_BOX_WIDTH will be moved to the UniChannelDefines.mqh file. After that the CUniChannelCentralControls file can be compiled in order to check errors. The CUniChannelWidthControls class should also be moved to a separate file. After that working with the form class will be more convenient. 

The Form Class

Include the two newly created files to UniChannelGUI.mqh:

#include <UniChannel/CUniChannelCentralControls.mqh>
    #include <UniChannel/CUniChannelWidthControls.mqh>

Re-name CUniOscForm to CUniChannelForm. In the public section, we delete the CUniOscControls type pointer. Instead, we declare two pointer variables: CUniChannelCentralControls and CUniChannelWidthControls, and determine other controls of the form class. The following variables will be located in the public section:

CComBox           m_c_cmb_main;  // central line selection list
CSpinInputBox     m_c_value1;    // input field for period 1
CSpinInputBox     m_c_value2;    // input field for period 2
CSpinInputBox     m_c_value3;    // input field for period 3
CComBox           m_c_price;     // price selection list
CComBox           m_c_method;    // method selection list
CSpinInputBox     m_c_shift;     // shift input fields
CComBox           m_w_cmb_main;  // border selection list
CSpinInputBox     m_w_value1;    // period input field
CSpinInputBox     m_w_value2;    // width input field
CComBox           m_w_price;     // price selection field
CComBox           m_w_method;    // method selection field
CComBox           m_w_lockto;    // lock option selection field     
CSpinInputBox     m_w_shift;     // shift input field          
// the group of central line controls
CUniChannelCentralControls * m_central_controls;
// the group of border controls
        CUniChannelWidthControls * m_width_controls;  

In the MainProperties() method, we change the values of the m_Name and m_Caption variables, all other variables remain unchanged:

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

In the OnInitEvent() method, we call the Init() methods of all controls with unchanging labels (a set of controls corresponding to the selected indicator) and fill in the drop-down lists:

void OnInitEvent(){
   // initialization of controls that are not included into the group
  
   m_c_cmb_main.Init("cb_c_main",COMBO_BOX_WIDTH," select central");
   m_w_cmb_main.Init("cb_w_main",COMBO_BOX_WIDTH," select bands");
   m_c_price.Init("c_price",COMBO_BOX_WIDTH," price");
   m_c_method.Init("c_method",COMBO_BOX_WIDTH," method");
   m_c_shift.Init("c_shift",COMBO_BOX_WIDTH,1," shift");    
  
   m_w_price.Init("w_price",COMBO_BOX_WIDTH," price");
   m_w_method.Init("w_method",COMBO_BOX_WIDTH," method");
   m_w_shift.Init("w_shift",COMBO_BOX_WIDTH,1," shift");    
  
   m_w_lockto.Init("cb_w_lockto",COMBO_BOX_WIDTH," lock period");
   m_w_value2.Init("w_value2",SPIN_BOX_WIDTH,0.001," width");
  
   // filling the drop-down lists
  
   for(int i=0;i<ArraySize(e_price);i++){
      m_c_price.AddItem(EnumToString(e_price[i]));
      m_w_price.AddItem(EnumToString(e_price[i]));
   }
   for(int i=0;i<ArraySize(e_method);i++){
      m_c_method.AddItem(EnumToString(e_method[i]));
      m_w_method.AddItem(EnumToString(e_method[i]));
   }            
   for(int i=0;i<ArraySize(e_lockto);i++){
      m_w_lockto.AddItem(EnumToString(e_lockto[i]));            
   }
  
   // allow typing the shift value using keyboard            
   m_c_shift.SetReadOnly(false);
   m_w_shift.SetReadOnly(false);                        
        }  

In the OnShowEvent() methods, we display controls. After the display of groups of elements, we obtain the Y coordinate and display the following controls in accordance with this coordinate:

void OnShowEvent(int aLeft, int aTop){
   m_c_cmb_main.Show(aLeft+10,aTop+10);        // central line type selection list
   m_central_controls.Show(aLeft+10,aTop+30);  // a group of central line parameters controls
   int m_y=m_central_controls.GetLastY();      // getting the coordinate of the last control
   m_c_shift.Show(aLeft+10,m_y+20);            // the shift parameter input field
   m_w_cmb_main.Show(aLeft+10,m_y+40);         // the channel selection list
   m_width_controls.Show(aLeft+10,m_y+60);     // a group of channel parameters controls
   m_y=m_width_controls.GetLastY();            // getting the coordinate of the last control
   m_w_value2.Show(aLeft+10,m_y+20);           // the width input field
   m_w_shift.Show(aLeft+10,m_y+40);            // the shift parameter input field
        }

Controls can be hidden in the OnHideEvent() method:

void OnHideEvent(){
   m_c_cmb_main.Hide();       // central line type selection list     
   m_central_controls.Hide(); // a group of central line parameters controls
   m_c_shift.Hide();          // the shift parameter input field
   m_w_cmb_main.Hide();       // channel selection field
   m_width_controls.Hide();   // a group of channel parameters controls
   m_w_shift.Hide();          // the shift parameter input field
   m_w_lockto.Hide();         // period lock type selection
   m_width_controls.Hide();   // width input field
    }

Make changes in the SetValues() method. Changing the set of the method parameters, set the values corresponding to these parameters to all controls in the method:

void SetValues(int c_value1,
               int c_value2,
               int c_value3,
               long c_method,
               long c_price,
               long c_shift,                    
               int w_value1,
               int w_value2,
               long w_method,
               long w_price,
               long w_lockto,
               long w_shift  
){
   // central line input field
   m_c_value1.SetValue(c_value1);
   m_c_value2.SetValue(c_value2);      
   m_c_value3.SetValue(c_value3);
   m_c_shift.SetValue(c_shift);        
   // channel parameters input fields
   m_w_value1.SetValue(w_value1);
   m_w_value2.SetValue(w_value2);        
   m_w_shift.SetValue(w_shift);            
  
   // display of selected types in the smoothing method selection lists
   for(int i=0;i<ArraySize(e_method);i++){
      if(c_method==e_method[i]){
         m_c_method.SetSelectedIndex(i);
      }
      if(w_method==e_method[i]){
         m_w_method.SetSelectedIndex(i);
      }            
   }
  
   // display of selected types in the price type selection lists
   for(int i=0;i<ArraySize(e_price);i++){
      if(c_price==e_price[i]){
         m_c_price.SetSelectedIndex(i);
      }
      if(w_price==e_price[i]){
         m_w_price.SetSelectedIndex(i);
      }            
   }
   // display of the selected type of channel period lock
   for(int i=0;i<ArraySize(e_lockto);i++){
      if(w_lockto==e_lockto[i]){
         m_w_lockto.SetSelectedIndex(i);
         break;
      }
   }                    
        }  

Instead of SetType(), we create two methods: SetCentralType() for setting the central line type, and SetWidthType() for setting the type of borders. At the end of each method, once objects are created, properties are set to controls, allowing to enter values using a keyboard. We also set the minimum allowable values and call a private method for the form height calculation:  

Метод SetCentralType():

void SetCentralType(long type){
   // If an object has already been created, we need to delete it
   if(CheckPointer(m_central_controls)==POINTER_DYNAMIC){
      delete(m_central_controls);
      m_central_controls=NULL;
   }
   switch((ECType)type){ // create an object depending on the selected class
      case UniCh_C_AMA:
         m_central_controls=new CUniChannelCentralControls_AMA();
      break;
      case UniCh_C_DEMA:
         m_central_controls=new CUniChannelCentralControls_DEMA();            
      break;
      case UniCh_C_FrAMA:
         m_central_controls=new CUniChannelCentralControls_FrAMA();            
      break;
      case UniCh_C_MA:
         m_central_controls=new CUniChannelCentralControls_MA();            
      break;
      case UniCh_C_TEMA:
         m_central_controls=new CUniChannelCentralControls_TEMA();            
      break;
      case UniCh_C_VIDyA:
         m_central_controls=new CUniChannelCentralControls_VIDyA();            
      break;
      case UniCh_C_PrCh:
         m_central_controls=new CUniChannelCentralControls_PrCh();            
      break;
   }    
  
   // passing pointers to control objects
   m_central_controls.SetPointers(m_c_value1,m_c_value2,m_c_value3,m_c_price,m_c_method);
   // initialization of group controls
   m_central_controls.InitControls();
  
   // allowing entering values from keyboard
   m_c_value1.SetReadOnly(false);
   m_c_value2.SetReadOnly(false);
   m_c_value3.SetReadOnly(false);
  
   // setting the minimum allowed values
   m_c_value1.SetMinValue(1);        
   m_c_value2.SetMinValue(1);
   m_c_value3.SetMinValue(1);            
  
   // calculating the height of the form
   this.SolveHeight();
        }

 The SetWidthType() method:

void SetWidthType(long type){
   // If an object has already been created, we need to delete it
   if(CheckPointer(m_width_controls)==POINTER_DYNAMIC){
      delete(m_width_controls);
      m_width_controls=NULL;
   }
   switch((EWType)type){ // creating an object depending on the selected class
      case UniCh_W_ATR:
         m_width_controls=new CUniChannelWidthControls_ATR();
      break;
      case UniCh_W_StdDev:
         m_width_controls=new CUniChannelWidthControls_StdDev();            
      break;
      case UniCh_W_Points:
         m_width_controls=new CUniChannelWidthControls_InPoints();            
      break;
      case UniCh_W_Percents:
         m_width_controls=new CUniChannelWidthControls_Envelopes();            
      break;
      case UniCh_W_PrCh:
         m_width_controls=new CUniChannelWidthControls_PrCh();                        
      break;
   }    
   // passing pointers to control objects
   m_width_controls.SetPointers(m_w_value1,m_w_value2,m_w_price,m_w_method);
   // initialization of group controls
   m_width_controls.InitControls();
  
   // setting the minimum allowed values
   m_w_value1.SetReadOnly(false);
   m_w_value2.SetReadOnly(false);
  
   // setting the minimum allowed values
   m_w_value1.SetMinValue(1);        
   m_w_value2.SetMinValue(0);
  
   // calculating the height of the form
   this.SolveHeight();
              
        }  

At the end of the SetCentralType() and SetWidthType() methods, the SolveHeight() method calculating the form height is called :

void SolveHeight(){
   // if both objects exist (central line and width)
   if(CheckPointer(m_central_controls)==POINTER_DYNAMIC && CheckPointer(m_width_controls)==POINTER_DYNAMIC){
      m_Height=(m_width_controls.ControlsCount()+m_central_controls.ControlsCount()+6)*20+10;
   }      
        }  

We proceed to connecting the indicator and the graphical user interface.  

Connecting the Indicator and the Graphical Interface

Let us save the iUniChannel indicator as  iUniChannelGUI. Similar to the iUniOscGUI inbdicator, we add an external UseGUI parameter at the very top of its properties window. After that we add variables UseDefault and KeepPrev, set them to true by default and display them in the properties window:

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

Include the file with a graphical interface (where the files with indicator classes are included):

#include <UniChannel/UniChannelGUI.mqh>

At the very bottom of the OnInit() function, we add a GUI boot code. But before doing so, we need arrays with the types of the central line and borders. We add them below the external parameters of the indicator:

// array of the central line types
ECType ctype[]={
   UniCh_C_AMA,
   UniCh_C_DEMA,
   UniCh_C_FrAMA,
   UniCh_C_MA,
   UniCh_C_TEMA,
   UniCh_C_VIDyA,
   UniCh_C_PrCh
};
// array with the border types
EWType wtype[]={
   UniCh_W_ATR,
   UniCh_W_StdDev,
   UniCh_W_Points,
   UniCh_W_Percents,
   UniCh_W_PrCh
};

We also add here a pointer to the form class:

CUniChannelForm * frm;

At the very end of the OnInit() function, implement the creation of the GUI object:

if(UseGUI){
  
   // creation and initialization of the form object
   frm=new CUniChannelForm();
   frm.Init();
  
   // auxiliary variables
   int ind1=0;
   int ind2=0;
  
   // search for the selected type of the central line in the array of central line types
   for(int i=0;i<ArraySize(ctype);i++){        
      frm.m_c_cmb_main.AddItem(EnumToString(ctype[i]));
      if(ctype[i]==_CentralType){
         ind1=i;
      }
   }
  
   // search for the selected type of the channel border in the array of border types
   for(int i=0;i<ArraySize(wtype);i++){        
      frm.m_w_cmb_main.AddItem(EnumToString(wtype[i]));
      if(wtype[i]==_WidthType){
         ind2=i;
      }
   }      
  
   // showing the selected type of the central line in the list
   frm.m_c_cmb_main.SetSelectedIndex(ind1);      
   // preparing controls corresponding to the type
   frm.SetCentralType(_CentralType);
  
   // showing the selected border type in the list
   frm.m_w_cmb_main.SetSelectedIndex(ind2);      
   frm.SetWidthType(_WidthType);      
  
   // setting values
   frm.SetValues(
                  _c_Period1,
                  _c_Period2,
                  _c_Period3,
                  _c_Method,
                  _c_Price,
                  _c_Shift,
                  _w_Period,
                  _w_Width,
                  _w_Method,
                  _w_Price,
                  _w_LockPeriod,
                  _w_Shift
   );
  
   // setting the form properties
   frm.SetSubWindow(0);
   frm.SetPos(10,30);
   // showing the form
   frm.Show();
    }    

In addition to creating the form object, the indicator selection lists are filled and selected variants are assigned to them. Let us also set all other values in controls. After that, upon attaching the indicator to a chart, a form with controls will be displayed (Fig. 1).


Fig. 1. A form with the universal channel controls

Controls are properly displayed, and now we need to ensure the operation of the form buttons. Six different events are processed in the OnChartEvent() function. Processing of some events is quite complex, so a separate function will be used:

void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   // form events
   if(frm.Event(id,lparam,dparam,sparam)==1){
      EventForm();
   }
  
   // selecting the central line type
   if(frm.m_c_cmb_main.Event(id,lparam,dparam,sparam)==1){
      EventCentralTypeChange();
   }  
  
   // selecting the border type
   if(frm.m_w_cmb_main.Event(id,lparam,dparam,sparam)==1){
      EventWidthTypeChange();
   }  
  
   // changing the central line parameters
   if(frm.m_central_controls.Event(id,lparam,dparam,sparam)==1){
      EventCentralParametersChange();
   }  
  
   // changing the border parameters
   if(frm.m_width_controls.Event(id,lparam,dparam,sparam)==1){
      EventWidthParametersChange();
   }  
   // changing the shift parameters
   if(frm.m_c_shift.Event(id,lparam,dparam,sparam)!=0 ||
      frm.m_w_shift.Event(id,lparam,dparam,sparam)
   ){
      EventShift();
   }    
    }  

Let's consider all these functions. The EventForm() function:

void EventForm(){      
   int win=ChartWindowFind(0,ShortName);  // determining the indicator sub-window
   ChartIndicatorDelete(0,win,ShortName); // deleting the indicator
   ChartRedraw();
    }  

This function is executed when you close the form by pressing on the cross button, the indicator window is searched by its short name, and the indicator is then deleted. 

The EventCentralTypeChange() function:

void EventCentralTypeChange(){    
   // getting a new type into the variable
   _CentralType=ctype[frm.m_c_cmb_main.SelectedIndex()];
  
   // deleting an old object and creating a new one
   delete(central);
   LoadCentral(true);
  
   // checking the indicator loading
   if(!central.CheckHandle()){
      Alert("Error while loading indicator "+central.Name());
   }
   // setting offsets and buffer names
   SetStyles();
   // setting a new type in the list
   frm.SetCentralType(ctype[frm.m_c_cmb_main.SelectedIndex()]);
   // updating the values of parameters on the form
   frm.SetValues(
                  _c_Period1,
                  _c_Period2,
                  _c_Period3,
                  _c_Method,
                  _c_Price,
                  _c_Shift,
                  _w_Period,
                  _w_Width,
                  _w_Method,
                  _w_Price,
                  _w_LockPeriod,
                  _w_Shift
   );
   // refreshing the form
   frm.Refresh();
  
   // starting the timer for the indicator re-calculation
   EventSetMillisecondTimer(100);
    }

The type of the central line indicator is changed in this function. First, the type of the selected indicator is obtained, then the old object is deleted and a new object is created. When creating a new object, some of its parameters can be changed (due to the UseDefault function), therefore the SetValues() method is called for setting new values to controls, and the for display is refreshed (the Refresh() method). At the end a timer is launched to perform indicator re-calculation.      

The EventWidthTypeChange() function is similar to the EventCentralTypeChange() function, so we will not consider it in detail. 

The EventCentralParametersChange() and EventWidthParametersChange() functions provide reaction of indicators to the change of parameters. These functions are identical in terms of their core functionality. However, when changing the parameters we need to pay attention to period locking and correct the parameters in accordance with the locking, so the functions have their own unique features and both of them will be considered.

void EventCentralParametersChange(){          
  
   // the variable indicates the need to restart the indicator of borders
   bool dolock=false;
  
   // changing the value of period 1
   if((int)frm.m_c_value1.Value()>0){
      // assigning a values received from the control to the variable 
      _c_Period1=(int)frm.m_c_value1.Value();
      // if period 1 is connected with the period of the width indicator
      if(_w_LockPeriod==LockTo_Period1){
         // we set the value of period 1 to the variable with the width indicator period
         _w_Period=_c_Period1;
         // displaying it on the form
         frm.m_w_value1.SetValue(_w_Period);
         // indicating that the second indicator needs to be restarted
         dolock=true;
      }
   }
  
   // changing the value of period 2 similar to period 1
   if((int)frm.m_c_value2.Value()>0){
      _c_Period2=(int)frm.m_c_value2.Value();
      if(_w_LockPeriod==LockTo_Period2){
         _w_Period=_c_Period2;
         frm.m_w_value1.SetValue(_w_Period);
         dolock=true;
      }        
   }
  
   // changing the value of period 3 similar to period 1
   if((int)frm.m_c_value3.Value()>0){
      _c_Period3=(int)frm.m_c_value3.Value();
      if(_w_LockPeriod==LockTo_Period3){
         _w_Period=_c_Period3;
         frm.m_w_value1.SetValue(_w_Period);
         dolock=true;
      }        
   }
  
   // changing the method
   if(frm.m_c_method.SelectedIndex()!=-1){
      _c_Method=e_method[frm.m_c_method.SelectedIndex()];
   }
  
   // changing the price
   if(frm.m_c_price.SelectedIndex()!=-1){
      _c_Price=e_price[frm.m_c_price.SelectedIndex()];
   }
  
   // deleting an old object and creating a new one
   delete(central);
   LoadCentral(false);
   if(!central.CheckHandle()){
      Alert("Error while loading indicator "+central.Name());
   }  
   // deleting and creating a new object of the second indicator
   if(dolock){
      delete(width);
      LoadWidth(false);
      if(!width.CheckHandle()){
         Alert("Error while loading indicator "+width.Name());
      }  
   }  
   // setting offsets and buffer names
   SetStyles();
   // starting the timer for the indicator re-calculation
   EventSetMillisecondTimer(100);
    }  

In this function, when any of the three periods is changed, a check of the locking parameter is performed. If locking is used, the parameter for the border indicator is changed, it is updated on the form, and true is assigned to the dolock variable. In the end, the old indicator object is removed, a new one is created, and if the dolock variable is equal to true, the object of borders is deleted and then created. After that, a timer is started, awaiting recalculation of the indicators.

void EventWidthParametersChange(){  
      
   // the variable indicates the need to restart the indicator of the central line
   bool dolock=false;
   // changing the period
   if((int)frm.m_w_value1.Value()>0){
      // assigning a values received from the control to the variable
      _w_Period=(int)frm.m_w_value1.Value();
      // performing the lock
      // the width parameter is connected with the first period of the central line
      if(_w_LockPeriod==LockTo_Period1){
         // assigning a new value to the variable of the central line indicator 
         _c_Period1=_w_Period;
         // updating the value on the form
         frm.m_c_value1.SetValue(_c_Period1);
         // indicating the need to restart the width indicator
         dolock=true;
      }
      else if(_w_LockPeriod==LockTo_Period2){ // if locking with period 2
         _c_Period2=_w_Period;
         frm.m_c_value2.SetValue(_c_Period2);
         dolock=true;
      }
      else if(_w_LockPeriod==LockTo_Period3){ // if locking with period 3
         _c_Period3=_w_Period;
         frm.m_c_value3.SetValue(_c_Period3);
         dolock=true;
      }
   }
  
   // changing the channel width parameter
   if((double)frm.m_w_value2.Value()>0){
      _w_Width=(double)frm.m_w_value2.Value();
   }      
  
   // changing the method
   if(frm.m_w_method.SelectedIndex()!=-1){
      _w_Method=e_method[frm.m_w_method.SelectedIndex()];
   }
  
   // changing the price
   if(frm.m_w_price.SelectedIndex()!=-1){
      _w_Price=e_price[frm.m_w_price.SelectedIndex()];
   }
  
   // the event of a change in the lock type selection list 
   if(frm.m_w_lockto.SelectedIndex()>=0){
      // assigning a values received from the control to the variable
      _w_LockPeriod=e_lockto[frm.m_w_lockto.SelectedIndex()];
      // if locking with some of the periods is selected,
      // its value is copied and the form is refreshed  
      if(_w_LockPeriod==LockTo_Period1){
         _w_Period=_c_Period1;
         frm.m_w_value1.SetValue(_w_Period);
      }
      else if(_w_LockPeriod==LockTo_Period2){
         _w_Period=_c_Period2;
         frm.m_w_value1.SetValue(_w_Period);
      }
      else if(_w_LockPeriod==LockTo_Period3){
         _w_Period=_c_Period3;
         frm.m_w_value1.SetValue(_w_Period);
      }
   }      
   // deleting an old object and creating a new one
   delete(width);
   LoadWidth(false);
   if(!width.CheckHandle()){
      Alert("Error while loading indicator "+width.Name());
   }
  
   // deleting and creating a new object of the second indicator
   if(dolock){
      delete(central);
      LoadCentral(false);
      if(!central.CheckHandle()){
         Alert("Error while loading indicator "+central.Name());
      }
   }
   // setting offsets and buffer names
   SetStyles();
   // starting the timer for the indicator re-calculation
   EventSetMillisecondTimer(100);      
    }    

In this function, lock type is checked when changing the period, and if necessary, the corresponding period of the central line indicator is changed. In an event of the lock type selection list is received, then a value from the appropriate variable of the central line indicator is assigned to the variable of the border indicator period.

Processing of the event of the shift value change is quite simple:

void EventShift(){     
   // receiving new values in the variables 
   _c_Shift=(int)frm.m_c_shift.Value();
   _w_Shift=(int)frm.m_w_shift.Value();
   // setting new styles
   SetStyles();
   // refreshing the chart
   ChartRedraw();
    }  

Values from controls are assigned to the variables, the SetStyles() is called and the chart is refreshed.

At this point, our indicator with a graphical interface is almost ready.

The following defect was detected during indicator testing. When the external UseDefault parameter was enabled and period lock was used, the locking failed. This is connected with the fact that when loading the second indicator (the width indicator) change of parameters is performed in its constructor. In order to fix this bug, I had to modify certain child classes of width indicators. An optional parameter 'locked' was added to the constructors of CChannelUni_Calculate_ATR, CChannelUni_Calculate_StdDev and CChannelUni_Calculate_PriceChannel. Its default value is false (if the parameter is not passed to the class, everything works without changes). When locked=true and use_default=true, period parameters in the constructor are not changed (provided that locked=true). Here is a part of the CChannelUni_Calculate_ATR class:

if(use_default){
   if(keep_previous){
      if(ma_period==-1 && !locked)ma_period=14// change
      if(ch_width==-1)ch_width=2;
   }
   else{
      if(!locked)ma_period=14// change
      ch_width=2;
   }      
    }  

A default value is assigned to ma_period only if the locked variable is set to false. The LoadWidth() function was refined accordingly. The value of 'Locked' is calculated at the function beginning:

bool Locked=(w_LockPeriod!=LockTo_Off);

Then this variable is passed to the constructors of classes during object creation.

Just as was done in the universal oscillator, here we add the ability to change the color scheme and provide saving of indicator parameters when changing the timeframe. We will not discuss the use of color schemes, because it was considered when creating a universal oscillator. Let us provide saving of indicator parameters.

In the OnDeinit() function of the indicator, if the de-initialization is performed due to the chart change, we create graphical objects with the values of parameters. Let us create these graphical objects outside of chart visibility:

void SaveOrDeleteParameters(const int reason){
   // if it is not chart change, we should delete graphical objects 
   if(reason!=REASON_CHARTCHANGE){
      ObjectDelete(0,"_CentralType");
      ObjectDelete(0,"_c_Period1");
      ObjectDelete(0,"_c_Period2");
      ObjectDelete(0,"_c_Period3");
      ObjectDelete(0,"_c_Shift");
      ObjectDelete(0,"_c_Method");
      ObjectDelete(0,"_c_Price");
      ObjectDelete(0,"_WidthType");
      ObjectDelete(0,"_w_Period");
      ObjectDelete(0,"_w_LockPeriod");
      ObjectDelete(0,"_w_Shift");
      ObjectDelete(0,"_w_Method");
      ObjectDelete(0,"_w_Price");
      ObjectDelete(0,"_w_Width");      
   }
   else// when changing the chart, we create graphical objects with the values of parameters
      SaveParameter("_CentralType",(string)_CentralType);
      SaveParameter("_c_Period1",(string)_c_Period1);
      SaveParameter("_c_Period2",(string)_c_Period2);
      SaveParameter("_c_Period3",(string)_c_Period3);
      SaveParameter("_c_Shift",(string)_c_Shift);
      SaveParameter("_c_Method",(string)_c_Method);
      SaveParameter("_c_Price",(string)_c_Price);
      SaveParameter("_WidthType",(string)_WidthType);
      SaveParameter("_w_Period",(string)_w_Period);
      SaveParameter("_w_LockPeriod",(string)_w_LockPeriod);
      SaveParameter("_w_Shift",(string)_w_Shift);
      SaveParameter("_w_Method",(string)_w_Method);
      SaveParameter("_w_Price",(string)_w_Price);
      SaveParameter("_w_Width",(string)_w_Width);        
   }
}
// an auxiliary function for saving one parameter in a graphical object
void SaveParameter(string name,string value){
   if(ObjectFind(0,name)==-1){
      ObjectCreate(0,name,OBJ_LABEL,0,0,0);
      ObjectSetInteger(0,name,OBJPROP_XDISTANCE,0);
      ObjectSetInteger(0,name,OBJPROP_YDISTANCE,-30);
   }
   ObjectSetString(0,name,OBJPROP_TEXT,value);
    }

In the OnInit() function, immediately after the call of the PrepareParameters() function, we call the LoadSavedParameters() function:

bool LoadSavedParameters(){
   // if all objects with parameters exist 
   if(ObjectFind(0,"_CentralType")==0 &&
      ObjectFind(0,"_c_Period1")==0 &&
      ObjectFind(0,"_c_Period2")==0 &&
      ObjectFind(0,"_c_Period3")==0 &&
      ObjectFind(0,"_c_Shift")==0 &&
      ObjectFind(0,"_c_Method")==0 &&
      ObjectFind(0,"_c_Price")==0 &&
      ObjectFind(0,"_WidthType")==0 &&
      ObjectFind(0,"_w_Period")==0 &&
      ObjectFind(0,"_w_LockPeriod")==0 &&
      ObjectFind(0,"_w_Shift")==0 &&
      ObjectFind(0,"_w_Method")==0 &&
      ObjectFind(0,"_w_Price")==0 &&
      ObjectFind(0,"_w_Width")==0
   ){
      // getting values from graphical objects
      _CentralType=(ECType)ObjectGetString(0,"_CentralType",OBJPROP_TEXT);
      _c_Period1=(int)ObjectGetString(0,"_c_Period1",OBJPROP_TEXT);
      _c_Period2=(int)ObjectGetString(0,"_c_Period2",OBJPROP_TEXT);
      _c_Period3=(int)ObjectGetString(0,"_c_Period3",OBJPROP_TEXT);
      _c_Shift=(int)ObjectGetString(0,"_c_Shift",OBJPROP_TEXT);
      _c_Method=(long)ObjectGetString(0,"_c_Method",OBJPROP_TEXT);
      _c_Price=(long)ObjectGetString(0,"_c_Price",OBJPROP_TEXT);
      _WidthType=(EWType)ObjectGetString(0,"_WidthType",OBJPROP_TEXT);
      _w_Period=(int)ObjectGetString(0,"_w_Period",OBJPROP_TEXT);
      _w_LockPeriod=(long)ObjectGetString(0,"_w_LockPeriod",OBJPROP_TEXT);
      _w_Shift=(int)ObjectGetString(0,"_w_Shift",OBJPROP_TEXT);
      _w_Method=(long)ObjectGetString(0,"_w_Method",OBJPROP_TEXT);
      _w_Price=(long)ObjectGetString(0,"_w_Price",OBJPROP_TEXT);
      _w_Width=(double)ObjectGetString(0,"_w_Width",OBJPROP_TEXT);
      return(true);
   }
   else{
      return(false);
   }
    }  

In this function a check is performed of whether these objects exist. If they do exist, the values if these objects are used, and the function returns true. If the function returns true, then the LoadCentral() and LoadWidth() functions should be called with the false parameter (in order to prevent setting of default parameters). Fragment of the OnInit() function:

bool ChartCange=LoadSavedParameters();
  
    LoadCentral(!ChartCange);  

The LoadWidth() function is called the same way:

LoadWidth(!ChartCange);

Now creation of the universal channel is fully complete. 

Conclusion

Despite the use of a large amount of ready code from the universal oscillator, creation of the universal channel still required a significant amount of additional work. The main difference from the universal oscillator is the existence of two independent units: the central line and channel borders. The difficulties increased the amount of work almost twice. The universal channel includes a more complex parameter changing algorithm connected with the period locking function. Loading of new indicators has also become more complicated, because now two indicators are used. We have also added new functionality — saving parameters when switching the timeframe. As a result, we have created a useful and convenient indicator. In addition, this indicator significantly extends the capabilities of the very idea of ​​the channel, because now you can separately select the central line and the method for constructing the channel borders. This provides a large variety of possible combinations. The increase of the indicator application speed through the use of the graphical interface allows to visually analyze all of these combinations.

Attachments

Attached to the article is the downloadable archive with the required files. The files are placed in right folders. They should be saved to the same folders of the terminal.