Gráficos na biblioteca DoEasy (Parte 99): Movendo um objeto gráfico estendido com um ponto de controle
Conteúdo
Ideia
No último artigo, geramos o movimento dos pontos de ancoragem de um objeto gráfico estendido por meio de formas de controle. Mas ainda não temos a funcionalidade para mover um objeto gráfico totalmente. Assim como qualquer objeto gráfico padrão pode ser movido inteiramente ao mover seu ponto central, aqui faremos um ponto de controle central do objeto gráfico para que possamos mover este ponto e todo o objeto gráfico em si, e não seus pontos de ancoragem individuais. Como escolhemos um objeto gráfico composto para realizar o teste, e tal objeto é composto por uma linha de tendência cujas extremidades têm objetos etiquetas de preço, todo o trabalho será feito hoje para objetos gráficos que possuem dois pontos de ancoragem para mover suas extremidades e um ponto central para mover todo o objeto gráfico (dois pontos para modificar as extremidades do objeto e um ponto central para movê-lo). A seguir, criaremos essas formas com pontos de controle para outros objetos gráficos que têm mais de três pontos de controle.
Além disso, vamos otimizar um pouco o código que calcula as coordenadas da tela dos pontos de ancoragem de um objeto gráfico, isto é, vamos dividi-lo em métodos separados, para assim entender melhor a lógica principal. Afinal, é mais fácil ler o código quando contém uma chamada para um método que retorna um determinado valor (e dentro dele chamar outro método que, por sua vez, também calcula algo) do que escrever todo o código desses métodos no bloco de cálculo principal, o que o tornaria volumoso e difícil de ler.
Nem tudo que é feito hoje funcionará corretamente em alguns casos, como pretendido. Mas os artigos também descrevem exatamente o processo de desenvolvimento e criação de código para obter o resultado pretendido. Acho muito mais interessante percorrer quase todo o caminho desde o planejamento da funcionalidade até sua implementação do que apenas ler uma exposição seca, tipo "foi esse o resultado".
Como a função para obter as coordenadas da tela ChartTimePriceToXY() nos retorna as coordenadas apenas da parte visível do gráfico, infelizmente não poderemos calcular as coordenadas da tela do ponto da linha que fica fora do gráfico. A função sempre retornará o valor 0 se solicitarmos em pixels a coordenada X da tela de tempo que está fora do lado esquerdo visível no gráfico. Por esse motivo, ao mover um objeto gráfico composto na tela e quando sua parte esquerda ultrapassar a borda esquerda da tela, o ponto de ancoragem esquerdo do objeto permanecerá na coordenada 0 do gráfico em pixels. Isto irá distorcer a aparência do objeto gráfico. O mesmo se aplica ao lado direito do objeto gráfico e ao lado direito da tela do gráfico (assim como a parte superior e inferior). Portanto, limitaremos a saída do objeto gráfico composto fora da parte visível do gráfico. Isso evitará que a aparência do objeto gráfico seja distorcida quando um dos lados dele "vazar" a borda da tela quando for movido.
Modificando as classes da biblioteca
Como o objeto-forma para exibir o ponto de controle que gere os pontos de controle do objeto padrão estendido é um objeto importante nos objetos da biblioteca, mas essas formas não fazem parte da coleção de objetos gráficos, precisamos definir um novo tipo para tais formas. Cada um dos nossos objetos da biblioteca possui nomes próprios que dizem respeito aos tipos de objetos da biblioteca, esses nomes nos ajudam a determinar que tipo de objeto está ativo. Vamos definir este tipo para objetos-formas que gerem o ponto de controle como parte de objetos gráficos estendidos da biblioteca.
Adicionamos um novo tipo à enumeração de tipos de objeto de biblioteca no arquivo \MQL5\Include\DoEasy\Defines.mqh:
//+------------------------------------------------------------------+ //| List of library object types | //+------------------------------------------------------------------+ enum ENUM_OBJECT_DE_TYPE { //--- Graphics OBJECT_DE_TYPE_GBASE = COLLECTION_ID_LIST_END+1, // "Base object of all library graphical objects" object type OBJECT_DE_TYPE_GELEMENT, // "Graphical element" object type OBJECT_DE_TYPE_GFORM, // Form object type OBJECT_DE_TYPE_GFORM_CONTROL, // "Form for managing pivot points of graphical object" object type OBJECT_DE_TYPE_GSHADOW, // Shadow object type //--- Animation OBJECT_DE_TYPE_GFRAME, // "Single animation frame" object type OBJECT_DE_TYPE_GFRAME_TEXT, // "Single text animation frame" object type OBJECT_DE_TYPE_GFRAME_QUAD, // "Single rectangular animation frame" object type OBJECT_DE_TYPE_GFRAME_GEOMETRY, // "Single geometric animation frame" object type OBJECT_DE_TYPE_GANIMATIONS, // "Animations" object type //--- Managing graphical objects ... ... ... }
Inserimos os índices das novas mensagens da biblioteca no arquivo \MQL5\Include\DoEasy\Data.mqh:
MSG_LIB_SYS_REQUEST_OUTSIDE_LONG_ARRAY, // Request outside long array MSG_LIB_SYS_REQUEST_OUTSIDE_DOUBLE_ARRAY, // Request outside double array MSG_LIB_SYS_REQUEST_OUTSIDE_STRING_ARRAY, // Request outside string array MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY, // Request outside the array MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY, // Failed to convert graphical object coordinates to screen ones MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY, // Failed to convert time/price coordinates to screen ones
e as mensagens de texto correspondentes aos índices recém-adicionados:
{"Запрос за пределами long-массива","Data requested outside the long-array"}, {"Запрос за пределами double-массива","Data requested outside the double-array"}, {"Запрос за пределами string-массива","Data requested outside the string-array"}, {"Запрос за пределами массива","Data requested outside the array"}, {"Не удалось преобразовать координаты графического объекта в экранные","Failed to convert graphics object coordinates to screen coordinates"}, {"Не удалось преобразовать координаты время/цена в экранные","Failed to convert time/price coordinates to screen coordinates"},
Para entender que há (ou não há) um erro na conversão de coordenadas de tempo/preço para coordenadas de tela ao desenvolver a funcionalidade para mover objetos gráficos, iremos reportar isso, o que excluirá esta cadeia da verificação de erro na lógica ao procurar tal erro
Com a função ChartTimePriceToXY() podemos obter o erro de conversão de coordenadas, ela também é usada na classe do objeto janela do gráfico no arquivo \MQL5\Include\DoEasy\Objects\Chart\ChartWnd.mqh. Vamos escrever no método TimePriceToXY() desta classe a exibição de uma mensagem de erro no log ao tentar converter coordenadas:
//+------------------------------------------------------------------+ //| Convert chart coordinates from the time/price representation | //| to X and Y coordinates | //+------------------------------------------------------------------+ bool CChartWnd::TimePriceToXY(const datetime time,const double price) { ::ResetLastError(); if(!::ChartTimePriceToXY(this.m_chart_id,this.WindowNum(),time,price,this.m_wnd_coord_x,this.m_wnd_coord_y)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY); CMessage::ToLog(DFUN,::GetLastError(),true); return false; } return true; } //+------------------------------------------------------------------+
Primeiro, vamos exibir a entrada "Falha ao converter tempo/preço em coordenadas de tela" e, em seguida, uma descrição junto com o código do erro.
Como agora declaramos um novo tipo de objeto de biblioteca para formas-pontos que gerem pontos de ancoragem de um objeto gráfico estendido, precisamos criar uma classe desse objeto herdado da classe do objeto-forma. Nele, adicionaremos algumas variáveis e métodos para simplificar o trabalho com eles.
Vamos escrever essa classe nas ferramentas do objeto gráfico padrão estendido no arquivo \MQL5\Include\DoEasy\Objects\Graph\Extend\CGStdGraphObjExtToolkit.mqh:
//+------------------------------------------------------------------+ //| CGStdGraphObjExtToolkit.mqh | //| Copyright 2022, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2022, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\..\Graph\Form.mqh" //+------------------------------------------------------------------+ //| Class of the form for managing pivot points of a graphical object| //+------------------------------------------------------------------+ class CFormControl : public CForm { private: bool m_drawn; // Flag indicating that the pivot point is drawn on the form int m_pivot_point; // Pivot point managed by the form public: //--- (1) Return and (2) set the drawn point flag bool IsControlAlreadyDrawn(void) const { return this.m_drawn; } void SetControlPointDrawnFlag(const bool flag) { this.m_drawn=flag; } //--- (1) Return and (2) set the pivot point managed by the form int GraphObjPivotPoint(void) const { return this.m_pivot_point; } void SetGraphObjPivotPoint(const int index) { this.m_pivot_point=index; } //--- Constructor CFormControl(void) { this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL; } CFormControl(const long chart_id,const int subwindow,const string name,const int pivot_point,const int x,const int y,const int w,const int h) : CForm(chart_id,subwindow,name,x,y,w,h) { this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL; this.m_pivot_point=pivot_point; } }; //+------------------------------------------------------------------+ //| Extended standard graphical | //| object toolkit class | //+------------------------------------------------------------------+ class CGStdGraphObjExtToolkit : public CObject
A variável membro privado da classe m_drawn armazenará um sinalizador nos informando que o ponto já foi desenhado na forma. Por que essa variável é necessária? Se o cursor do mouse for removido da área ativa da forma de controle para gerir os pontos de controle do objeto gráfico, precisamos apagar o ponto desenhado na forma. Agora, em todas essas formas, o ponto desenhado é permanentemente apagado se o ponteiro do mouse não estiver sobre a área ativa da forma. Isto é algo pouco prático, não faz sentido sobrecarregar o sistema com o constante redesenho de todas essas formas se podemos primeiro olhar para o sinalizador que nos indica que a forma já foi redesenhada e que não há nenhum ponto nela. Este sinalizador nos indicará que o ponto já foi traçado ou apagado. E como no futuro desenvolveremos alguns efeitos visuais para desenho desses pontos (e não apenas para eles), será melhor ter um sinalizador que será definido imediatamente após a execução do manipulador de efeitos visuais do que determinar de alguma forma que o desenho processo já tem sido concluído.
A variável membro privado m_pivot_point armazenará o índice do ponto de ancoragem que a forma gere. Um objeto gráfico tem vários desses pontos de controle. Por exemplo, uma linha de tendência tem três, isto é: dois pontos nas extremidades da linha (para alterar independentemente a localização das extremidades da linha) e um ponto central (para mover todo o objeto). Os índices armazenados nos objetos-formas corresponderão aos índices dos pontos de controle desse tipo de linha: 0 e 1 para pontos das extremidades da linha e 2 para a linha central. Outros objetos gráficos podem ter pontos de controle completamente diferentes, mas todos os índices corresponderão aos pontos de controle existentes do objeto + um adicional (nem sempre, e isso será discutido em artigos futuros) para mover todo o objeto.
Os métodos públicos da classe são usados para definir/retornar os valores das variáveis descritas acima. A classe também tem dois construtores. No construtor padrão, o novo tipo OBJECT_DE_TYPE_GFORM_CONTROL, que adicionamos hoje, é definido no tipo de objeto.
Todos os valores passados para o construtor da classe pai são passados para o construtor paramétrico, além de uma variável, isto é, o índice do ponto de ancoragem do objeto gráfico que a forma criada gere.
Agora como todos os formas que gerem os pontos de controle na classe CGStdGraphObjExtToolkit serão do tipo CFormControl, precisamos alterar o tipo do objeto-forma CForm para CFormControl, e devemos adicionar novos métodos para trabalhar com os formas que gerem os pontos de controle do objeto gráfico:
//+------------------------------------------------------------------+ //| Extended standard graphical | //| object toolkit class | //+------------------------------------------------------------------+ class CGStdGraphObjExtToolkit : public CObject { private: long m_base_chart_id; // Base graphical object chart ID int m_base_subwindow; // Base graphical object chart subwindow ENUM_OBJECT m_base_type; // Base object type string m_base_name; // Base object name int m_base_pivots; // Number of base object reference points datetime m_base_time[]; // Time array of base object reference points double m_base_price[]; // Price array of base object reference points int m_base_x; // Base object X coordinate int m_base_y; // Base object Y coordinate int m_ctrl_form_size; // Size of forms for managing reference points int m_shift; // Shift coordinates for adjusting the form location CArrayObj m_list_forms; // List of form objects for managing reference points //--- Create a form object on a base object reference point CFormControl *CreateNewControlPointForm(const int index); //--- Return X and Y (1) screen coordinates of the specified reference point of the graphical object bool GetControlPointCoordXY(const int index,int &x,int &y); //--- Set the parameters of a form object for managing pivot points void SetControlFormParams(CFormControl *form,const int index); public: //--- Set the parameters of the base object of a composite graphical object void SetBaseObj(const ENUM_OBJECT base_type,const string base_name, const long base_chart_id,const int base_subwindow, const int base_pivots,const int ctrl_form_size, const int base_x,const int base_y, const datetime &base_time[],const double &base_price[]); //--- Set the base object (1) time, (2) price, (3) time and price coordinates void SetBaseObjTime(const datetime time,const int index); void SetBaseObjPrice(const double price,const int index); void SetBaseObjTimePrice(const datetime time,const double price,const int index); //--- Set the base object (1) X, (2) Y, (3) X and Y screen coordinates void SetBaseObjCoordX(const int value) { this.m_base_x=value; } void SetBaseObjCoordY(const int value) { this.m_base_y=value; } void SetBaseObjCoordXY(const int value_x,const int value_y) { this.m_base_x=value_x; this.m_base_y=value_y; } //--- (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 CFormControl *GetControlPointForm(const int index) { return this.m_list_forms.At(index); } CFormControl *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); //--- (1) Draw a control point on the form, (2) draw a control point on the form and delete it on all other forms void DrawControlPoint(CFormControl *form,const uchar opacity,const color clr); void DrawOneControlPoint(CFormControl *form,const uchar opacity=255,const color clr=CTRL_POINT_COLOR); //--- (1) Draw using a default color, (remove) a control point on the form void DrawControlPoint(CFormControl *form) { this.DrawControlPoint(form,255,CTRL_POINT_COLOR);} void ClearControlPoint(CFormControl *form) { this.DrawControlPoint(form,0,CTRL_POINT_COLOR); } //--- Remove all form objects from the list void DeleteAllControlPointForm(void); //--- Event handler void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Constructor/destructor CGStdGraphObjExtToolkit(const ENUM_OBJECT base_type,const string base_name, const long base_chart_id,const int base_subwindow, const int base_pivots,const int ctrl_form_size, const int base_x,const int base_y, const datetime &base_time[],const double &base_price[]) { this.m_list_forms.Clear(); this.SetBaseObj(base_type,base_name,base_chart_id,base_subwindow,base_pivots,ctrl_form_size,base_x,base_y,base_time,base_price); this.CreateAllControlPointForm(); } CGStdGraphObjExtToolkit(){;} ~CGStdGraphObjExtToolkit(){;} }; //+------------------------------------------------------------------+
Vamos modificar o método GetControlPointCoordXY(), que retorna as coordenadas X e Y do ponto de ancoragem especificado do objeto gráfico nas coordenadas da tela.
Anteriormente, o método simplesmente retornava as coordenadas calculadas do ponto de ancoragem especificado do objeto gráfico. Agora precisamos levar em conta que objetos gráficos podem ter um número diferente de pontos de ancoragem e uma localização diferente do ponto de ancoragem central. Por isso, faremos um cálculo para diferentes tipos de objetos no interruptor switch(). Ao mesmo tempo, precisamos levar em consideração as coordenadas do ponto de ancoragem que queremos obter, isto é, um daqueles que estão localizados nas extremidades do objeto ou um comum a todos, que é o central. Se o índice do ponto de ancoragem passado para o método for menor que o número total de pontos de ancoragem do objeto gráfico, as coordenadas do ponto de ancoragem serão solicitadas. Caso contrário, serão solicitadas as coordenadas do ponto de ancoragem central.
Por enquanto, obteremos apenas as coordenadas X e Y para aqueles objetos gráficos que possuem dois pontos de ancoragem nas bordas e um central:
//+------------------------------------------------------------------+ //| Return the X and Y coordinates of the specified pivot point | //| of the graphical object in screen coordinates | //+------------------------------------------------------------------+ bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(const int index,int &x,int &y) { //--- Declare form objects, from which we are to receive their screen coordinates CFormControl *form0=NULL, *form1=NULL; //--- Set X and Y to zero - these values will be received in case of a failure x=0; y=0; //--- Depending on the graphical object type switch(this.m_base_type) { //--- Objects drawn using screen coordinates case OBJ_LABEL : case OBJ_BUTTON : case OBJ_BITMAP_LABEL : case OBJ_EDIT : case OBJ_RECTANGLE_LABEL : case OBJ_CHART : case OBJ_EVENT : //--- Write object screen coordinates and return 'true' x=this.m_base_x; y=this.m_base_y; return true; //--- Lines (vertical and horizontal) case OBJ_VLINE : break; case OBJ_HLINE : break; //--- Lines case OBJ_TREND : case OBJ_TRENDBYANGLE : case OBJ_CYCLES : case OBJ_ARROWED_LINE : //--- Channels case OBJ_CHANNEL : case OBJ_STDDEVCHANNEL : case OBJ_REGRESSION : //--- Gann case OBJ_GANNLINE : case OBJ_GANNGRID : //--- Fibo case OBJ_FIBO : case OBJ_FIBOTIMES : case OBJ_FIBOFAN : case OBJ_FIBOARC : case OBJ_FIBOCHANNEL : case OBJ_EXPANSION : //--- Calculate coordinates for forms on the line pivot points if(index<this.m_base_pivots) return(::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y) ? true : false); //--- Calculate the coordinates for the central form located between the line pivot points else { form0=this.GetControlPointForm(0); form1=this.GetControlPointForm(1); if(form0==NULL || form1==NULL) return false; x=(form0.CoordX()+this.m_shift+form1.CoordX()+this.m_shift)/2; y=(form0.CoordY()+this.m_shift+form1.CoordY()+this.m_shift)/2; return true; } //--- Channels case OBJ_PITCHFORK : break; //--- Gann case OBJ_GANNFAN : break; //--- Elliott case OBJ_ELLIOTWAVE5 : break; case OBJ_ELLIOTWAVE3 : break; //--- Shapes case OBJ_RECTANGLE : break; case OBJ_TRIANGLE : break; case OBJ_ELLIPSE : break; //--- Arrows case OBJ_ARROW_THUMB_UP : break; case OBJ_ARROW_THUMB_DOWN : break; case OBJ_ARROW_UP : break; case OBJ_ARROW_DOWN : break; case OBJ_ARROW_STOP : break; case OBJ_ARROW_CHECK : break; case OBJ_ARROW_LEFT_PRICE : break; case OBJ_ARROW_RIGHT_PRICE : break; case OBJ_ARROW_BUY : break; case OBJ_ARROW_SELL : break; case OBJ_ARROW : break; //--- Graphical objects with time/price coordinates case OBJ_TEXT : break; case OBJ_BITMAP : break; //--- default : break; } return false; } //+------------------------------------------------------------------+
O cálculo do ponto de ancoragem é feito a partir dos valores armazenados nas matrizes de coordenadas de pontos de ancoragem de linha m_base_time e m_base_price. E para calcular as coordenadas do ponto central, usaremos as coordenadas dos objetos-formas anexados aos pontos de ancoragem extremos da linha. Se as coordenadas forem calculadas com sucesso, o método retornará imediatamente true. Se não for assim, devolverá false, ou o break interromperá a execução do código no case do interruptor switch, e caímos no final do método, onde false será retornado.
No método que retorna um ponteiro para a forma de ponto de ancoragem por nome, substituímos CForm por CFormControl:
//+------------------------------------------------------------------+ //| Return the pointer to the pivot point form by name | //+------------------------------------------------------------------+ CFormControl *CGStdGraphObjExtToolkit::GetControlPointForm(const string name,int &index) { index=WRONG_VALUE; for(int i=0;i<this.m_list_forms.Total();i++) { CFormControl *form=this.m_list_forms.At(i); if(form==NULL) continue; if(form.Name()==name) { index=i; return form; } } return NULL; } //+------------------------------------------------------------------+
No método que cria o objeto-forma no ponto de ancoragem do objeto base, substituímos CForm por CFormControl e definimos os parâmetros para o objeto-forma criado com sucesso:
//+------------------------------------------------------------------+ //| Create a form object on a base object reference point | //+------------------------------------------------------------------+ CFormControl *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index) { string name=this.m_base_name+"_CP_"+(index<this.m_base_pivots ? (string)index : "X"); CFormControl *form=this.GetControlPointForm(index); if(form!=NULL) return NULL; int x=0, y=0; if(!this.GetControlPointCoordXY(index,x,y)) return NULL; form=new CFormControl(this.m_base_chart_id,this.m_base_subwindow,name,index,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize()); //--- Set all the necessary properties for the created form object if(form!=NULL) this.SetControlFormParams(form,index); return form; } //+------------------------------------------------------------------+
No método que cria objetos-formas nos pontos de ancoragem do objeto base, substituímos CForm por CFormControl e removemos as linhas para definir os parâmetros do objeto-forma criado, pois os parâmetros agora são definidos imediatamente quando o objeto é criado no método acima:
//+------------------------------------------------------------------+ //| 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 CFormControl *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; } } //--- Redraw the chart for displaying changes (if successful) and return the final result if(res) ::ChartRedraw(this.m_base_chart_id); return res; } //+------------------------------------------------------------------+
Agora o loop passa não pelo número de pontos de ancoragem do objeto base, mas, sim, por mais um. Ou seja, será criada mais uma forma do que pontos de ancoragem o objeto gráfico possui. A última forma será a central, para mover todo o objeto gráfico.
Método que define os parâmetros do objeto-forma que gere o ponto de controle:
//+------------------------------------------------------------------+ //| Set the parameters of a form object for managing pivot points | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetControlFormParams(CFormControl *form,const int index) { 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 center of the form, 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 form.SetID(index+1); // Set the form ID form.SetControlPointDrawnFlag(false); // Set the flag that the pivot point is not drawn on the form form.Done(); // Save the initial form object state (its appearance) } //+------------------------------------------------------------------+
Aqui estão as linhas de código transportadas do método acima. E a configuração do sinalizador do ponto desenhado na forma e o identificador da forma são adicionados.
No método que desenha um ponto de controle na forma, colocaremos o cálculo do centro da forma em uma linha separada, isto para não realizar essencialmente os mesmos cálculos quatro vezes, e após a conclusão do método, definimos o sinalizador do ponto desenhado na forma:
//+------------------------------------------------------------------+ //| Draw a control point on the form | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::DrawControlPoint(CFormControl *form,const uchar opacity,const color clr) { if(form==NULL) return; int c=int(::floor(form.Width()/2)); // Form center (coordinates) form.DrawCircle(c,c,CTRL_POINT_RADIUS,clr,opacity); // Draw a circle in the form center form.DrawCircleFill(c,c,2,clr,opacity); // Draw a circle in the form center form.SetControlPointDrawnFlag(opacity>0 ? true : false); // Set the flag that the pivot point is drawn on the form } //+------------------------------------------------------------------+
O que temos feito agora é que quando passamos o cursor sobre a forma de controle do ponto de ancoragem do objeto gráfico, um ponto é desenhado sobre ela. O ponto é apagado somente após o cursor sair da forma. Mas se aproximarmos todos os pontos de ancoragem do objeto de modo que as formas construídas nas extremidades do objeto gráfico e a forma central comecem a se sobrepor, mover o cursor para longe de uma forma fará com que o cursor se mova para outra forma próxima. Assim, é possível obter tal resultado que todos os pontos em todas as formas do objeto serão exibidos:
Se ao mesmo tempo capturarmos a forma e começar a movê-la, o ponto de ancoragem do objeto se moverá depois dela. Mas as formas erroneamente mostradas permanecerão onde estavam antes do início do movimento. Isto não é correto. Portanto, precisamos de um método que desenhe um ponto em um objeto-forma de um objeto gráfico e simultaneamente apague pontos em outros objetos-formas do mesmo objeto.
Método que desenha um ponto de controle em uma forma e os remove em todas as outras formas:
//+------------------------------------------------------------------+ //| Draw a control point on the form, | //| remove it on all other forms | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::DrawOneControlPoint(CFormControl *form,const uchar opacity=255,const color clr=CTRL_POINT_COLOR) { this.DrawControlPoint(form,opacity,clr); for(int i=0;i<this.GetNumControlPointForms();i++) { CFormControl *ctrl=this.GetControlPointForm(i); if(ctrl==NULL || ctrl.ID()==form.ID()) continue; this.ClearControlPoint(ctrl); } } //+------------------------------------------------------------------+
Aqui: um ponteiro para a forma sobre a qual o cursor está localizado é passado para o método. Desenhamos um ponto nesta forma e, em seguida, em um loop por todas as formas desse objeto, selecionamos a forma, e, se não for uma forma passada para o método, apagamos o ponto nela.
No manipulador de eventos, alteramos o tipo de forma de CForm para CFormControl:
//+------------------------------------------------------------------+ //| 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++) { CFormControl *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); } } //+------------------------------------------------------------------+
Na classe de objeto gráfico padrão abstrato no arquivo \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh, precisamos fazer algumas melhorias para otimizar o código do método. Temos trechos repetidos do mesmo código em métodos diferentes, então faz sentido agrupar esses blocos de código em métodos separados e chamá-los quando necessário, tornando tudo mais fácil de ler.
Nas seções public e private da classe, declararemos os novos métodos para os quais transferiremos as seções repetidas do código:
//--- 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); //--- Set X and Y coordinates into the appropriate pivot points of a specified subordinate object bool SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj); //--- Return the pivot point data object CLinkedPivotPoint*GetLinkedPivotPoint(void) { return &this.m_linked_pivots; }
...
private: //--- Set the X coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object void SetCoordXToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to); void SetCoordXFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to); //--- Set the Y coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object void SetCoordYToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to); void SetCoordYFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to); //--- Set X and Y coordinates into the appropriate pivot point of a specified subordinate object void SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj,CLinkedPivotPoint *pivot_point,const int index); //--- Set the (1) integer, (2) real and (3) string property to the specified subordinate property void SetDependentINT(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_INTEGER prop,const long value,const int modifier); void SetDependentDBL(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_DOUBLE prop,const double value,const int modifier); void SetDependentSTR(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_STRING prop,const string value,const int modifier); public: //--- Event handler
No método que verifica as alterações nas propriedades do objeto, excluímos o bloco de código especificado (este código será movido para um método separado):
//--- If subordinate objects are attached to the base one (in a composite graphical object) if(this.m_list.Total()>0) { //--- In the loop by the number of added graphical objects, for(int i=0;i<this.m_list.Total();i++) { //--- get the next graphical object, CGStdGraphObj *dep=m_list.At(i); if(dep==NULL) continue; //--- get the data object of its pivot points, CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint(); if(pp==NULL) continue; //--- 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 in the object selected as the current one in the main loop 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 in the object selected as the current one in the main loop int prop_from=pp.GetPropertyY(j,ny); int modifier_from=pp.GetPropertyModifierY(j,ny); this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny); } } dep.PropertiesCopyToPrevData(); } //--- Move reference control points to new coordinates if(ExtToolkit!=NULL) { for(int i=0;i<this.Pivots();i++) { ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i); } ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); long lparam=0; double dparam=0; string sparam=""; ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } //--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes ::ChartRedraw(m_chart_id); }
Em vez de um bloco separado, vamos escrever uma chamada para um novo método:
//--- If subordinate objects are attached to the base one (in a composite graphical object) if(this.m_list.Total()>0) { //--- In the loop by the number of added graphical objects, for(int i=0;i<this.m_list.Total();i++) { //--- get the next graphical object, CGStdGraphObj *dep=m_list.At(i); if(dep==NULL) continue; //--- Set X and Y coordinates to all pivot points of a subordinate object and //--- save the current properties of a subordinate graphical object as the previous ones if(this.SetCoordsXYtoDependentObj(dep)) dep.PropertiesCopyToPrevData(); } //--- Move reference control points to new coordinates if(this.ExtToolkit!=NULL) { for(int i=0;i<this.Pivots();i++) { this.ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i); } this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); long lparam=0; double dparam=0; string sparam=""; this.ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } //--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes ::ChartRedraw(m_chart_id); }
Como agora nossa lógica de exclusão do ponto desenhado da forma-objeto que gere os pontos de ancoragem do objeto gráfico é feita de tal forma que, se o cursor não estiver em nenhuma das formas, cada uma dessas formas será constantemente redesenhada, o que não é ideal e consome muitos recursos, portanto, no método que redesenha a forma que gere o ponto de controle do objeto gráfico padrão estendido inseriremos uma verificação de que, se precisarmos apagar o ponto, e ele ainda estiver desenhado, então somente neste caso precisaremos redesenhar a forma para apagar o ponto. Bem, vamos substituir o tipo de objeto-forma por um novo:
//+------------------------------------------------------------------+ //| 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 CFormControl *form=this.ExtToolkit.GetControlPointForm(i); if(form==NULL) continue; //--- Draw a point and a circle with a specified non-transparency and color //--- If a point should be completely transparent (deleted), //--- and the form still has the point, delete the point, if(opacity==0 && form.IsControlAlreadyDrawn()) this.ExtToolkit.DrawControlPoint(form,0,clr); //--- otherwise, draw the point with a specified non-transparency and color else 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); } } //+------------------------------------------------------------------+
Agora, o ponto será apagado apenas se realmente precisar ser apagado (opacidade do ponto definida como zero) e se o ponto ainda estiver desenhado (sinalizador de ponto desenhado definido).
Também retrabalharemos o método que altera as coordenadas X e Y dos objetos atuais e de todos os dependentes, para tal removeremos dele as seções de código que agora serão substituídas por chamadas para o novo método:
//+----------------------------------------------------------------------+ //| 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 dependent 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 dependent 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; } //+------------------------------------------------------------------+
Agora o método será muito mais simples:
//+----------------------------------------------------------------------+ //| 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 a composite graphical object, //--- and subordinate graphical objects are attached to the object if(this.ExtToolkit!=NULL && this.m_list.Total()>0) { //--- Get the graphical object bound to the 'modifier' point CGStdGraphObj *dep=this.GetDependentObj(modifier); if(dep==NULL) return false; //--- Set X and Y coordinates to all pivot points of a subordinate object and //--- save the current properties of a subordinate graphical object as the previous ones if(this.SetCoordsXYtoDependentObj(dep)) 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; } //+------------------------------------------------------------------+
Escrevemos uma implementação do método que define as coordenadas X e Y para o ponto de ancoragem vinculado do objeto filho especificado por índice:
//+------------------------------------------------------------------+ //| Set X and Y coordinates to the associated pivot point | //| of a specified subordinate object by index | //+------------------------------------------------------------------+ void CGStdGraphObj::SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj,CLinkedPivotPoint *pivot_point,const int index) { //--- get the number of coordinate points of the base object for setting the X coordinate int numx=pivot_point.GetBasePivotsNumX(index); //--- 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 'index' point int prop_from=pivot_point.GetPropertyX(index,nx); int modifier_from=pivot_point.GetPropertyModifierX(index,nx); this.SetCoordXToDependentObj(dependent_obj,prop_from,modifier_from,nx); } //--- Get the number of coordinate points of the base object for setting the Y coordinate int numy=pivot_point.GetBasePivotsNumY(index); //--- 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 'index' point int prop_from=pivot_point.GetPropertyY(index,ny); int modifier_from=pivot_point.GetPropertyModifierY(index,ny); this.SetCoordYToDependentObj(dependent_obj,prop_from,modifier_from,ny); } } //+------------------------------------------------------------------+
Na verdade, esses são os mesmos blocos de código que removemos dos métodos da classe e transferimos para ela. Consideramos a lógica desse código em artigos anteriores, e está tudo explicado nos comentários do código, por isso acho que não precisa de explicações adicionais.
Implementação do método que define as coordenadas X e Y para os pontos de ancoragem vinculados do objeto filho especificado:
//+------------------------------------------------------------------+ //| Set X and Y coordinates to associated pivot points | //| of the specified subordinate object | //+------------------------------------------------------------------+ bool CGStdGraphObj::SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj) { //--- Get the object of pivot point data of the bound graphical object CLinkedPivotPoint *pp=dependent_obj.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, //--- set X and Y to all pivot points of a subordinate object for(int j=0;j<num;j++) this.SetCoordsXYtoDependentObj(dependent_obj,pp,j); return true; } //+------------------------------------------------------------------+
O método permite definir coordenadas para todos os pontos de controle de um objeto subordinado. Se outros objetos gráficos estiverem anexados ao objeto gráfico composto, este método definirá neles nas coordenadas especificadas.
Inserimos modificações na classe-coleção de elementos gráficos \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh.
Como a função padrão ChartTimePriceToXY() nos retorna duas coordenadas de uma só vez, X e Y, para armazená-las, criaremos uma estrutura na seção privada que armazenará as coordenadas X e Y e os deslocamentos dessas coordenadas em relação ao ponto central. E como um objeto gráfico pode ter vários pontos de ancoragem, para armazenar as coordenadas de cada um dos pontos de ancoragem para dado objeto gráfico, vamos declarar um array com o tipo da estrutura criada. Desse modo, em cada célula deste array teremos X e Y convertidos (de coordenadas "tempo/preço" em coordenadas de tela X e Y), bem como deslocamentos das coordenadas do ponto de ancoragem em relação ao ponto central do objeto gráfico.
criamos uma estrutura na seção privada da classe e declaramos o array que precisamos:
//+------------------------------------------------------------------+ //| Collection of graphical objects | //+------------------------------------------------------------------+ #resource "\\"+PATH_TO_EVENT_CTRL_IND; // Indicator for controlling graphical object events packed into the program resources class CGraphElementsCollection : public CBaseObj { private: //--- Pivot point data structure struct SDataPivotPoint { public: int X; // Pivot point X coordinate int Y; // Pivot point Y coordinate int ShiftX; // Pivot point X coordinate shift from the central one int ShiftY; // Pivot point Y coordinate shift from the central one }; SDataPivotPoint m_data_pivot_point[]; // Pivot point data structure array CArrayObj m_list_charts_control; // List of chart management objects CListObj m_list_all_canv_elm_obj; // List of all graphical elements on canvas CListObj m_list_all_graph_obj; // List of all graphical objects CArrayObj m_list_deleted_obj; // List of removed graphical objects CMouseState m_mouse; // "Mouse status" class object bool m_is_graph_obj_event; // Event flag in the list of graphical objects int m_total_objects; // Number of graphical objects int m_delta_graph_obj; // Difference in the number of graphical objects compared to the previous check
Na seção privada da classe, vamos declarar um método que devolve as coordenadas de tela de cada ponto de ancoragem do objeto gráfico ao array de estrutura:
private: //--- Find an object present in the collection but not on a chart CGStdGraphObj *FindMissingObj(const long chart_id); CGStdGraphObj *FindMissingObj(const long chart_id,int &index); //--- Find the graphical object present on a chart but not in the collection string FindExtraObj(const long chart_id); //--- Remove the graphical object class object from the graphical object collection list: (1) specified object, (2) by chart ID bool DeleteGraphObjFromList(CGStdGraphObj *obj); void DeleteGraphObjectsFromList(const long chart_id); //--- Move the graphical object class object to the list of removed graphical objects: (1) specified object, (2) by index bool MoveGraphObjToDeletedObjList(CGStdGraphObj *obj); bool MoveGraphObjToDeletedObjList(const int index); //--- Move all objects by chart ID to the list of removed graphical objects void MoveGraphObjectsToDeletedObjList(const long chart_id); //--- Remove the object of managing charts from the list bool DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj); //--- Set the flags of scrolling the chart with the mouse, context menu and crosshairs tool for the specified chart void SetChartTools(const long chart_id,const bool flag); //--- Return the screen coordinates of each pivot point of the graphical object bool GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[]); public:
Escrevemos a implementação deste método fora do corpo da classe:
//+------------------------------------------------------------------+ //| Return screen coordinates | //| of each graphical object pivot point | //+------------------------------------------------------------------+ bool CGraphElementsCollection::GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[]) { //--- If failed to increase the array of structures to match the number of pivot points, //--- inform of that in the journal and return 'false' if(::ArrayResize(array_pivots,obj.Pivots())!=obj.Pivots()) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); return false; } //--- In the loop by the number of graphical object pivot points for(int i=0;i<obj.Pivots();i++) { //--- Convert the object coordinates into screen ones. If failed, inform of that and return 'false' if(!::ChartTimePriceToXY(obj.ChartID(),obj.SubWindow(),obj.Time(i),obj.Price(i),array_pivots[i].X,array_pivots[i].Y)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY); return false; } } //--- Depending on the graphical object type switch(obj.TypeGraphObject()) { //--- One pivot point in screen coordinates case OBJ_LABEL : case OBJ_BUTTON : case OBJ_BITMAP_LABEL : case OBJ_EDIT : case OBJ_RECTANGLE_LABEL : case OBJ_CHART : break; //--- One pivot point (price only) case OBJ_HLINE : break; //--- One pivot point (time only) case OBJ_VLINE : break; case OBJ_EVENT : break; //--- Two pivot points and a central one //--- Lines case OBJ_TREND : case OBJ_TRENDBYANGLE : case OBJ_CYCLES : case OBJ_ARROWED_LINE : //--- Channels case OBJ_CHANNEL : case OBJ_STDDEVCHANNEL : case OBJ_REGRESSION : //--- Gann case OBJ_GANNLINE : case OBJ_GANNGRID : //--- Fibo case OBJ_FIBO : case OBJ_FIBOTIMES : case OBJ_FIBOFAN : case OBJ_FIBOARC : case OBJ_FIBOCHANNEL : case OBJ_EXPANSION : //--- Calculate the shifts of all pivot points from the central one and write them to the structure array array_pivots[0].ShiftX=(array_pivots[1].X-array_pivots[0].X)/2; array_pivots[0].ShiftY=(array_pivots[1].Y-array_pivots[0].Y)/2; array_pivots[1].ShiftX=(array_pivots[0].X-array_pivots[1].X)/2; array_pivots[1].ShiftY=(array_pivots[0].Y-array_pivots[1].Y)/2; return true; //--- Channels case OBJ_PITCHFORK : break; //--- Gann case OBJ_GANNFAN : break; //--- Elliott case OBJ_ELLIOTWAVE5 : break; case OBJ_ELLIOTWAVE3 : break; //--- Shapes case OBJ_RECTANGLE : break; case OBJ_TRIANGLE : break; case OBJ_ELLIPSE : break; //--- Arrows case OBJ_ARROW_THUMB_UP : break; case OBJ_ARROW_THUMB_DOWN : break; case OBJ_ARROW_UP : break; case OBJ_ARROW_DOWN : break; case OBJ_ARROW_STOP : break; case OBJ_ARROW_CHECK : break; case OBJ_ARROW_LEFT_PRICE : break; case OBJ_ARROW_RIGHT_PRICE : break; case OBJ_ARROW_BUY : break; case OBJ_ARROW_SELL : break; case OBJ_ARROW : break; //--- Graphical objects with time/price coordinates case OBJ_TEXT : break; case OBJ_BITMAP : break; //--- default: break; } return false; } //+------------------------------------------------------------------+
Por enquanto, esse método grava na estrutura as coordenadas da tela apenas dos objetos gráficos que possuem dois pontos de ancoragem e um central.
Um ponteiro para um objeto gráfico é passado para o método, objeto esse cujas coordenadas de seus pontos de ancoragem devem ser escritas em um array de estruturas, que também é passado para o método por referência. Se a transformação de coordenadas for bem sucedida, o método retorna true e um array de estruturas completamente preenchido com coordenadas de tela para cada ponto de ancoragem do objeto gráfico. Em caso de falha, o método retorna false.
No manipulador de eventos de classe, precisamos manipular o deslocamento da forma de controle do objeto para que, se este for o ponto central, o objeto se mova em sua totalidade. Para isso, precisamos calcular os deslocamentos de suas formas extremas em relação à central (para a qual o objeto é deslocado) e deslocar ambos os pontos de ancoragem do objeto com base no deslocamento calculado e registrado na estrutura. Assim, todos os seus pontos de ancoragem serão deslocados mantendo a proporção de movimento do ponto central mexido pelo mouse.
Inserimos o seguinte processamento do evento para mover o ponto de controle central (forma) do objeto gráfico estendido:
//+------------------------------------------------------------------+ //| 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 form index of 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 outside the 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 chart has no one-click trading panel, if(!::ChartGetInteger(form.ChartID(),CHART_SHOW_ONE_CLICK)) { //--- calculate the form coordinates so that the form does not overlap with the one-click trading panel button if(y<17 && x<41) y=17; } //--- If the chart has the one-click trading panel enabled, else { //--- calculate the form coordinate so that the form does not enter the one-click trading panel area during relocation 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 the 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 is received, if(ext!=NULL) { //--- get the object type ENUM_OBJECT type=ext.GraphObjectType(); //--- If the object is constructed based on 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 based on time/price coordinates else { //--- calculate the coordinate shift int shift=(int)::ceil(form.Width()/2)+1; //--- If the form is located on one of the graphical object pivot points, if(form_index<ext.Pivots()) { //--- limit the form coordinates so that they do not move beyond the chart borders 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 calculated coordinates in the object ext.ChangeCoordsExtendedObj(x+shift,y+shift,form_index); } //--- If the form is central for managing all pivot points of a graphical object else { //--- Get screen coordinates of all object pivot points and write them to the m_data_pivot_point structure if(this.GetPivotPointCoordsAll(ext,m_data_pivot_point)) { //--- In the loop by the number of object pivot points, for(int i=0;i<(int)this.m_data_pivot_point.Size();i++) { //--- limit the screen coordinates of the current pivot point so that they do not move beyond the chart borders if(x+shift-this.m_data_pivot_point[i].ShiftX<0) x=-shift+m_data_pivot_point[i].ShiftX; if(x+shift+this.m_data_pivot_point[i].ShiftX>chart_width) x=chart_width-shift-this.m_data_pivot_point[i].ShiftX; if(y+shift+this.m_data_pivot_point[i].ShiftY<0) y=-shift-this.m_data_pivot_point[i].ShiftY; if(y+shift-this.m_data_pivot_point[i].ShiftY>chart_height) y=chart_height-shift+this.m_data_pivot_point[i].ShiftY; //--- set the calculated coordinates to the current object pivot point ext.ChangeCoordsExtendedObj(x+shift-this.m_data_pivot_point[i].ShiftX,y+shift-this.m_data_pivot_point[i].ShiftY,i); } } } } } } } } //--- 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 extended graphical object pivot point control form, if(graph_obj_id>WRONG_VALUE) { //--- get the object by the object and chart IDs CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID()); if(graph_obj!=NULL) { //--- Get the toolkit of the extended standard graphical object CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit(); if(toolkit!=NULL) { //--- Draw a point with a circle on the form and delete it on all other forms toolkit.DrawOneControlPoint(form); } } } } //--- '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 extended graphical object pivot point control form if(graph_obj_id>WRONG_VALUE) { //--- get the object by the object and chart IDs CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID()); if(graph_obj!=NULL) { //--- Get the toolkit of the extended standard graphical object CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit(); if(toolkit!=NULL) { //--- Draw a point with a circle on the form and delete it on all other forms toolkit.DrawOneControlPoint(form); } } } } //--- '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) { } } } } } //+------------------------------------------------------------------+
Além do novo manipulador para mover a forma de controle central, também adicionamos uma chamada do método que desenha um ponto no objeto-forma sob o cursor e apaga esses pontos em outras formas de dado objeto gráfico. Isso evitará o desenho simultâneo de pontos em vários objetos-formas se eles estiverem próximos e se sobreporem, como mostrado acima.
No momento, estamos prontos para testar a nova funcionalidade.
Teste
Para elaborar o teste, vamos pegar o Expert Advisor do artigo anterior e salvá-lo em uma nova pasta \MQL5\Experts\TestDoEasy\Part99 \ com o novo nome TestDoEasyPart99.mq5.
Não precisaremos fazer nenhuma alteração no próprio EA, já que todas as alterações são feitas apenas nas classes da biblioteca.
Compilamos o Expert Advisor e o executamos no gráfico:
Como se pode ver, se movermos o objeto gráfico composto construído assim como foi criado, todas as restrições para sair do gráfico quanto aos seus pontos de ancoragem funcionam corretamente. Mas vale a pena "virar" a localização dos pontos de ancoragem do objeto gráfico em relação à sua localização original, pois quando o ponto de ancoragem ultrapassa o gráfico, sua "configuração" começa a ser distorcida. Isso nos fala sobre o cálculo incorreto de restrições e depende de qual ponto de ancoragem ultrapassa a borda direita, esquerda, superior ou inferior do gráfico.
E isso não é surpreendente, pois os deslocamentos dos pontos de ancoragem são calculados em relação ao central. Isso significa que o ponto terá um deslocamento positivo e o segundo terá um negativo. Ao alterar a localização dos pontos de ancoragem em relação ao ponto central, temos um erro de cálculo de restrições. Vamos corrigir isso no próximo artigo.
O que virá a seguir?
No próximo artigo, continuaremos trabalhando em objetos gráficos compostos e suas funcionalidades.
*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
Gráficos na biblioteca DoEasy (Parte 98): Movendo pontos de ancoragem de objetos gráficos padrão estendidos
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/10584
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso