English Русский 中文 Español Deutsch 日本語
preview
Gráficos na biblioteca DoEasy (Parte 98): Movendo pontos de ancoragem de objetos gráficos padrão estendidos

Gráficos na biblioteca DoEasy (Parte 98): Movendo pontos de ancoragem de objetos gráficos padrão estendidos

MetaTrader 5Exemplos | 21 julho 2022, 16:12
483 0
Artyom Trishkin
Artyom Trishkin

Conteúdo


Ideia

A partir do artigo 93, começamos a criar objetos gráficos compostos na biblioteca. Depois disso, tivemos que mudar de tópico e tratar da modificação da funcionalidade dos objetos-formas criados com base na classe CCanvas, já que em objetos gráficos compostos usamos objetos-formas para criar elementos de controle para pontos de ancoragem de um objeto gráfico que faz parte de um objeto gráfico padrão estendido, e precisávamos de uma nova funcionalidade de objetos-formas, que, claro, foi planejada, mas ainda não tem sido implementada na biblioteca.

No último artigo, concluímos a modificação de objetos com base na classe CCanvas, e hoje continuaremos desenvolvendo o tópico de objetos gráficos padrão estendidos, a partir dos quais são criados objetos gráficos compostos.

O objetivo deste artigo não será o de desenvolver novas classes. Hoje falaremos sobre como modificar a funcionalidade previamente preparada que nos permitia criar ferramentas convenientes para mover pontos de ancoragem de objetos gráficos padrão. Este artigo não será longo, ele descreverá a criação de um determinado protótipo de um objeto gráfico composto. A propósito, já o criamos anteriormente, em artigos passados, trata-se de uma linha de tendência regular, que possui objetos-etiquetas de preço em suas extremidades:


Hoje vamos resolver o problema de mover os pontos de ancoragem da linha de tendência para que as etiquetas de preço anexadas ao lado móvel da linha de tendência também se movam. É bom dizer que temos objetos-formas para gerar seu deslocamento nos pontos de ancoragem da linha. Também moveremos o lado correspondente da linha de tendência ao arrastar o objeto em questão. Além disso, enquanto o cursor estiver longe do ponto de ancoragem da linha de tendência, o objeto-forma permanecerá invisível. Mas quando o cursor estiver perto, a uma certa distância, do ponto de ancoragem (i. e., ao entrar na área de uma forma transparente), um ponto com um círculo será desenhado na forma:


Assim, é criada a impressão de que o objeto de controle do ponto de ancoragem da linha de tendência aparece. Nesse caso, a forma terá um tamanho maior que sua área ativa. A área ativa de uma forma é a região sobre a qual a forma pode ser movimentada. Já a forma em si pode ser utilizada para gerar outro tipo de interação com ela por meio dos botões do mouse ou da sua roda.

Assim, se o cursor do mouse estiver dentro da forma, mas fora de sua área ativa, poderemos implementar, por exemplo, o menu de contexto de um objeto gráfico composto pressionando o botão direito do mouse. Se o cursor estiver na área ativa, fora o menu de contexto, podemos pegar essa forma com o mouse e movimentá-la. Nesse caso, o final da linha à qual a forma está anexada se movimentará atrás dela.

Naturalmente, este é apenas um objeto gráfico composto de teste dentro do qual "desenvolvemos" a funcionalidade a ser criada. Depois que todas as ferramentas que permitem gerar objetos gráficos compostos e trabalhar com objetos-formas forem criadas, elaboraremos um pequeno conjunto de objetos gráficos compostos padrão para a biblioteca, e já com base neles poderemos criar nossos próprios. E criá-los servirá como um exemplo e uma descrição de como você deve criar seus próprios objetos.

Mas, por enquanto, estamos apenas desenvolvendo a funcionalidade da biblioteca e criando aqueles "tijolos" a partir dos quais será possível fazer nossos próprios objetos, e aquelas etapas que percorremos, estudamos e implementamos artigo após artigo, ademais de estarem prontas, serão a base que você poderá usar "tal como está", poupando você de fazer tudo com suas próprias mãos do zero.


Modificando as classes da biblioteca

Vamos abrir o arquivo \MQL5\Include\DoEasy\Defines.mqh e fazer algumas melhorias nele.

Ao construir formas para controlar pontos de ancoragem, um ponto e um círculo são desenhados neles. A cor em que eles são desenhados foi previamente especificada diretamente no código. Adicionaremos uma macro-substituição na qual escreveremos esta cor por padrão:

//--- Pending request type IDs
#define PENDING_REQUEST_ID_TYPE_ERR    (1)                        // Type of a pending request created based on the server return code
#define PENDING_REQUEST_ID_TYPE_REQ    (2)                        // Type of a pending request created by request
//--- Timeseries parameters
#define SERIES_DEFAULT_BARS_COUNT      (1000)                     // Required default amount of timeseries data
#define PAUSE_FOR_SYNC_ATTEMPTS        (16)                       // Amount of pause milliseconds between synchronization attempts
#define ATTEMPTS_FOR_SYNC              (5)                        // Number of attempts to receive synchronization with the server
//--- Tick series parameters
#define TICKSERIES_DEFAULT_DAYS_COUNT  (1)                        // Required number of days for tick data in default series
#define TICKSERIES_MAX_DATA_TOTAL      (200000)                   // Maximum number of stored tick data of a single symbol
//--- Parameters of the DOM snapshot series
#define MBOOKSERIES_DEFAULT_DAYS_COUNT (1)                        // The default required number of days for DOM snapshots in the series
#define MBOOKSERIES_MAX_DATA_TOTAL     (200000)                   // Maximum number of stored DOM snapshots of a single symbol
//--- Canvas parameters
#define PAUSE_FOR_CANV_UPDATE          (16)                       // Canvas update frequency
#define CLR_CANV_NULL                  (0x00FFFFFF)               // Zero for the canvas with the alpha channel
#define OUTER_AREA_SIZE                (16)                       // Size of one side of the outer area around the form workspace
//--- Graphical object parameters
#define PROGRAM_OBJ_MAX_ID             (10000)                    // Maximum value of an ID of a graphical object belonging to a program
#define CTRL_POINT_RADIUS              (5)                        // Radius of the control point on the form for managing graphical object pivot points
#define CTRL_POINT_COLOR               (clrDodgerBlue)            // Radius of the control point on the form for managing graphical object pivot points
#define CTRL_FORM_SIZE                 (40)                       // Size of the control point form for managing graphical object pivot points
//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+

Vamos renomear a macro-substituição chamada CTRL_POINT_SIZE para CTRL_POINT_RADIUS, já que este não é o tamanho total do círculo, mas, sim, seu raio. É que o nome dessa macro-substituição era um pouco enganador ao calcular a área ativa do objeto-forma.

No arquivo da classe do objeto para o elemento gráfico \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh, modificamos um pouco o método de criação de objeto gráfico elemento. Ao chamar o método CreateBitmapLabel() da classe CCanvas, infelizmente, nenhum código de erro é retornado. Portanto, antes de chamar este método, redefiniremos o último código de erro, e, caso não seja possível criar a etiqueta, exibiremos uma mensagem com o código de erro no log. Isso tornará a depuração um pouco mais fácil.

//+------------------------------------------------------------------+
//| Create the graphical element object                              |
//+------------------------------------------------------------------+
bool CGCnvElement::Create(const long chart_id,     // Chart ID
                          const int wnd_num,       // Chart subwindow
                          const string name,       // Element name
                          const int x,             // X coordinate
                          const int y,             // Y coordinate
                          const int w,             // Width
                          const int h,             // Height
                          const color colour,      // Background color
                          const uchar opacity,     // Opacity
                          const bool redraw=false) // Flag indicating the need to redraw
                         
  {
   ::ResetLastError();
   if(this.m_canvas.CreateBitmapLabel(chart_id,wnd_num,name,x,y,w,h,COLOR_FORMAT_ARGB_NORMALIZE))
     {
      this.Erase(CLR_CANV_NULL);
      this.m_canvas.Update(redraw);
      this.m_shift_y=(int)::ChartGetInteger(chart_id,CHART_WINDOW_YDISTANCE,wnd_num);
      return true;
     }
   CMessage::ToLog(DFUN,::GetLastError(),true);
   return false;
  }
//+------------------------------------------------------------------+

Por que tinha que ser desta maneira? Ao criar objetos-formas, foi necessário muito tempo para que eu descobrisse por qual motivo o recurso gráfico não podia ser criado. Como resultado, descobri que o nome do recurso gráfico que estava sendo criado tinha mais de 63 caracteres. Se o método para criar uma etiqueta da classe CCanvas nos informasse sobre um erro, não teríamos que procurar nada, uma vez que uma mensagem com um código de erro seria recebida imediatamente, e não precisaríamos passar por todas as cadeias de chamadas para os vários métodos em várias classes.

Em geral, essa modificação também não nos apontará para o verdadeiro código de erro se o comprimento do nome do recurso exceder 63 caracteres:

ERR_RESOURCE_NAME_IS_TOO_LONG     4018     The resource name exceeds 63 characters

e retornar um código de erro

ERR_RESOURCE_NOT_FOUND    4016    Resource with such a name is not found in EX5

mas isso ainda é melhor, e imediatamente faz pensar: "por que o recurso gráfico não é criado?"...


Modificamos o arquivo da classe de ferramentas do objeto gráfico padrão estendido no arquivo
\MQL5\Include\DoEasy\Objects\Graph\Extend\CGStdGraphObjExtToolkit.mqh.

Cada objeto gráfico possui um ou mais pontos de ancoragem com os quais esse objeto pode ser posicionado no gráfico. Anexado a cada um desses pontos está um objeto-forma para gerenciar tais pontos de ancoragem. Os objetos gráficos padrão têm pontos próprios com os quais podem ser movidos. Eles aparecem quando um objeto gráfico é selecionado. Não gerenciaremos objetos gráficos padrão estendidos na biblioteca dessa maneira. Para implementar nossa funcionalidade, é mais conveniente usarmos objetos-forma que irão facilitar o deslocamento dos pontos de ancoragem dos objetos gráficos. Pode haver mais objetos-formas do que pontos de ancoragem de um objeto gráfico. Logo, não podemos julgar o número de objetos-formas pelo número de pontos de ancoragem de um objeto gráfico, e precisamos saber esse número. Portanto, à seção pública da classe adicionaremos um método que retorna o número de objetos-formas criados para controlar os pontos de ancoragem do objeto gráfico e declararemos um método que desenha os pontos de controle dos pontos de ancoragem do objeto gráfico em objetos-formas completamente transparentes:

//--- (1) Set and (2) return the size of the form of pivot point management control points
   void              SetControlFormSize(const int size);
   int               GetControlFormSize(void)                           const { return this.m_ctrl_form_size;                 }
//--- Return the pointer to the pivot point form by (1) index and (2) name
   CForm            *GetControlPointForm(const int index)                     { return this.m_list_forms.At(index);           }
   CForm            *GetControlPointForm(const string name,int &index);
//--- Return the number of (1) base object pivot points and (2) newly created form objects for managing control points
   int               GetNumPivotsBaseObj(void)                          const { return this.m_base_pivots;                    }
   int               GetNumControlPointForms(void)                      const { return this.m_list_forms.Total();             }
//--- Create form objects on the base object pivot points
   bool              CreateAllControlPointForm(void);
//--- Draw a reference point on the form
   void              DrawControlPoint(CForm *form,const uchar opacity,const color clr);
//--- Remove all form objects from the list
   void              DeleteAllControlPointForm(void);


No método que cria o objeto de formulário no ponto de ancoragem do objeto base, encurte o nome do objeto que está sendo criado - em vez de "_TKPP_" digite "_CP_":

//+------------------------------------------------------------------+
//| Create a form object on a base object reference point            |
//+------------------------------------------------------------------+
CForm *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index)
  {
   string name=this.m_base_name+"_CP_"+(index<this.m_base_pivots ? (string)index : "X");
   CForm *form=this.GetControlPointForm(index);
   if(form!=NULL)
      return NULL;
   int x=0, y=0;
   if(!this.GetControlPointCoordXY(index,x,y))
      return NULL;
   return new CForm(this.m_base_chart_id,this.m_base_subwindow,name,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize());
  }
//+------------------------------------------------------------------+

Foi aqui (e no arquivo do EA de teste) que tive que encurtar o nome do objeto-forma criado, pois o nome do recurso gráfico acabou tendo mais de 63 caracteres e o objeto não era criado. O motivo está no método de criação de recurso dinâmico na classe CCanvas, onde o nome do recurso a ser criado consiste nos caracteres "::" + o nome passado para o método (que especificamos no método acima) + o identificador do gráfico + o número de milissegundos decorridos desde que o sistema foi iniciado + um número pseudo-aleatório:

//+------------------------------------------------------------------+
//| Create dynamic resource                                          |
//+------------------------------------------------------------------+
bool CCanvas::Create(const string name,const int width,const int height,ENUM_COLOR_FORMAT clrfmt)
  {
   Destroy();
//--- prepare data array
   if(width>0 && height>0 && ArrayResize(m_pixels,width*height)>0)
     {
      //--- generate resource name
      m_rcname="::"+name+(string)ChartID()+(string)(GetTickCount()+MathRand());
      //--- initialize data with zeros
      ArrayInitialize(m_pixels,0);
      //--- create dynamic resource
      if(ResourceCreate(m_rcname,m_pixels,width,height,0,0,0,clrfmt))
        {
         //--- successfully created
         //--- complete initialization
         m_width =width;
         m_height=height;
         m_format=clrfmt;
         //--- succeed
         return(true);
        }
     }
//--- error - destroy object
   Destroy();
   return(false);
  }
//+------------------------------------------------------------------+

Tudo isso, infelizmente, impõe sérias restrições à escolha de um nome intuitivo para o objeto criado.

No método que cria objetos-formas nos pontos de ancoragem do objeto base, calculamos o deslocamento de cada lado do objeto-forma para indicar a localização e o tamanho da área ativa da forma, que deve estar no centro da mesma, e seu tamanho deve ser igual a dois valores registrados na macro-substituição CTRL_POINT_RADIUS . Como se trata do raio, para que a zona ativa da forma seja igual ao círculo desenhado em seu centro, precisamos pegar dois valores do raio, subtraí-los da largura da forma (a altura de a forma é igual à sua largura) e dividir o valor resultante por dois.
O valor calculado para o recuo da borda da área ativa em relação à borda da forma é especificado no método SetActiveAreaShift():

//+------------------------------------------------------------------+
//| Create form objects on the base object pivot points              |
//+------------------------------------------------------------------+
bool CGStdGraphObjExtToolkit::CreateAllControlPointForm(void)
  {
   bool res=true;
   //--- In the loop by the number of base object pivot points
   for(int i=0;i<this.m_base_pivots;i++)
     {
      //--- Create a new form object on the current pivot point corresponding to the loop index
      CForm *form=this.CreateNewControlPointForm(i);
      //--- If failed to create the form, inform of that and add 'false' to the final result
      if(form==NULL)
        {
         CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM);
         res &=false;
        }
      //--- If failed to add the form to the list, inform of that, remove the created form and add 'false' to the final result
      if(!this.m_list_forms.Add(form))
        {
         CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST);
         delete form;
         res &=false;
        }
      //--- Set all the necessary properties for the created form object
      form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM);                // Object is created programmatically
      form.SetActive(true);                                    // Form object is active
      form.SetMovable(true);                                   // Movable object
      int x=(int)::floor((form.Width()-CTRL_POINT_RADIUS*2)/2);// Shift the active area from the form edge
      form.SetActiveAreaShift(x,x,x,x);                        // Object active area is located at the form center, its size is equal to two CTRL_POINT_RADIUS values
      form.SetFlagSelected(false,false);                       // Object is not selected
      form.SetFlagSelectable(false,false);                     // Object cannot be selected by mouse
      form.Erase(CLR_CANV_NULL,0);                             // Fill in the form with transparent color and set the full transparency
      //form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,clrSilver);    // Draw an outlining rectangle for visual display of the form location
      //form.DrawRectangle(x,x,form.Width()-x-1,form.Height()-x-1,clrSilver);// Draw an outlining rectangle for visual display of the form active area location
      this.DrawControlPoint(form,0,CTRL_POINT_COLOR);          // Draw a circle and a point at the form center
      form.Done();                                             // Save the initial form object state (its appearance)
     }
   //--- Redraw the chart for displaying changes (if successful) and return the final result
   if(res)
      ::ChartRedraw(this.m_base_chart_id);
   return res;
  }
//+------------------------------------------------------------------+

Para fins de depuração, durante a criação de forma, podemos desenhar retângulos, exibindo o tamanho da forma e sua área ativa, e essas linhas são comentadas. Também desenhamos círculos completamente transparentes no centro da forma com um novo método, visto abaixo. E se são transparentes, vale a pena desenhá-los então?

Método que desenha um ponto de controle na forma:

//+------------------------------------------------------------------+
//| Draw a control point on the form                                 |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::DrawControlPoint(CForm *form,const uchar opacity,const color clr)
  {
   if(form==NULL)
      return;
   form.DrawCircle((int)::floor(form.Width()/2),(int)::floor(form.Height()/2),CTRL_POINT_RADIUS,clr,opacity);// Draw a circle at the center of the form
   form.DrawCircleFill((int)::floor(form.Width()/2),(int)::floor(form.Height()/2),2,clr,opacity);            // Draw a point at the center of the form
  }
//+------------------------------------------------------------------+

O método contém duas linhas, que simplesmente movemos do método acima para um método à parte. Para que é isso? Precisamos mostrar ou ocultar um ponto com um círculo no centro da forma em momentos diferentes. Para fazer isso, chamamos esse método, e especificamos a opacidade e a cor desejadas das formas desenhadas.

No manipulador de eventos, removemos o processamento de movimento do cursor do mouse , porque não precisamos disso aqui.

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CGStdGraphObjExtToolkit::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam)
  {
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      for(int i=0;i<this.m_list_forms.Total();i++)
        {
         CForm *form=this.m_list_forms.At(i);
         if(form==NULL)
            continue;
         int x=0, y=0;
         if(!this.GetControlPointCoordXY(i,x,y))
            continue;
         form.SetCoordX(x-this.m_shift);
         form.SetCoordY(y-this.m_shift);
         form.Update();
        }
      ::ChartRedraw(this.m_base_chart_id);
     }
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      for(int i=0;i<this.m_list_forms.Total();i++)
        {
         CForm *form=this.m_list_forms.At(i);
         if(form==NULL)
            continue;
         form.OnChartEvent(id,lparam,dparam,sparam);
        }
      ::ChartRedraw(this.m_base_chart_id);
     }
  }
//+------------------------------------------------------------------+


Vamos alterar a classe do objeto gráfico padrão abstrato no arquivo \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh.

Na seção pública da classe, declaramos um método que permite alterar simultaneamente as coordenadas dos pontos de ancoragem de um objeto gráfico e dos objetos que estão anexados a ele:

//--- Return (1) the list of dependent objects, (2) dependent graphical object by index and (3) the number of dependent objects
   CArrayObj        *GetListDependentObj(void)        { return &this.m_list;           }
   CGStdGraphObj    *GetDependentObj(const int index) { return this.m_list.At(index);  }
   int               GetNumDependentObj(void)         { return this.m_list.Total();    }
//--- Return the name of the dependent object by index
   string            NameDependent(const int index);
//--- Add the dependent graphical object to the list
   bool              AddDependentObj(CGStdGraphObj *obj);
//--- Change X and Y coordinates of the current and all dependent objects
   bool              ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false);
//--- Return the pivot point data object
   CLinkedPivotPoint*GetLinkedPivotPoint(void)        { return &this.m_linked_pivots;  }

Para ter acesso à forma que irá controlar os pontos de ancoragem do objeto gráfico, vamos declarar três métodos:

//--- Return the number of base object pivot points for calculating the coordinates in the (1) current and (2) specified object
   int               GetLinkedCoordsNum(void)               const { return this.m_linked_pivots.GetNumLinkedCoords();      }
   int               GetLinkedPivotsNum(CGStdGraphObj *obj) const { return(obj!=NULL ? obj.GetLinkedCoordsNum() : 0);      }
//--- Return the form for managing an object pivot point
   CForm            *GetControlPointForm(const int index);
//--- Return the number of form objects for managing reference points
   int               GetNumControlPointForms(void);
//--- Redraw the form for managing a reference point of an extended standard graphical object
   void              RedrawControlPointForms(const uchar opacity,const color clr);

private:


Adicionamos um método que define o tempo e o preço de acordo com as coordenadas da tela:

//--- Symbol for the Chart object 
   string            ChartObjSymbol(void)          const { return this.GetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,0);                    }
   bool              SetChartObjSymbol(const string symbol)
                       {
                        if(!::ObjectSetString(CGBaseObj::ChartID(),CGBaseObj::Name(),OBJPROP_SYMBOL,symbol))
                           return false;
                        this.SetProperty(GRAPH_OBJ_PROP_CHART_OBJ_SYMBOL,0,symbol);
                        return true;
                       }
//--- Set the time and price by screen coordinates
   bool              SetTimePrice(const int x,const int y,const int modifier)
                       {
                        bool res=true;
                        ENUM_OBJECT type=this.GraphObjectType();
                        if(type==OBJ_LABEL || type==OBJ_BUTTON || type==OBJ_BITMAP_LABEL || type==OBJ_EDIT || type==OBJ_RECTANGLE_LABEL)
                          {
                           res &=this.SetXDistance(x);
                           res &=this.SetYDistance(y);
                          }
                        else
                          {
                           int subwnd=0;
                           datetime time=0;
                           double price=0;
                           if(::ChartXYToTimePrice(this.ChartID(),x,y,subwnd,time,price))
                             {
                              res &=this.SetTime(time,modifier);
                              res &=this.SetPrice(price,modifier);
                             }
                          }
                        return res;
                       }
  
//--- Return the flags indicating object visibility on timeframes

Para podermos trabalhar com objetos gráficos em coordenadas de tela X e Y, precisamos converter as coordenadas de tela em coordenadas de tempo/preço. Este método verifica, primeiro, qual o tipo do objeto atual, e, se for construído a partir de coordenadas de tela, suas coordenadas de tela serão alteradas imediatamente. Se este objeto gráfico for construído de acordo com as coordenadas de tempo/preço, primeiro precisamos converter as coordenadas de tela passadas para o método em valores de tempo e preço e, em seguida, registrar esses valores recebidos nos parâmetros do objeto gráfico.

Método que retorna a forma de controle pertencente ao ponto de ancoragem do objeto:

//+------------------------------------------------------------------+
//| Return the form for managing an object pivot point               |
//+------------------------------------------------------------------+
CForm *CGStdGraphObj::GetControlPointForm(const int index)
  {
   return(this.ExtToolkit!=NULL ? this.ExtToolkit.GetControlPointForm(index) : NULL);
  }
//+------------------------------------------------------------------+

Tudo é simples aqui: caso o objeto da ferramentas pertencentes ao objeto gráfico padrão estendido exista, o objeto-forma será retornado segundo o índice. Se não for assim, será retornado NULL.

Método que retorna o número de objetos-formas que controlam o ponto de controle:

//+------------------------------------------------------------------+
//| Return the number of form objects                                |
//| for managing control points                                      |
//+------------------------------------------------------------------+
int CGStdGraphObj::GetNumControlPointForms(void)
  {
   return(this.ExtToolkit!=NULL ? this.ExtToolkit.GetNumControlPointForms() : 0);
  }
//+------------------------------------------------------------------+

O método é semelhante ao discutido acima: se o objeto das ferramentas do objeto gráfico padrão estendido existir, o número de objetos-formas será retornado. Se não for assim,será retornado 0.

Método que redesenha a forma que gere o ponto de controle do objeto gráfico padrão estendido:

//+------------------------------------------------------------------+
//| Redraw the form for managing a control point                     |
//| of an extended standard graphical object                         |
//+------------------------------------------------------------------+
void CGStdGraphObj::RedrawControlPointForms(const uchar opacity,const color clr)
  {
//--- Leave if the object has no toolkit of an extended standard graphical object
   if(this.ExtToolkit==NULL)
      return;
//--- Get the number of pivot point management forms
   int total_form=this.GetNumControlPointForms();
//--- In the loop by the number of pivot point management forms
   for(int i=0;i<total_form;i++)
     {
      //--- get the next form object
      CForm *form=this.ExtToolkit.GetControlPointForm(i);
      if(form==NULL)
         continue;
      //--- draw a point and a circle with the specified non-transparency and color
      this.ExtToolkit.DrawControlPoint(form,opacity,clr);
     }
   
//--- Get the total number of bound graphical objects
   int total_dep=this.GetNumDependentObj();
//--- In the loop by all bound graphical objects,
   for(int i=0;i<total_dep;i++)
     {
      //--- get the next graphical object from the list
      CGStdGraphObj *dep=this.GetDependentObj(i);
      if(dep==NULL)
         continue;
      //--- call the method for it
      dep.RedrawControlPointForms(opacity,clr);
     }
  }
//+------------------------------------------------------------------+

O método é comentado em detalhes. Em poucas palavras, primeiro redesenhamos todos os objetos-formas vinculados ao objeto atual. E depois lembramos que objetos gráficos dependentes podem ser anexados a esse objeto, objetos gráficos esses que, por sua vez, também possuem objetos-formas. Sendo assim, em um loop por todos os objetos dependentes, chamamos esse método, para cada um dos objetos dependentes. E esses objetos, por sua vez, também passarão pela lista de seus objetos dependentes e chamarão o mesmo método para eles. E assim por diante até que todos os objetos-formas para todos os objetos gráficos vinculados sejam redesenhados.

Método que altera as coordenadas X e Y do objeto atual e de todos os objetos dependentes:

//+----------------------------------------------------------------------+
//| Change X and Y coordinates of the current and all dependent objects  |
//+----------------------------------------------------------------------+
bool CGStdGraphObj::ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false)
  {
//--- Set new coordinates for the pivot point specified in 'modifier'
   if(!this.SetTimePrice(x,y,modifier))
      return false;
//--- If the object is not a composite graphical object
//--- or if subordinate graphical objects are not attached to the object,
//--- there is nothing else to do here, return 'true'
   if(this.ExtToolkit==NULL || this.m_list.Total()==0)
      return true;
//--- Get the graphical object bound to the 'modifier' point
   CGStdGraphObj *dep=this.GetDependentObj(modifier);
   if(dep==NULL)
      return false;
//--- Get the object of pivot point data of the bound graphical object
   CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint();
   if(pp==NULL)
      return false;
//--- get the number of coordinate points the object is attached to
   int num=pp.GetNumLinkedCoords();
//--- In the loop by the object coordinate points,
   for(int j=0;j<num;j++)
     {
      //--- get the number of coordinate points of the base object for setting the X coordinate
      int numx=pp.GetBasePivotsNumX(j);
      //--- In the loop by each coordinate point for setting the X coordinate,
      for(int nx=0;nx<numx;nx++)
        {
         //--- get the property for setting the X coordinate, its modifier
         //--- and set it to the subordinate graphical object attached to the 'modifier' point
         int prop_from=pp.GetPropertyX(j,nx);
         int modifier_from=pp.GetPropertyModifierX(j,nx);
         this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx);
        }
      //--- Get the number of coordinate points of the base object for setting the Y coordinate
      int numy=pp.GetBasePivotsNumY(j);
      //--- In the loop by each coordinate point for setting the Y coordinate,
      for(int ny=0;ny<numy;ny++)
        {
         //--- get the property for setting the Y coordinate, its modifier
         //--- and set it to the subordinate graphical object attached to the 'modifier' point
         int prop_from=pp.GetPropertyY(j,ny);
         int modifier_from=pp.GetPropertyModifierY(j,ny);
         this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny);
        }
     }
//--- Save the current properties of a subordinate graphical object as the previous ones
   dep.PropertiesCopyToPrevData();
//--- Move a reference control point to new coordinates
   this.ExtToolkit.SetBaseObjTimePrice(this.Time(modifier),this.Price(modifier),modifier);
   this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
//--- If the flag is active, redraw the chart
   if(redraw)
      ::ChartRedraw(m_chart_id);
//--- All is successful - return 'true'
   return true;
  }
//+------------------------------------------------------------------+

O método é comentado em detalhes. Resumindo: primeiro alteramos as coordenadas do ponto de ancoragem especificado para o objeto atual. Além disso, se o objeto tiver objetos gráficos dependentes vinculados a ele, o objeto gráfico dependente também deve ser deslocado para novas coordenadas no ponto que foi movido e ao qual o objeto dependente pode ser anexado, e isso acontece no método.

No manipulador de eventos, removemos o processamento do movimento do mouse, porque não é necessário aqui:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CGStdGraphObj::OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(GraphElementType()!=GRAPH_ELEMENT_TYPE_STANDARD_EXTENDED)
      return;
   string name=this.Name();
   if(id==CHARTEVENT_CHART_CHANGE)
     {
      if(ExtToolkit==NULL)
         return;
      for(int i=0;i<this.Pivots();i++)
        {
         ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i);
        }
      ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance());
      ExtToolkit.OnChartEvent(id,lparam,dparam,name);
     }
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      if(ExtToolkit!=NULL)
         ExtToolkit.OnChartEvent(id,lparam,dparam,name);
     }
  }
//+------------------------------------------------------------------+


Vamos alterar a classe-coleção de objetos gráficos no arquivo \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.

Quando movemos o cursor sobre o gráfico, se houver objetos no gráfico, o objeto sobre o qual o cursor do mouse está é determinado no manipulador de eventos da classe. Em seguida, o manipulador de estado do mouse é iniciado em relação a esse objeto. Quando passamos o cursor sobre o objeto que gere o ponto de controle do objeto gráfico padrão estendido, graças ao manipulador saberemos tanto a própria forma quanto seu índice e o objeto gráfico ao qual essa forma está vinculada. Para não procurar novamente esta forma e o objeto gráfico fora do manipulador, basta armazenar em variáveis o identificador do objeto gráfico ao qual a forma está vinculada e o índice da forma sobre a qual o cursor está localizado. Esses dados nos ajudarão a selecionar rapidamente os objetos desejados das listas e entender que o cursor está sobre a forma de acordo com o valor dessas variáveis.

Escrevemos essas variáveis na declaração de um método que retorna um ponteiro para a forma sob o cursor:

//--- Return the pointer to the form located under the cursor
   CForm            *GetFormUnderCursor(const int id, 
                                        const long &lparam, 
                                        const double &dparam, 
                                        const string &sparam,
                                        ENUM_MOUSE_FORM_STATE &mouse_state,
                                        long &obj_ext_id,
                                        int &form_index);

As variáveis são passadas para o método por referência. Ou seja, dentro do método, simplesmente escrevemos os valores necessários neles, e esses valores são salvos nas variáveis correspondentes, e assim podemos usá-los depois.

Na seção pública da classe, declaramos dois métodos que retornam objetos gráficos estendidos padrão de acordo com o identificador:

//--- Return an (1) existing and (2) removed graphical object by chart name and ID
   CGStdGraphObj    *GetStdGraphObject(const string name,const long chart_id);
   CGStdGraphObj    *GetStdDelGraphObject(const string name,const long chart_id);
//--- Return the existing (1) extended and (2) standard graphical object by its ID
   CGStdGraphObj    *GetStdGraphObjectExt(const long id,const long chart_id);
   CGStdGraphObj    *GetStdGraphObject(const long id,const long chart_id);
//--- Return the list of (1) chart management objects and (2) removed graphical objects
   CArrayObj        *GetListChartsControl(void)                                                          { return &this.m_list_charts_control;  }
   CArrayObj        *GetListDeletedObj(void)                                                             { return &this.m_list_deleted_obj;     }

Serão necessários métodos para obter um ponteiro para um objeto gráfico segundo seu identificador. Vejamos a implementação desses métodos.

Método que retorna um objeto gráfico padrão estendido existente por seu identificador:

//+------------------------------------------------------------------+
//| Return the existing extended standard                            |
//| graphical object by its ID                                       |
//+------------------------------------------------------------------+
CGStdGraphObj *CGraphElementsCollection::GetStdGraphObjectExt(const long id,const long chart_id)
  {
   CArrayObj *list=this.GetListStdGraphObjectExt();
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_CHART_ID,0,chart_id,EQUAL);
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_ID,0,id,EQUAL);
   return(list!=NULL && list.Total()>0 ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

Aqui: obtemos uma lista de objetos gráficos padrão estendidos, deixamos apenas objetos com o identificador gráfico especificado na lista.
Na lista resultante, selecionamos um objeto com o identificador do objeto especificado. Se a lista resultante for válida e não estiver vazia, retornamos o ponteiro para o objeto requerido contido nele. Se não for assim, retornamos NULL.

Método que retorna um objeto gráfico padrão existente segundo seu identificador:

//+------------------------------------------------------------------+
//| Return the existing standard                                     |
//| graphical object by its ID                                       |
//+------------------------------------------------------------------+
CGStdGraphObj *CGraphElementsCollection::GetStdGraphObject(const long id,const long chart_id)
  {
   CArrayObj *list=this.GetList(GRAPH_OBJ_PROP_CHART_ID,0,chart_id);
   list=CSelect::ByGraphicStdObjectProperty(list,GRAPH_OBJ_PROP_ID,0,id,EQUAL);
   return(list!=NULL && list.Total()>0 ? list.At(0) : NULL);
  }
//+------------------------------------------------------------------+

Aqui: obtemos uma lista de objetos gráficos de acordo com o identificador gráfico. Na lista resultante, deixamos o objeto com o identificador do objeto especificado.
Se a lista resultante for válida e não estiver vazia, retornamos o ponteiro para o objeto contido nela
. Se não for assim, retornamos NULL.

Modificamos o método que retorna um ponteiro para a forma sob o cursor. Precisamos adicionar e inicializar duas novas variáveis para armazenar o identificador do objeto gráfico padrão estendido e o índice do ponto de ancoragem, que é controlado pela forma, e também devemos inserir o bloco para processamento de objetos gráficos padrão estendidos, isto é, a busca de formas vinculadas a esses objetos e sobre as quais o cursor do mouse está:

//+------------------------------------------------------------------+
//| Return the pointer to the form located under the cursor          |
//+------------------------------------------------------------------+
CForm *CGraphElementsCollection::GetFormUnderCursor(const int id, 
                                                    const long &lparam, 
                                                    const double &dparam, 
                                                    const string &sparam,
                                                    ENUM_MOUSE_FORM_STATE &mouse_state,
                                                    long &obj_ext_id,
                                                    int &form_index)
  {
//--- Set the ID of the extended standard graphical object to -1 
//--- and the index of the anchor point managed by the form to -1
   obj_ext_id=WRONG_VALUE;
   form_index=WRONG_VALUE;
//--- Initialize the mouse status relative to the form
   mouse_state=MOUSE_FORM_STATE_NONE;
//--- Declare the pointers to graphical element collection class objects
   CGCnvElement *elm=NULL;
   CForm *form=NULL;
//--- Get the list of objects the interaction flag is set for (there should be only one object)
   CArrayObj *list=CSelect::ByGraphCanvElementProperty(GetListCanvElm(),CANV_ELEMENT_PROP_INTERACTION,true,EQUAL);
//--- If managed to obtain the list and it is not empty,
   if(list!=NULL && list.Total()>0)
     {
      //--- Get the only graphical element there
      elm=list.At(0);
      //--- If it is a form object
      if(elm.TypeGraphElement()==GRAPH_ELEMENT_TYPE_FORM)
        {
         //--- Assign the pointer to the element for the form object pointer
         form=elm;
         //--- Get the mouse status relative to the form
         mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
         //--- If the cursor is within the form, return the pointer to the form
         if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
            return form;
        }
     }
//--- If there is no a single form object with a specified interaction flag,
//--- in the loop by all graphical element collection class objects
   int total=this.m_list_all_canv_elm_obj.Total();
   for(int i=0;i<total;i++)
     {
      //--- get the next element
      elm=this.m_list_all_canv_elm_obj.At(i);
      if(elm==NULL)
         continue;
      //--- if the obtained element is a form object
      if(elm.TypeGraphElement()==GRAPH_ELEMENT_TYPE_FORM)
        {
         //--- Assign the pointer to the element for the form object pointer
         form=elm;
         //--- Get the mouse status relative to the form
         mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
         //--- If the cursor is within the form, return the pointer to the form
         if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
            return form;
        }
     }
//--- If there is no a single form object from the collection list
//--- Get the list of extended standard graphical objects
   list=this.GetListStdGraphObjectExt();
   if(list!=NULL)
     {
      //--- in the loop by all extended standard graphical objects
      for(int i=0;i<list.Total();i++)
        {
         //--- get the next graphical object,
         CGStdGraphObj *obj_ext=list.At(i);
         if(obj_ext==NULL)
            continue;
         //--- get the object of its toolkit,
         CGStdGraphObjExtToolkit *toolkit=obj_ext.GetExtToolkit();
         if(toolkit==NULL)
            continue;
         //--- handle the event of changing the chart for the current graphical object
         obj_ext.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
         //--- Get the total number of form objects created for the current graphical object
         total=toolkit.GetNumControlPointForms();
         //--- In the loop by all form objects
         for(int j=0;j<total;j++)
           {
            //--- get the next form object,
            form=toolkit.GetControlPointForm(j);
            if(form==NULL)
               continue;
            //--- get the mouse status relative to the form
            mouse_state=form.MouseFormState(id,lparam,dparam,sparam);
            //--- If the cursor is inside the form,
            if(mouse_state>MOUSE_FORM_STATE_OUTSIDE_FORM_WHEEL)
              {
               //--- set the object ID and form index
               //--- and return the pointer to the form
               obj_ext_id=obj_ext.ObjectID();
               form_index=j;
               return form;
              }
           }
        }
     }
//--- Nothing is found - return NULL
   return NULL;
  }
//+------------------------------------------------------------------+

Toda a lógica do bloco de código adicionado está descrita nos comentários. Resumindo: precisamos encontrar o objeto-forma sobre o qual o cursor do mouse está. Primeiro, procuramos objetos-formas armazenados na lista de elementos gráficos da classe-coleção. Se nenhuma forma for encontrada, precisamos percorrer todos os objetos gráficos padrão estendidos procurando suas formas, o cursor pode estar sobre um deles. Se for esse o caso, nas variáveis passadas por referência ao método, escrevemos o identificador do objeto gráfico padrão estendido ao qual esta forma está vinculada e o índice desta forma, para saber qual ponto de ancoragem de qual objeto gráfico esta forma gere.

Agora precisamos processar a interação do cursor do mouse com os objetos-formas de objetos gráficos padrão estendidos no manipulador de eventos. Além disso, controlaremos o movimento dos objetos-formas para que eles não possam entrar na área do gráfico, nomeadamente em seu canto superior direito, onde está localizado o botão de ativação do modo de negociação com um clique. Este botão está sempre em cima de todos os objetos, e não precisamos que a forma flutuante possa aparecer sob ele, para não clicar acidentalmente neste botão em vez de na forma. Se o painel de negociação com um clique já estiver ativado, a forma também não deve aparece sob ele, porque simplesmente não será visível se for menor que este painel, o que causará inconvenientes ao trabalhar com a forma, fazendo com que você tenha que desabilitar o painel de negociação com um clique para ver novamente a forma que apareceu acidentalmente sob tal painel.

Vejamos as melhorias e alterações no manipulador de eventos:

//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   CGStdGraphObj *obj_std=NULL;  // Pointer to the standard graphical object
   CGCnvElement  *obj_cnv=NULL;  // Pointer to the graphical element object on canvas
   ushort idx=ushort(id-CHARTEVENT_CUSTOM);
   if(id==CHARTEVENT_OBJECT_CHANGE  || id==CHARTEVENT_OBJECT_DRAG    || id==CHARTEVENT_OBJECT_CLICK   ||
      idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG   || idx==CHARTEVENT_OBJECT_CLICK)
     {
      //--- Calculate the chart ID
      //--- If the event ID corresponds to an event from the current chart, the chart ID is received from ChartID
      //--- If the event ID corresponds to a user event, the chart ID is received from lparam
      //--- Otherwise, the chart ID is assigned to -1
      long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE);
      long chart_id=(param==WRONG_VALUE ? (lparam==0 ? ::ChartID() : lparam) : param);
      //--- Get the object, whose properties were changed or which was relocated,
      //--- from the collection list by its name set in sparam
      obj_std=this.GetStdGraphObject(sparam,chart_id);
      //--- If failed to get the object by its name, it is not on the list,
      //--- which means its name has been changed
      if(obj_std==NULL)
        {
         //--- Let's search the list for the object that is not on the chart
         obj_std=this.FindMissingObj(chart_id);
         //--- If failed to find the object here as well, exit
         if(obj_std==NULL)
            return;
         //--- Get the name of the renamed graphical object on the chart, which is not in the collection list
         string name_new=this.FindExtraObj(chart_id);
         //--- set a new name for the collection list object, which does not correspond to any graphical object on the chart,
         //--- and send an event with the new name of the object to the control program chart
         if(obj_std.SetNamePrev(obj_std.Name()) && obj_std.SetName(name_new))
            ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj_std.ChartID(),obj_std.TimeCreate(),obj_std.Name());
        }
      //--- Update the properties of the obtained object
      //--- and check their change
      obj_std.PropertiesRefresh();
      obj_std.PropertiesCheckChanged();
     }

//--- Handle standard graphical object events in the collection list
   for(int i=0;i<this.m_list_all_graph_obj.Total();i++)
     {
      //--- Get the next graphical object and
      obj_std=this.m_list_all_graph_obj.At(i);
      if(obj_std==NULL)
         continue;
      //--- call its event handler
      obj_std.OnChartEvent((id<CHARTEVENT_CUSTOM ? id : idx),lparam,dparam,sparam);
     }

//--- Handle chart changes for extended standard objects
   if(id==CHARTEVENT_CHART_CHANGE || idx==CHARTEVENT_CHART_CHANGE)
     {
      CArrayObj *list=this.GetListStdGraphObjectExt();
      if(list!=NULL)
        {
         for(int i=0;i<list.Total();i++)
           {
            obj_std=list.At(i);
            if(obj_std==NULL)
               continue;
            obj_std.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam);
           }
        }
     }
//--- Handling mouse events of graphical objects on canvas
//--- If the event is not a chart change
   else
     {
      //--- Check whether the mouse button is pressed
      bool pressed=(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)==MOUSE_BUTT_KEY_STATE_LEFT ? true : false);
      ENUM_MOUSE_FORM_STATE mouse_state=MOUSE_FORM_STATE_NONE;
      //--- Declare static variables for the active form and status flags
      static CForm *form=NULL;
      static bool pressed_chart=false;
      static bool pressed_form=false;
      static bool move=false;
      //--- Declare static variables for the index of the form for managing an extended standard graphical object and its ID
      static int  form_index=WRONG_VALUE;
      static long graph_obj_id=WRONG_VALUE;
      
      //--- If the button is not pressed on the chart and the movement flag is not set, get the form, above which the cursor is located
      if(!pressed_chart && !move)
         form=this.GetFormUnderCursor(id,lparam,dparam,sparam,mouse_state,graph_obj_id,form_index);
      
      //--- If the button is not pressed, reset all flags and enable the chart tools 
      if(!pressed)
        {
         pressed_chart=false;
         pressed_form=false;
         move=false;
         this.SetChartTools(::ChartID(),true);
        }
      
      //--- If this is a mouse movement event and the movement flag is active, move the form, above which the cursor is located (if the pointer to it is valid)
      if(id==CHARTEVENT_MOUSE_MOVE && move)
        {
         if(form!=NULL)
           {
            //--- calculate the cursor movement relative to the form coordinate origin
            int x=this.m_mouse.CoordX()-form.OffsetX();
            int y=this.m_mouse.CoordY()-form.OffsetY();
            //--- get the width and height of the chart the form is located at
            int chart_width=(int)::ChartGetInteger(form.ChartID(),CHART_WIDTH_IN_PIXELS,form.SubWindow());
            int chart_height=(int)::ChartGetInteger(form.ChartID(),CHART_HEIGHT_IN_PIXELS,form.SubWindow());
            //--- If the form is not within an extended standard graphical object
            if(form_index==WRONG_VALUE)
              {
               //--- Adjust the calculated form coordinates if the form is out of the chart range
               if(x<0) x=0;
               if(x>chart_width-form.Width()) x=chart_width-form.Width();
               if(y<0) y=0;
               if(y>chart_height-form.Height()) y=chart_height-form.Height();
               //--- If the one-click trading panel is not present on the chart,
               if(!::ChartGetInteger(form.ChartID(),CHART_SHOW_ONE_CLICK))
                 {
                  //--- calculate the form coordinate so that the form does not enter the one-click trading button during relocation
                  if(y<17 && x<41)
                     y=17;
                 }
               //--- If the one-click trading panel is on the chart,
               else
                 {
                  //--- calculate the form coordinates so that the form does not overlap with the one-click trading panel
                  if(y<80 && x<192)
                     y=80;
                 }
              }
            //--- If the form is included into the extended standard graphical object
            else
              {
               if(graph_obj_id>WRONG_VALUE)
                 {
                  //--- Get the list of objects by object ID (there should be one object)
                  CArrayObj *list_ext=CSelect::ByGraphicStdObjectProperty(GetListStdGraphObjectExt(),GRAPH_OBJ_PROP_ID,0,graph_obj_id,EQUAL);
                  //--- If managed to obtain the list and it is not empty,
                  if(list_ext!=NULL && list_ext.Total()>0)
                    {
                     //--- get the graphical object from the list
                     CGStdGraphObj *ext=list_ext.At(0);
                     //--- If the pointer to the object has been received,
                     if(ext!=NULL)
                       {
                        //--- get the object type
                        ENUM_OBJECT type=ext.GraphObjectType();
                        //--- If the object is built using screen coordinates, set the coordinates to the object
                        if(type==OBJ_LABEL || type==OBJ_BUTTON || type==OBJ_BITMAP_LABEL || type==OBJ_EDIT || type==OBJ_RECTANGLE_LABEL)
                          {
                           ext.SetXDistance(x);
                           ext.SetYDistance(y);
                          }
                        //--- otherwise, if the object is built based on time/price coordinates,
                        else
                          {
                           //--- calculate the coordinate shift and limit the coordinates so that they are not out of the chart range
                           int shift=(int)::ceil(form.Width()/2)+1;
                           if(x+shift<0)
                              x=-shift;
                           if(x+shift>chart_width)
                              x=chart_width-shift;
                           if(y+shift<0)
                              y=-shift;
                           if(y+shift>chart_height)
                              y=chart_height-shift;
                           //--- set the calculated coordinates to the object
                           ext.ChangeCoordsExtendedObj(x+shift,y+shift,form_index);
                          }
                       }
                    }
                 }
              }
            //--- Move the form by the obtained coordinates
            form.Move(x,y,true);
           }
        }
   
      //--- Display debugging comments on the chart
      Comment
        (
         (form!=NULL ? form.Name()+":" : ""),"\n",
         EnumToString((ENUM_CHART_EVENT)id),"\n",
         EnumToString(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)),
         "\n",EnumToString(mouse_state),
         "\npressed=",pressed,", move=",move,(form!=NULL ? ", Interaction="+(string)form.Interaction() : ""),
         "\npressed_chart=",pressed_chart,", pressed_form=",pressed_form,
         "\nform_index=",form_index,", graph_obj_id=",graph_obj_id
        );
      
      //--- If the cursor is not above the form
      if(form==NULL)
        {
         //--- If the mouse button is pressed
         if(pressed)
           {
            //--- If the button is still pressed and held on the form, exit
            if(pressed_form)
              {
               return;
              }
            //--- If the button hold flag is not enabled yet, set the flags and enable chart tools
            if(!pressed_chart)
              {
               pressed_chart=true;  // Button is held on the chart
               pressed_form=false;  // Cursor is not above the form
               move=false;          // movement disabled
               this.SetChartTools(::ChartID(),true);
              }
           }
         //--- If the mouse button is not pressed
         else
           {
            //--- Get the list of extended standard graphical objects
            CArrayObj *list_ext=GetListStdGraphObjectExt();
            //--- In the loop by all extended graphical objects,
            int total=list_ext.Total();
            for(int i=0;i<total;i++)
              {
               //--- get the next graphical object
               CGStdGraphObj *obj=list_ext.At(i);
               if(obj==NULL)
                  continue;
               //--- and redraw it without a point and a circle
               obj.RedrawControlPointForms(0,CTRL_POINT_COLOR);
              }
           }
        }
      //--- If the cursor is above the form
      else
        {
         //--- If the button is still pressed and held on the chart, exit
         if(pressed_chart)
           {
            return;
           }
         
         //--- If the flag of holding the button on the form is not set yet
         if(!pressed_form)
           {
            pressed_chart=false;    // The button is not pressed on the chart
            this.SetChartTools(::ChartID(),false);
            
            //--- 'The cursor is inside the form, no mouse buttons are clicked' event handler
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED)
              {
               //--- If the cursor is above the form for managing the pivot point of an extended graphical object,
               if(graph_obj_id>WRONG_VALUE)
                 {
                  //--- get the object by its ID and by the chart ID
                  CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID());
                  if(graph_obj!=NULL)
                    {
                     //--- Get the toolkit of an extended standard graphical object
                     CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit();
                     if(toolkit!=NULL)
                       {
                        //--- Draw a circle with a point on the form
                        toolkit.DrawControlPoint(form,255,CTRL_POINT_COLOR);
                       }
                    }
                 }
              }
            //--- 'The cursor is inside the form, a mouse button is clicked (any)' event handler
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_PRESSED)
              {
               this.SetChartTools(::ChartID(),false);
               //--- If the flag of holding the form is not set yet
               if(!pressed_form)
                 {
                  pressed_form=true;      // set the flag of pressing on the form
                  pressed_chart=false;    // disable the flag of pressing on the form
                 }
              }
            //--- 'The cursor is inside the form, the mouse wheel is being scrolled' event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_WHEEL)
              {
               
              }
            
            
            //--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED)
              {
               //--- Set the cursor shift relative to the form initial coordinates
               form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX());
               form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY());
               //--- If the cursor is above the active area of the form for managing the pivot point of an extended graphical object,
               if(graph_obj_id>WRONG_VALUE)
                 {
                  //--- get the object by its ID and by the chart ID
                  CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID());
                  if(graph_obj!=NULL)
                    {
                     //--- Get the toolkit of an extended standard graphical object
                     CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit();
                     if(toolkit!=NULL)
                       {
                        //--- Draw a circle with a point on the form
                        toolkit.DrawControlPoint(form,255,CTRL_POINT_COLOR);
                       }
                    }
                 }
              }
            //--- 'The cursor is inside the active area,  any mouse button is clicked' event handler
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED && !move)
              {
               pressed_form=true;   // the flag of holding the mouse button on the form
               //--- If the left mouse button is pressed
               if(this.m_mouse.IsPressedButtonLeft())
                 {
                  //--- Set flags and form parameters
                  move=true;                                            // movement flag
                  form.SetInteraction(true);                            // flag of the form interaction with the environment
                  form.BringToTop();                                    // form on the background - above all others
                  this.ResetAllInteractionExeptOne(form);               // Reset interaction flags for all forms except the current one
                  form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX()); // Cursor shift relative to the X coordinate
                  form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY()); // Cursor shift relative to the Y coordinate
                 }
              }
            //--- 'The cursor is inside the active area, the mouse wheel is being scrolled' event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL)
              {
               
              }
            
            
            //--- 'The cursor is inside the window scrolling area, no mouse buttons are clicked' event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED)
              {
               
              }
            //--- 'The cursor is inside the window scrolling area, a mouse button is clicked (any)' event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED)
              {
               
              }
            //--- 'The cursor is inside the window scrolling area, the mouse wheel is being scrolled' event handler workpiece
            if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL)
              {
               
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Todas as melhorias no método são comentadas em detalhes diretamente no código, nós as deixamos para um estudo por conta própria. Em qualquer caso, todas as perguntas podem ser colocadas na discussão do artigo.

Estas são todas as melhorias que precisávamos fazer hoje.


Teste

Para testar, vamos pegar o Expert Advisor do artigo anterior e salvá-lo na nova pasta \MQL5\Experts\TestDoEasy\Part98\ com o novo nome TestDoEasyPart98.mq5.

Não faremos praticamente nenhuma alteração, exceto que criaremos três objetos-formas:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart98.mq5 |
//|                                  Copyright 2021, MetaQuotes Ltd. |
//|                             https://mql5.com/en/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, MetaQuotes Ltd."
#property link      "https://mql5.com/en/users/artmedia70"
#property version   "1.00"
//--- includes
#include <DoEasy\Engine.mqh>
//--- defines
#define        FORMS_TOTAL (3)   // Number of created forms
#define        START_X     (4)   // Initial X coordinate of the shape
#define        START_Y     (4)   // Initial Y coordinate of the shape
#define KEY_LEFT           (188) // Left
#define KEY_RIGHT          (190) // Right
#define KEY_ORIGIN         (191) // Initial properties
//--- input parameters
sinput   bool              InpMovable     =  true;          // Movable forms flag
sinput   ENUM_INPUT_YES_NO InpUseColorBG  =  INPUT_YES;     // Use chart background color to calculate shadow color
sinput   color             InpColorForm3  =  clrCadetBlue;  // Third form shadow color (if not background color) 
//--- global variables
CEngine        engine;
color          array_clr[];
//+------------------------------------------------------------------+

e, a esse respeito, corrigiremos levemente o cálculo das coordenadas de cada forma criada.
A declaração do objeto-forma será retirada do loop
.
A primeira forma será construída na coordenada Y 100, já o resto será traçado com um recuo de 20px em relação à borda inferior da forma anterior:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Set EA global variables
   ArrayResize(array_clr,2);        // Array of gradient filling colors
   array_clr[0]=C'26,100,128';      // Original ≈Dark-azure color
   array_clr[1]=C'35,133,169';      // Lightened original color
//--- Create the array with the current symbol and set it to be used in the library
   string array[1]={Symbol()};
   engine.SetUsedSymbols(array);
   //--- Create the timeseries object for the current symbol and period, and show its description in the journal
   engine.SeriesCreate(Symbol(),Period());
   engine.GetTimeSeriesCollection().PrintShort(false); // Short descriptions
//--- Create form objects
   CForm *form=NULL;
   for(int i=0;i<FORMS_TOTAL;i++)
     {
      //--- When creating an object, pass all the required parameters to it
      form=new CForm("Form_0"+string(i+1),30,(form==NULL ? 100 : form.BottomEdge()+20),100,30);
      if(form==NULL)
         continue;
      //--- Set activity and moveability flags for the form
      form.SetActive(true);
      form.SetMovable(true);
      //--- Set the form ID and the index in the list of objects
      form.SetID(i);
      form.SetNumber(0);   // (0 - main form object) Auxiliary objects may be attached to the main one. The main object is able to manage them
      //--- Set the opacity of 200
      form.SetOpacity(245);
      //--- The form background color is set as the first color from the color array
      form.SetColorBackground(array_clr[0]);
      //--- Form outlining frame color
      form.SetColorFrame(clrDarkBlue);
      //--- Draw the shadow drawing flag
      form.SetShadow(false);
      //--- Calculate the shadow color as the chart background color converted to the monochrome one
      color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100);
      //--- If the settings specify the usage of the chart background color, replace the monochrome color with 20 units
      //--- Otherwise, use the color specified in the settings for drawing the shadow
      color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-20) : InpColorForm3);
      //--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes
      //--- Set the shadow opacity to 200, while the blur radius is equal to 4
      form.DrawShadow(3,3,clr,200,4);
      //--- Fill the form background with a vertical gradient
      form.Erase(array_clr,form.Opacity(),true);
      //--- Draw an outlining rectangle at the edges of the form
      form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity());
      form.Done();
      
      //--- Display the text describing the gradient type and update the form
      //--- Text parameters: the text coordinates and the anchor point in the form center
      //--- Create a new text animation frame with the ID of 0 and display the text on the form
      form.TextOnBG(0,TextByLanguage("Тест 0","Test 0")+string(i+1),form.Width()/2,form.Height()/2,FRAME_ANCHOR_CENTER,C'211,233,149',255,true,true);
      //--- Add the form to the list
      if(!engine.GraphAddCanvElmToCollection(form))
         delete form;
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Além disso, no manipulador OnChartEvent() , encurtamos o nome dos objetos gráficos criados pelo clique do mouse (anteriormente o nome era "TrendLineExt") para não ultrapassar 63 caracteres ao criar um recurso gráfico:

   if(id==CHARTEVENT_CLICK)
     {
      if(!IsCtrlKeyPressed())
         return;
      //--- Get the chart click coordinates
      datetime time=0;
      double price=0;
      int sw=0;
      if(ChartXYToTimePrice(ChartID(),(int)lparam,(int)dparam,sw,time,price))
        {
         //--- Get the right point coordinates for a trend line
         datetime time2=iTime(Symbol(),PERIOD_CURRENT,1);
         double price2=iOpen(Symbol(),PERIOD_CURRENT,1);
         
         //--- Create the "Trend line" object
         string name_base="TLineExt";
         engine.CreateLineTrend(name_base,0,true,time,price,time2,price2);
         //--- Get the object from the list of graphical objects by chart name and ID
         CGStdGraphObj *obj=engine.GraphGetStdGraphObjectExt(name_base,ChartID());
         
         //--- Create the "Left price label" object
         string name_dep="PriceLeftExt";
         engine.CreatePriceLabelLeft(name_dep,0,false,time,price);
         //--- Get the object from the list of graphical objects by chart name and ID and
         CGStdGraphObj *dep=engine.GraphGetStdGraphObject(name_dep,ChartID());
         //--- add it to the list of graphical objects bound to the "Trend line" object
         obj.AddDependentObj(dep);
         //--- Set its pivot point by X and Y axis to the trend line left point
         dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,0,GRAPH_OBJ_PROP_PRICE,0);
         
         //--- Create the "Right price label" object
         name_dep="PriceRightExt";
         engine.CreatePriceLabelRight(name_dep,0,false,time2,price2);
         //--- Get the object from the list of graphical objects by chart name and ID and
         dep=engine.GraphGetStdGraphObject(name_dep,ChartID());
         //--- add it to the list of graphical objects bound to the "Trend line" object
         obj.AddDependentObj(dep);
         //--- Set its pivot point by X and Y axis to the trend line right point
         dep.AddNewLinkedCoord(GRAPH_OBJ_PROP_TIME,1,GRAPH_OBJ_PROP_PRICE,1);
        }
     }

//--- Handling graphical element collection events

Compilamos o Expert Advisor e o executamos no gráfico:


Então, o que vemos: a forma não entra na área onde está localizado o botão de ativação do painel de negociação com um clique, nem entra na área deste painel se ela estiver ativada. As formas de controle dos pontos de ancoragem do objeto gráfico padrão estendido funcionam como pretendido e não vão além dos limites do gráfico.
Mas também vemos deficiências. Depois de criar um objeto gráfico composto e ao mover seus pontos de ancoragem, eles ficam acima dos objetos-formas. Talvez isso seja normal, mas nem sempre. Por exemplo, se criamos um painel, a linha movida pelo cursor ainda deve aparecer sob o painel e não ser desenhada em cima dele. Se cada uma das formas for "clicada" com o mouse, essas formas se tornarão mais altas que o objeto gráfico composto e, quando movidas, não serão mais desenhadas sobre essas formas. Se você sobrepor parcialmente três formas umas sobre as outras, quando você passar o mouse sobre o segunda forma, a primeira se tornará ativa. Vamos corrigir isso, aqui será necessário usar, para todas as formas, a "profundidade" de sua localização em relação umas às outras e a outros objetos no gráfico.


O que virá a seguir?

No próximo artigo, continuaremos trabalhando em objetos gráficos compostos e suas funcionalidades.

Todos os arquivos da versão atual da biblioteca, arquivos do EA de teste e o indicador de controle de eventos do gráfico para MQL5 estão anexados abaixo. Você pode baixá-los e testar tudo sozinho. Se você tiver dúvidas, comentários e sugestões, pode expressá-los nos comentários do artigo.

Voltar ao conteúdo

*Artigos desta série:

Gráficos na biblioteca DoEasy (Parte 93): Preparando a funcionalidade para criar objetos gráficos compostos
Gráficos na biblioteca DoEasy (Parte 94): Objetos gráficos compostos, movimentação e eliminação
Gráficos na biblioteca DoEasy (Parte 95): Controles de objetos gráficos compostos
Gráficos na biblioteca DoEasy (Parte 96): Trabalhando com eventos de mouse/gráfico em objetos-forma
Gráficos na biblioteca DoEasy (Parte 97): Processando o movimento dos objetos-formas independentemente

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/10521

Arquivos anexados |
MQL5.zip (4233.69 KB)
Gráficos na biblioteca DoEasy (Parte 99): Movendo um objeto gráfico estendido com um ponto de controle Gráficos na biblioteca DoEasy (Parte 99): Movendo um objeto gráfico estendido com um ponto de controle
No último artigo, geramos o movimento dos pontos de ancoragem de um objeto gráfico estendido por meio de formas de controle. Agora vamos mover o objeto gráfico composto com ajuda de um ponto/forma de controle de objeto gráfico.
Desenvolvendo um EA de negociação do zero (Parte 24): Dado robustez ao sistema (I) Desenvolvendo um EA de negociação do zero (Parte 24): Dado robustez ao sistema (I)
Neste artigo iremos deixar o sistema mais robusto, de forma que ele fique mais estável e seguro de ser usado. Uma das formas de se conseguir robustez é procuramos reutilizar ao máximo o código, desta forma ele será testado o tempo todo e em diversas ocasiões diferente, mas esta é apenas uma das formas, outra forma é o uso da programação OOP.
Gráficos na biblioteca DoEasy (Parte 100): Eliminando bugs ao trabalhar com objetos gráficos padrão estendidos Gráficos na biblioteca DoEasy (Parte 100): Eliminando bugs ao trabalhar com objetos gráficos padrão estendidos
Hoje vamos retocar e eliminar falhas evidentes ao trabalhar com objetos gráficos estendidos (e padrão) e com objetos-formas na tela, além disso vamos consertar os erros observados durante os testes no último artigo. E assim vamos concluir esta seção da descrição da biblioteca.
Como desenvolver um sistema de negociação baseado no indicador MACD Como desenvolver um sistema de negociação baseado no indicador MACD
Neste artigo, nós aprenderemos uma nova ferramenta de nossa série: aprenderemos como projetar um sistema de negociação com base em um dos indicadores técnicos mais populares, o Moving Average Convergence Divergence (MACD).