Interfaces Gráficas X: O Controle Gráfico Padrão (build 4)
Conteúdo
- Introdução
- Desenvolvimento de uma Classe para a Criação do Controle Gráfico Padrão
- Aplicação para testar o controle
- Otimização do timer e do manipulador de eventos do motor da biblioteca
- Otimização dos controles Lista Hierárquica e Navegador de Arquivos
- Novos ícones para as pastas e arquivos no navegador de arquivos.
- Conclusão
Introdução
O primeiro artigo Interfaces gráficas I: Preparação da Estrutura da Biblioteca (Capítulo 1) considera em detalhes a finalidade desta biblioteca. Você irã encontrar uma lista de artigos com os links no final de cada capítulo. Lá, você também pode encontrar e baixar a versão completa da biblioteca, no estágio de desenvolvimento atual. Os arquivos devem estar localizados nas mesmas pastas que o arquivo baixado.
Vamos considerar um outro controle, que não poderia ficar de fora da biblioteca em desenvolvimento. Quando o usuário inicia o terminal de negociação, aparecem os gráficos de preços. Seria conveniente ter uma ferramenta que permitisse gerenciar os gráficos com maior facilidade. No artigo anterior chamado Guia Prático do MQL5: Monitoramento de Múltiplos Períodos de Tempo em uma Única Janela foi demonstrado uma das possíveis variantes de tal ferramenta. Desta vez, nós vamos escrever uma classe para a criação de um controle, que será simples e fácil de utilizar nas interfaces gráficas das aplicações personalizadas em MQL. Ao contrário da versão anterior fornecida no link acima, esta aplicação permitirá percorrer horizontalmente o conteúdo dos objetos gráficos, como na janela principal do gráfico convencional.
Além disso, nós continuaremos a otimizar o código da biblioteca para reduzir o consumo de recursos do CPU. Mais detalhes serão fornecidos no decorrer do artigo.
Desenvolvimento de uma Classe para a Criação do Controle Gráfico Padrão
Antes de iniciar o desenvolvimento da classe CStandardChart para criar o controle gráfico padrão, a classe base CSubChartcom as propriedades adicionais (veja o código abaixo) devem ser adicionadas ao arquivo Object.mqh. Isto foi feito anteriormente para todos os tipos de objetos gráficos, que são utilizados durante a criação da biblioteca de controles. A classe base para a classe CSubChart é uma classe da biblioteca padrão - CChartObjectSubChart.
//+------------------------------------------------------------------+ //| Objects.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ ... //--- Lista de classes no arquivo para uma navegação rápida (Alt+G) ... class CSubChart; //+------------------------------------------------------------------+ //| Classe com propriedades adicionais para o objeto Subchart | //+------------------------------------------------------------------+ class CSubChart : public CChartObjectSubChart { protected: int m_x; int m_y; int m_x2; int m_y2; int m_x_gap; int m_y_gap; int m_x_size; int m_y_size; bool m_mouse_focus; //--- public: CSubChart(void); ~CSubChart(void); //--- Coordenadas int X(void) { return(m_x); } void X(const int x) { m_x=x; } int Y(void) { return(m_y); } void Y(const int y) { m_y=y; } int X2(void) { return(m_x+m_x_size); } int Y2(void) { return(m_y+m_y_size); } //--- Margens do ponto da margem (XY) int XGap(void) { return(m_x_gap); } void XGap(const int x_gap) { m_x_gap=x_gap; } int YGap(void) { return(m_y_gap); } void YGap(const int y_gap) { m_y_gap=y_gap; } //--- Tamanho int XSize(void) { return(m_x_size); } void XSize(const int x_size) { m_x_size=x_size; } int YSize(void) { return(m_y_size); } void YSize(const int y_size) { m_y_size=y_size; } //--- Foco bool MouseFocus(void) { return(m_mouse_focus); } void MouseFocus(const bool focus) { m_mouse_focus=focus; } }; //+------------------------------------------------------------------+ //| Construtor | //+------------------------------------------------------------------+ CSubChart::CSubChart(void) : m_x(0), m_y(0), m_x2(0), m_y2(0), m_x_gap(0), m_y_gap(0), m_x_size(0), m_y_size(0), m_mouse_focus(false) { } //+------------------------------------------------------------------+ //| Destrutor | //+------------------------------------------------------------------+ CSubChart::~CSubChart(void) { }
A classe CChartObjectSubChart contém o método para a criação de um objeto gráfico, bem como os métodos para modificar as propriedades do gráfico que são utilizadas com maior frequência. A lista inclui os métodos para definir e obter tais propriedades como:
- coordenadas e dimensões;
- símbolo, tempo gráfico e escala;
- exibição do preço e das escalas de tempo.
//+------------------------------------------------------------------+ //| ChartObjectSubChart.mqh | //| Copyright 2009-2013, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "ChartObject.mqh" //+------------------------------------------------------------------+ //| Classe CChartObjectSubChart. | //| Objetivo: Classe do objeto gráfico "SubChart". | //| Deriva da classe CChartObject. | //+------------------------------------------------------------------+ class CChartObjectSubChart : public CChartObject { public: CChartObjectSubChart(void); ~CChartObjectSubChart(void); //--- métodos de acesso as propriedades do objeto int X_Distance(void) const; bool X_Distance(const int X) const; int Y_Distance(void) const; bool Y_Distance(const int Y) const; ENUM_BASE_CORNER Corner(void) const; bool Corner(const ENUM_BASE_CORNER corner) const; int X_Size(void) const; bool X_Size(const int size) const; int Y_Size(void) const; bool Y_Size(const int size) const; string Symbol(void) const; bool Symbol(const string symbol) const; int Period(void) const; bool Period(const int period) const; int Scale(void) const; bool Scale(const int scale) const; bool DateScale(void) const; bool DateScale(const bool scale) const; bool PriceScale(void) const; bool PriceScale(const bool scale) const; //--- a alteração das coordenadas de tempo/preço está bloqueada bool Time(const datetime time) const { return(false); } bool Price(const double price) const { return(false); } //--- método de criação do objeto bool Create(long chart_id,const string name,const int window, const int X,const int Y,const int sizeX,const int sizeY); //--- método para identificar o objeto virtual int Type(void) const { return(OBJ_CHART); } //--- métodos para trabalhar com arquivos virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); };
Agora nós podemos criar o arquivo StandardChart.mqh com o a classe CStandardChart, onde os métodos padrão de todos os controles da biblioteca podem ser especificados no conteúdo base, conforme mostrado no código abaixo. Já que o controle contará com o modo de deslocamento horizontal, será necessário um ícone para o cursor do mouse informar ao usuário que o modo de deslocamento está ativado e os dados do objeto gráfico serão deslocados conforme o cursor do mouse é movido horizontalmente. Para alterar o ícone, inclua o arquivo Pointer.mqh contendo a classe CPointer, considerada anteriormente no artigo Interfaces Gráficas VIII: O Controle Lista Hierárquica (Capítulo 2). Para o ícone do ponteiro do mouse, nós vamos usar uma cópia daquele que está ativado com o deslocamento horizontal do gráfico principal (seta dupla preta com um contorno branco). Duas versões deste ícone (setas preta e azul) serão anexadas no final do artigo.
Por conseguinte, a enumeração dos ponteiros do mouse (ENUM_MOUSE_POINTER) foi suplementada com outro identificador (MP_X_SCROLL):
//+------------------------------------------------------------------+ //| Enumeração dos tipos de ponteiros | //+------------------------------------------------------------------+ enum ENUM_MOUSE_POINTER { MP_CUSTOM =0, MP_X_RESIZE =1, MP_Y_RESIZE =2, MP_XY1_RESIZE =3, MP_XY2_RESIZE =4, MP_X_SCROLL =5 };
Além disso, é necessário incluir os ícones como recursos para este tipo de cursor no arquivo Pointer.mqh, e uma construção switch no método CPointer::SetPointerBmp() deve ser expandido com outro bloco do tipo case:
//+------------------------------------------------------------------+ //| Pointer.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "Element.mqh" //--- Recursos ... #resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll.bmp" #resource "\\Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll_blue.bmp" //+------------------------------------------------------------------+ //| Define as imagens do cursor com base no tipo do cursor | //+------------------------------------------------------------------+ void CPointer::SetPointerBmp(void) { switch(m_type) { case MP_X_RESIZE : m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs_blue.bmp"; m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_rs.bmp"; break; case MP_Y_RESIZE : m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs_blue.bmp"; m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_y_rs.bmp"; break; case MP_XY1_RESIZE : m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_xy1_rs_blue.bmp"; m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_xy1_rs.bmp"; break; case MP_XY2_RESIZE : m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_xy2_rs_blue.bmp"; m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_xy2_rs.bmp"; break; case MP_X_SCROLL : m_file_on ="Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll_blue.bmp"; m_file_off ="Images\\EasyAndFastGUI\\Controls\\pointer_x_scroll.bmp"; break; } //--- Se o tipo personalizado (MP_CUSTOM) foi especificado if(m_file_on=="" || m_file_off=="") ::Print(__FUNCTION__," > Ambas as imagens devem ser definidas para o cursor!"); }
Deve-se notar também que o método Moving() também pode ser utilizado em dois modos, podendo ser definidos pelo terceiro argumento do método. O valor padrão desse argumento é false, significando que o controle pode ser movido apenas no caso do formulário, que está anexado a ele, também se encontrar no modo de movimento.
//+------------------------------------------------------------------+ //| StandardChart.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include "Element.mqh" #include "Window.mqh" #include "Pointer.mqh" //+------------------------------------------------------------------+ //| Classe para a criação do gráfico padrão | //+------------------------------------------------------------------+ class CStandardChart : public CElement { private: //--- Ponteiro para o formulário que este elemento está anexado CWindow *m_wnd; //--- public: //--- Armazena o ponteiro do formulário void WindowPointer(CWindow &object) { m_wnd=::GetPointer(object); } //--- public: //--- Manipulador de eventos do gráfico virtual void OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam); //--- Move o elemento virtual void Moving(const int x,const int y,const bool moving_mode=false); //--- (1) Exibe, (2) oculta, (3) reseta, (4) remove virtual void Show(void); virtual void Hide(void); virtual void Reset(void); virtual void Delete(void); //--- (1) Definir (2), resetar as prioridades para o clique esquerdo do mouse virtual void SetZorders(void); virtual void ResetZorders(void); //--- private: //--- Altera a largura da margem direita da janela virtual void ChangeWidthByRightWindowSide(void); //--- Altera a altura na borda inferior da janela virtual void ChangeHeightByBottomWindowSide(void); }; //+------------------------------------------------------------------+ //| Construtor | //+------------------------------------------------------------------+ CStandardChart::CStandardChart(void) { //--- Armazena o nome da classe do elemento na classe base CElement::ClassName(CLASS_NAME); } //+------------------------------------------------------------------+ //| Destrutor | //+------------------------------------------------------------------+ CStandardChart::~CStandardChart(void) { }
Se o valor do terceiro argumento no método Moving() for definido como true, então o controle será movido de forma forçada após a chamada do método, independentemente do fato do formulário estar no modo de movimento. Em alguns casos, isto reduz significativamente o consumo de recursos do CPU.
Para verificar se o formulário está no modo de movimento, foi adicionado o método ClampingAreaMouse() à classe CWindow. Ela retorna a área onde o botão esquerdo do mouse foi pressionado:
//+------------------------------------------------------------------+ //| Classe para criar um formulário de controles | //+------------------------------------------------------------------+ class CWindow : public CElement { public: //--- Retorna a área onde o botão esquerdo do mouse foi pressionado ENUM_MOUSE_STATE ClampingAreaMouse(void) const { return(m_clamping_area_mouse); } };
O método CWindow::ClampingAreaMouse() só pode ser acessado através do ponteiro do formulário em cada controle anexado à ele. Por tudo funcionar como foi descrito acima, um bloco de código deve ser inserido ao método Moving() de cada controle, como é mostrado abaixo (veja o destaque em amarelo). Como um exemplo, é exibido o método da classe CSimpleButton (Versão reduzida).
//+------------------------------------------------------------------+ //| Deslocamento dos controles | //+------------------------------------------------------------------+ void CSimpleButton::Moving(const int x,const int y,const bool moving_mode=false) { //--- Sai se o elemento está oculto if(!CElement::IsVisible()) return; //--- Se a gestão é delegada para a janela, então, identifica a sua localização if(!moving_mode) if(m_wnd.ClampingAreaMouse()!=PRESSED_INSIDE_HEADER) return; //--- Se está ancorado à direita //--- Se está ancorado à esquerda //--- Se está ancorado no canto inferior //--- Se está ancorado no canto superior //--- Atualizando as coordenadas dos objetos gráficos m_button.X_Distance(m_button.X()); m_button.Y_Distance(m_button.Y()); }
Um exemplo de uso do método Moving() poderá ser visto abaixo, que demonstra o código do método CSimpleButton::Show(). Neste caso, as coordenadas do controle deveriam ser atualizadas à força, portanto, nós teremos como terceiro argumento o valou igual a true. As modificações apropriadas foram feitas em todas as classes da biblioteca, que utilizam o método Moving().
//+------------------------------------------------------------------+ //| Exibe o botão | //+------------------------------------------------------------------+ void CSimpleButton::Show(void) { //--- Retorna, se o elemento já for visível if(CElement::IsVisible()) return; //--- Faz com que todos os objetos sejam visíveis m_button.Timeframes(OBJ_ALL_PERIODS); //--- Estado de visibilidade CElement::IsVisible(true); //--- Atualiza a posição dos objetos Moving(m_wnd.X(),m_wnd.Y(),true); }
A otimização da biblioteca desenvolvida será discutida mais adiante neste artigo, por enquanto, consideraremos apenas o controle gráfico padrão.
Vamos tornar possível a criação de um array de objetos gráficos colocados em uma linha. Para isso, é necessário declarar arrays dinâmicos para os objetos que representam os gráficos, bem como para certas propriedades, tais como: (1) Identificador do gráfico, (2) símbolo e (3) tempo gráfico. Antes de criar o controle Gráfico Padrão, é necessário usar o método CStandardChart::AddSubChart(), onde o símbolo e o tempo do gráfico devem ser passados. No início deste método, antes de adicionar os elementos aos arrays e inicializá-los com os valores passados, deve-se fazer uma verificação de sua disponibilidade do símbolo usando o método CStandardChart::CheckSymbol().
class CStandardChart : public CElement { private: //--- Objetos para criar o controle CSubChart m_sub_chart[]; //--- Propriedades do gráfico: long m_sub_chart_id[]; string m_sub_chart_symbol[]; ENUM_TIMEFRAMES m_sub_chart_tf[]; //--- public: //--- Adiciona um gráfico com as propriedades especificadas antes de sua criação void AddSubChart(const string symbol,const ENUM_TIMEFRAMES tf); //--- private: //--- Verificação do símbolo bool CheckSymbol(const string symbol); };
O método CStandardChart::CheckSymbol() verifica primeiro se o símbolo especificado está disponível na janela do Market Watch. Se o símbolo não for encontrado, ele tenta localizar o símbolo na lista geral. Se o símbolo for encontrado, então, ele deve ser adicionado ao Market Watch. Caso contrário, será impossível criar um objeto gráfico com este símbolo (será criado em seu lugar um objeto gráfico com o símbolo da janela principal do gráfico).
Se bem sucedido, o método CStandardChart::CheckSymbol() retorna true. Se o símbolo especificado não for encontrado, o método irá retornar false e o objeto gráfico não será adicionado (os arrays terão o mesmo tamanho), uma mensagem sobre isso será exibido no registro.
//+------------------------------------------------------------------+ //| Adiciona um gráfico | //+------------------------------------------------------------------+ void CStandardChart::AddSubChart(const string symbol,const ENUM_TIMEFRAMES tf) { //--- Verifica se o símbolo está disponível no servidor if(!CheckSymbol(symbol)) { ::Print(__FUNCTION__," > O símbolo "+symbol+" não está disponível no servidor!"); return; } //--- Aumenta o tamanho do array por um elemento int array_size=::ArraySize(m_sub_chart); int new_size=array_size+1; ::ArrayResize(m_sub_chart,new_size); ::ArrayResize(m_sub_chart_id,new_size); ::ArrayResize(m_sub_chart_symbol,new_size); ::ArrayResize(m_sub_chart_tf,new_size); //--- Armazenar o valor dos parâmetros passados m_sub_chart_symbol[array_size] =symbol; m_sub_chart_tf[array_size] =tf; } //+------------------------------------------------------------------+ //| Verifica a disponibilidade do símbolo | //+------------------------------------------------------------------+ bool CStandardChart::CheckSymbol(const string symbol) { bool flag=false; //--- Verifica se o símbolo está no Market Watch int symbols_total=::SymbolsTotal(true); for(int i=0; i<symbols_total; i++) { //--- Se o símbolo estiver disponível, interrompe o loop if(::SymbolName(i,true)==symbol) { flag=true; break; } } //--- Se o símbolo não está disponível na janela do Market Watch if(!flag) { //--- ... tenta encontrá-lo na lista geral symbols_total=::SymbolsTotal(false); for(int i=0; i<symbols_total; i++) { //--- Se este símbolo está disponível if(::SymbolName(i,false)==symbol) { //--- ... adiciona ele a janela do Market Watch e interrompe o loop ::SymbolSelect(symbol,true); flag=true; break; } } } //--- Retorna os resultados da pesquisa return(flag); }
A criação do controle gráfico padrão irá exigir três métodos: um método público que será principal e dois privados, sendo que um deles se refere ao ícone para o cursor do mouse no modo de deslocamento horizontal. O método público CStandardChart::SubChartsTotal() foi adicionado à classe como um método auxiliar para recuperar o número de objetos gráficos.
class CStandardChart : public CElement { private: //--- Objetos para criar o controle CSubChart m_sub_chart[]; CPointer m_x_scroll; //--- public: //--- Métodos para a criação de um gráfico padrão bool CreateStandardChart(const long chart_id,const int subwin,const int x,const int y); //--- private: bool CreateSubChart(void); bool CreateXScrollPointer(void); //--- public: //--- Retorna o tamanho do array de gráficos int SubChartsTotal(void) const { return(::ArraySize(m_sub_chart)); } };
Vamos considerar o método CStandardChart::CreateSubCharts() para a criação dos objetos gráficos. Logo no início deste método, nós encontramos uma verificação do número de gráficos adicionados ao array antes da criação do controle. Se não foi adicionado nenhum, o programa simplesmente deixará o método e mostrará uma mensagem relevante no registro.
Se os gráficos foram adicionados, então, a largura é calculada para cada objeto. A largura total de um controle deve ser definida pelo usuário na classe personalizada da aplicação MQL, que foi desenvolvida. Caso seja necessário criar vários gráficos, basta dividir a largura total do controle pelo número de gráficos, para obter a largura de cada objeto.
Em seguida, os objetos são criados em um loop. Isso leva em conta o posicionamento do controle (pontos de ancoragem para um dos lados do formulário). Este tópico tem sido exaustivamente descrito no artigo anterior, por isso ele não será discutido aqui.
Depois de criar o objeto gráfico, o identificador do gráfico criado é obtido e armazenado no array, as suas propriedades são ajustadas e armazenadas.
//+------------------------------------------------------------------+ //| Cria os gráficos | //+------------------------------------------------------------------+ bool CStandardChart::CreateSubCharts(void) { //--- Obtém o número de gráficos int sub_charts_total=SubChartsTotal(); //--- Se não houver um gráfico no grupo, relata if(sub_charts_total<1) { ::Print(__FUNCTION__," > Este método era para ser chamado, " "se um grupo conter pelo menos um gráfico! Use o método CStandardChart::AddSubChart()"); return(false); } //--- Calcula as coordenadas e o tamanho int x=m_x; int x_size=(sub_charts_total>1)? m_x_size/sub_charts_total : m_x_size; //--- Cria o número especificado de gráficos for(int i=0; i<sub_charts_total; i++) { //--- Elaborando o nome do objeto string name=CElement::ProgramName()+"_sub_chart_"+(string)i+"__"+(string)CElement::Id(); //--- Cálculo da coordenada X x=(i>0)?(m_anchor_right_window_side)? x-x_size+1 : x+x_size-1 : x; //--- Ajusta a largura do último gráfico if(i+1>=sub_charts_total) x_size=m_x_size-(x_size*(sub_charts_total-1)-(sub_charts_total-1)); //--- Configura um botão if(!m_sub_chart[i].Create(m_chart_id,name,m_subwin,x,m_y,x_size,m_y_size)) return(false); //--- Obtém e armazena o identificador do gráfico criado m_sub_chart_id[i]=m_sub_chart[i].GetInteger(OBJPROP_CHART_ID); //--- Define as propriedades m_sub_chart[i].Symbol(m_sub_chart_symbol[i]); m_sub_chart[i].Period(m_sub_chart_tf[i]); m_sub_chart[i].Z_Order(m_zorder); m_sub_chart[i].Tooltip("\n"); //--- Armazena o tamanho m_sub_chart[i].XSize(x_size); m_sub_chart[i].YSize(m_y_size); //--- Margens da borda m_sub_chart[i].XGap((m_anchor_right_window_side)? x : x-m_wnd.X()); m_sub_chart[i].YGap((m_anchor_bottom_window_side)? m_y : m_y-m_wnd.Y()); //--- Armazena o ponteiro de objeto CElement::AddToArray(m_sub_chart[i]); } //--- return(true); }
Após a sua criação é possível alterar qualquer propriedade dos objetos gráficos contidos no controle Gráfico Padrão. Isso é feito por meio do ponteiro que pode ser obtido com a ajuda do método CStandardChart::GetSubChartPointer(). Se um índice incorreto for passado por acidente, ele será corrigido a fim de impedir que exceda o tamanho do array.
class CStandardChart : public CElement { public: //--- Retorna o ponteiro para o objeto gráfico através do índice especificado CSubChart *GetSubChartPointer(const uint index); }; //+------------------------------------------------------------------+ //| Retorna o ponteiro para o gráfico através do índice especificado | //+------------------------------------------------------------------+ CSubChart *CStandardChart::GetSubChartPointer(const uint index) { uint array_size=::ArraySize(m_sub_chart); //--- Se não houver nenhum gráfico, relata if(array_size<1) { ::Print(__FUNCTION__," > Este método era para ser chamado, " "se um grupo conter pelo menos um gráfico!); } //--- Ajuste no caso do tamanho exceder uint i=(index>=array_size)? array_size-1 : index; //--- Retorna o ponteiro return(::GetPointer(m_sub_chart[i])); }
Um ícone para o cursor do mouse é criado apenas se está habilitado o modo de deslocamento horizontal para os dados nos objetos gráficos. Ele deve ser ativado antes da criação do controle, usando o método CStandardChart::XScrollMode().
class CStandardChart : public CElement { private: //--- Modo de deslocamento horizontal bool m_x_scroll_mode; //--- public: //--- Modo de deslocamento horizontal void XScrollMode(const bool mode) { m_x_scroll_mode=mode; } }; //+------------------------------------------------------------------+ //| Cria o cursor de deslocamento horizontal | //+------------------------------------------------------------------+ bool CStandardChart::CreateXScrollPointer(void) { //--- Sai, se o deslocamento horizontal não for necessário if(!m_x_scroll_mode) return(true); //--- Define as propriedades m_x_scroll.XGap(0); m_x_scroll.YGap(-20); m_x_scroll.Id(CElement::Id()); m_x_scroll.Type(MP_X_SCROLL); //--- Criação de um elemento if(!m_x_scroll.CreatePointer(m_chart_id,m_subwin)) return(false); //--- return(true); }
Para resumir o que foi exposto acima. Se o modo de deslocamento horizontal está habilitado, a sua operação utiliza o método CStandardChart::HorizontalScroll(), que será chamado no manipulador de eventos do controle quando o evento CHARTEVENT_MOUSE_MOVE é disparado. No caso do botão esquerdo do mouse ser pressionado, dependendo do que foi pressionado ou se forem chamadas repetidas para o método, durante o processo de deslocamento horizontal, o método calcula a distância percorrida pelo cursor do mouse em pixels a partir do ponto que houve o pressionamento. Aqui:
- O formulário está bloqueado.
- As coordenadas para o ícone do cursor do mouse são calculadas.
- O ícone é mostrado.
O valor calculado para o deslocamento de dados nos objetos gráficos pode ser negativo, como o deslocamento irá ser realizado em relação à última barra - o método ::ChartNavigate() com o valor CHART_END (o segundo argumento) da enumeração ENUM_CHART_POSITION. Se o valor de deslocamento for positivo, o programa deixa o método. Se é passado na verificação, então, o valor atual do deslocamento é armazenado para a iteração seguinte. Em seguida, o "Deslocamento Automático" (CHART_AUTOSCROLL) e o "Deslocar o final do gráfico a partir da borda direita" (CHART_SHIFT) são desativados para todos os objetos gráficos sendo que o deslocamento é realizado de acordo com o valor calculado.
Se o botão esquerdo do mouse for liberado, o formulário será desbloqueado, e o ícone do cursor do mouse, que indica o processo de deslocamento horizontal será ocultado. Depois disso, o programa sairá do método.
class CStandardChart : public CElement { private: //--- As variáveis relacionadas ao deslocamento horizontal do gráfico int m_prev_x; int m_new_x_point; int m_prev_new_x_point; //--- private: //--- Deslocamento horizontal void HorizontalScroll(void); }; //+------------------------------------------------------------------+ //| Deslocamento horizontal do gráfico | //+------------------------------------------------------------------+ void CStandardChart::HorizontalScroll(void) { //--- Sai, se o deslocamento horizontal dos gráficos está desativado if(!m_x_scroll_mode) return; //--- Se o botão do mouse foi pressionado if(m_mouse.LeftButtonState()) { //--- Armazena as coordenadas X atuais do cursor if(m_prev_x==0) { m_prev_x =m_mouse.X()+m_prev_new_x_point; m_new_x_point =m_prev_new_x_point; } else m_new_x_point=m_prev_x-m_mouse.X(); //--- Bloqueia o formulário if(!m_wnd.IsLocked()) { m_wnd.IsLocked(true); m_wnd.IdActivatedElement(CElement::Id()); } //--- Atualiza as coordenadas do ponteiro e torná-o visível int l_x=m_mouse.X()-m_x_scroll.XGap(); int l_y=m_mouse.Y()-m_x_scroll.YGap(); m_x_scroll.Moving(l_x,l_y); //--- Mostra o ponteiro m_x_scroll.Show(); //--- Define a visibilidade do sinalizador m_x_scroll.IsVisible(true); } else { m_prev_x=0; //--- Desbloqueia o formulário if(m_wnd.IdActivatedElement()==CElement::Id()) { m_wnd.IsLocked(false); m_wnd.IdActivatedElement(WRONG_VALUE); } //--- Oculta o ponteiro m_x_scroll.Hide(); //--- Define a visibilidade do sinalizador m_x_scroll.IsVisible(false); return; } //--- Sai, se houver um valor positivo if(m_new_x_point>0) return; //--- Armazena a posição atual m_prev_new_x_point=m_new_x_point; //--- Aplica à todos os gráficos int symbols_total=SubChartsTotal(); //--- Desativa o deslocamento automáticos e o deslocamento a partir da borda direita for(int i=0; i<symbols_total; i++) { if(::ChartGetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL)) ::ChartSetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL,false); if(::ChartGetInteger(m_sub_chart_id[i],CHART_SHIFT)) ::ChartSetInteger(m_sub_chart_id[i],CHART_SHIFT,false); } //--- Reseta o último erro ::ResetLastError(); //--- Desloca os gráficos for(int i=0; i<symbols_total; i++) if(!::ChartNavigate(m_sub_chart_id[i],CHART_END,m_new_x_point)) ::Print(__FUNCTION__," > erro: ",::GetLastError()); }
O método CStandardChart::ZeroHorizontalScrollVariables() será usado para repor as variáveis auxiliares do modo de deslocamento horizontal para os dados dos objetos gráficos. É possível também que seja necessário ir até a última barra mediante programação. Para isto, o método público CStandardChart::ResetCharts() é usado.
class CStandardChart : public CElement { public: //--- Redefine os gráficos void ResetCharts(void); //--- private: //--- Reseta as variáveis de deslocamento horizontal void ZeroHorizontalScrollVariables(void); }; //+------------------------------------------------------------------+ //| Reseta os gráficos | //+------------------------------------------------------------------+ void CStandardChart::ResetCharts(void) { int sub_charts_total=SubChartsTotal(); for(int i=0; i<sub_charts_total; i++) ::ChartNavigate(m_sub_chart_id[i],CHART_END); //--- Reseta as variáveis auxiliares para o deslocamento horizontal dos gráficos ZeroHorizontalScrollVariables(); } //+------------------------------------------------------------------+ //| Reseta as variáveis de deslocamento horizontal | //+------------------------------------------------------------------+ void CStandardChart::ZeroHorizontalScrollVariables(void) { m_prev_x =0; m_new_x_point =0; m_prev_new_x_point =0; }
Também pode ser necessário monitorar o evento de pressionamento do botão esquerdo do mouse sobre um objeto gráfico do controle "Gráfico Padrão". Então, adicione o novo identificador ON_CLICK_SUB_CHART para o arquivo Defines.mqh:
//+------------------------------------------------------------------+ //| Defines.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ ... //--- Identificadores de eventos ... #define ON_CLICK_SUB_CHART (28) // Ao clicar no objeto gráfico
Para determinar o clique no objeto gráfico, implemente o método CStandardChart::OnClickSubChart(). Se as verificações do nome e do identificador forem bem sucedidos (veja abaixo), então ele gera uma mensagem com o (1) identificador do evento ON_CLICK_SUB_CHART, (2) identificador do controle, (3) o índice do objeto gráfico e (4) o nome do símbolo.
class CStandardChart : public CElement { private: //--- Manipulando o pressionamento do objeto gráfico bool OnClickSubChart(const string clicked_object); }; //+------------------------------------------------------------------+ //| Manipulando o pressionamento de um botão | //+------------------------------------------------------------------+ bool CStandardChart::OnClickSubChart(const string clicked_object) { //--- Sai, se o pressionamento não foi no elemento de menu if(::StringFind(clicked_object,CElement::ProgramName()+"_sub_chart_",0)<0) return(false); //--- Obtém o identificador e o índice a partir do nome do objeto int id=CElement::IdFromObjectName(clicked_object); //--- Retorna, se o tipo definido não corresponder if(id!=CElement::Id()) return(false); //--- Obtém o índice int group_index=CElement::IndexFromObjectName(clicked_object); //--- Envia um sinal sobre ele ::EventChartCustom(m_chart_id,ON_CLICK_SUB_CHART,CElement::Id(),group_index,m_sub_chart_symbol[group_index]); return(true); }
Suponha que você precise de outra maneira de navegar nos objetos gráficos, semelhante à forma como ele foi implementado ao gráfico principal por meio do terminal. Se a tecla «Space» ou «Enter» for pressionada no terminal MetaTrader, uma caixa de edição é ativada no canto inferior esquerdo do gráfico (veja a imagem abaixo). Este é um tipo de linha de comando, onde uma data poderá ser inserida, a fim de saltar para essa data no gráfico. Essa linha de comando também pode ser usada para alterar o símbolo e o tempo gráfico do gráfico.
Fig. 1. Linha de comando do gráfico no canto esquerdo.
A propósito, um novo recurso para o gerenciamento da linha de comando foi adicionado na última atualização do terminal de negociação (build 1455).
…
8. MQL5: A nova propriedade CHART_QUICK_NAVIGATION permite ativar/desativar a barra de navegação rápida no gráfico. Se você precisar modificar e acessar o estado de propriedade, use as funções ChartSetInteger e ChartGetInteger.
A barra de navegação é aberta pressionando Enter ou Space. Ela permite que você se mova rapidamente até a data especificada no gráfico, bem como para alterar o símbolo e o tempo gráfico. Se os seu programa em MQL5 processa o pressionamento das teclas Enter ou Space, desative a propriedade CHART_QUICK_NAVIGATION, para evitar a interceptação destes eventos pelo terminal. A barra de navegação rápida ainda pode ser aberta por um duplo clique.
…
Dentro da interface gráfica, tudo pode ser feito de forma mais fácil e cômoda. A biblioteca Easy And Fast já contém o controle Calendário (a classe CCalendar). A navegação do gráfico e do objeto gráfico principal pode ser implementada simplesmente escolhendo uma data no calendário. Vamos simplificar tudo para um único método com um argumento. O valor deste argumento será a data em que o gráfico precisa ser deslocado. Este método será chamado de CStandardChart::SubChartNavigate(), o código a seguir mostra a versão atual deste método.
Os modos "Deslocamento Automático" e "Deslocar o final do gráfico a partir da borda direita" do gráfico principal estão desativados no início do método. Então, se a variável passada para o método for maior do que a do início do dia atual, simplesmente vá para a última barra e deixe o método. Se a data for menor, em seguida, é necessário calcular o número de barras para o deslocamento à esquerda. Primeiro, o cálculo é realizado para o gráfico principal:
- Obtenha o número total de barras disponíveis para o símbolo atual e o tempo gráfico desde o início do dia atual até a data especificada.
- Obtenha o número de barras visíveis no gráfico.
- Obtenha o número de barras a partir do início do dia atual + duas barras como um recuo adicional.
- Calcule o número de barras para o deslocamento a partir da última barra.
Depois disso, o gráfico principal é deslocado para a esquerda, e tudo é repetido para os objetos gráficos.
class CStandardChart : public CElement { public: //--- Pula para a data especificada void SubChartNavigate(const datetime date); }; //+------------------------------------------------------------------+ //| Pula para a data especificada | //+------------------------------------------------------------------+ void CStandardChart::SubChartNavigate(const datetime date) { //--- (1) A data do gráfico atual e (2) a nova data selecionada no calendário datetime current_date =::StringToTime(::TimeToString(::TimeCurrent(),TIME_DATE)); datetime selected_date =date; //--- Desativa o deslocamento automáticos e o deslocamento a partir da borda direita ::ChartSetInteger(m_chart_id,CHART_AUTOSCROLL,false); ::ChartSetInteger(m_chart_id,CHART_SHIFT,false); //--- Se a data selecionada no calendário for maior do que a data atual if(selected_date>=current_date) { //--- Vai para a data atual em todos os gráficos ::ChartNavigate(m_chart_id,CHART_END); ResetCharts(); return; } //--- Obtém o número de barras a partir da data especificada int bars_total =::Bars(::Symbol(),::Period(),selected_date,current_date); int visible_bars =(int)::ChartGetInteger(m_chart_id,CHART_VISIBLE_BARS); long seconds_today =::TimeCurrent()-current_date; int bars_today =int(seconds_today/::PeriodSeconds())+2; //--- Define o avanço da margem direita para todos os gráficos m_prev_new_x_point=m_new_x_point=-((bars_total-visible_bars)+bars_today); ::ChartNavigate(m_chart_id,CHART_END,m_new_x_point); //--- int sub_charts_total=SubChartsTotal(); for(int i=0; i<sub_charts_total; i++) { //--- Desativa o deslocamento automáticos e o deslocamento a partir da borda direita ::ChartSetInteger(m_sub_chart_id[i],CHART_AUTOSCROLL,false); ::ChartSetInteger(m_sub_chart_id[i],CHART_SHIFT,false); //--- Obtém o número de barras a partir da data especificada bars_total =::Bars(m_sub_chart[i].Symbol(),(ENUM_TIMEFRAMES)m_sub_chart[i].Period(),selected_date,current_date); visible_bars =(int)::ChartGetInteger(m_sub_chart_id[i],CHART_VISIBLE_BARS); bars_today =int(seconds_today/::PeriodSeconds((ENUM_TIMEFRAMES)m_sub_chart[i].Period()))+2; //--- Recuo da margem direita do gráfico m_prev_new_x_point=m_new_x_point=-((bars_total-visible_bars)+bars_today); ::ChartNavigate(m_sub_chart_id[i],CHART_END,m_new_x_point); } }
O desenvolvimento da classe CStandardChart para a criação do controlo Gráfico Padrão foi concluída. Agora, vamos escrever um aplicação para ver como ela funciona.
Aplicação para testar o controle
Para fins de teste, você pode usar o Expert Advisor do artigo anterior. Remova todos os controles, exceto o menu principal, barra de estado e guias. Faça com que cada guia tenha um grupo separado de objetos gráficos. Cada grupo irá conter uma determinada moeda; portanto, cada guia terá uma descrição correspondente:
- A primeira guia - EUR (Euro).
- A segunda guia - GBP (Libra Esterlina).
- A terceira guia - AUD (Dólar Australiano).
- A quarta guia - CAD (Dólar Canadense).
- A quinta guia - JPY (Iene Japonês).
Os objetos gráficos estarão localizados estritamente na área de trabalho das guias e elas serão redimensionadas automaticamente sempre que o formulário for redimensionado. A borda direita da área de trabalho nas guias sempre terá um recuo de 173 pixels da borda direita do formulário. Este espaço será preenchido com os controles para definir essas propriedades como:
- Exibição da escala de tempo (Date time).
- Exibição da escala de preços (Price scale).
- Alteração do período do gráfico (Timeframes).
- Navegação dos dados do gráfico através do calendário.
Como um exemplo, basta mostrar o código para a criação de um único controle do Gráfico Padrão (CStandardChart). Lembre-se que o deslocamento horizontal dos gráficos é desativado por padrão, de modo que no caso de ser necessário, o método CStandardChart::XScrollMode() pode ser utilizado. O método CStandardChart::AddSubChart() é usado para a adição de gráficos ao grupo.
//+------------------------------------------------------------------+ //| Classe para a criação de uma aplicação | //+------------------------------------------------------------------+ class CProgram : public CWndEvents { protected: //--- Gráfico Padrão CStandardChart m_sub_chart1; //--- protected: //--- Gráfico Padrão bool CreateSubChart1(const int x_gap,const int y_gap); }; //+------------------------------------------------------------------+ //| Cria um gráfico padrão 1 | //+------------------------------------------------------------------+ bool CProgram::CreateSubChart1(const int x_gap,const int y_gap) { //--- Armazena o ponteiro da janela m_sub_chart1.WindowPointer(m_window); //--- Anexa à primeira guia m_tabs.AddToElementsArray(0,m_sub_chart1); //--- Coordenadas int x=m_window.X()+x_gap; int y=m_window.Y()+y_gap; //--- Define as propriedades antes da criação m_sub_chart1.XSize(600); m_sub_chart1.YSize(200); m_sub_chart1.AutoXResizeMode(true); m_sub_chart1.AutoYResizeMode(true); m_sub_chart1.AutoXResizeRightOffset(175); m_sub_chart1.AutoYResizeBottomOffset(25); m_sub_chart1.XScrollMode(true); //--- Adiciona os gráficos m_sub_chart1.AddSubChart("EURUSD",PERIOD_D1); m_sub_chart1.AddSubChart("EURGBP",PERIOD_D1); m_sub_chart1.AddSubChart("EURAUD",PERIOD_D1); //--- Cria o controle if(!m_sub_chart1.CreateStandardChart(m_chart_id,m_subwin,x,y)) return(false); //--- Adiciona o objeto ao array comum dos grupos de objetos CWndContainer::AddToElementsArray(0,m_sub_chart1); return(true); }
A imagem abaixo mostra o resultado final. Neste exemplo, os dados do objeto gráfico pode ser deslocado horizontalmente, de forma semelhante como ele foi implementado ao gráfico principal. Além disso, a navegação entre objetos gráficos funciona através do calendário, incluindo o avanço rápido das datas.
Fig. 2. Teste do controle Gráfico Padrão.
O aplicativo de teste apresentado no artigo pode ser baixado usando o link abaixo para estudá-lo ainda mais.
Otimização do timer e do manipulador de eventos do motor da biblioteca
Mais cedo, a biblioteca Easy And Fast só tinha sido testada no sistema operacional Windows 7 x64. Após a atualização para o Windows 10 x64, foi verificado que o uso do CPU aumenta significativamente. Os processos da biblioteca consumiam até 10% dos recursos da CPU mesmo no modo de repouso, quando não havia nenhuma interação com a interface gráfica. As imagens abaixo mostram o consumo dos recursos da CPU antes e depois de anexar a aplicação MQL de teste ao gráfico.
Fig. 3. Consumo dos recursos da CPU antes de anexar a aplicação MQL de teste ao gráfico.
Fig. 4. Consumo dos recursos da CPU após anexar a aplicação MQL de teste ao gráfico.
Descobriu-se que o problema estava no timer do motor da biblioteca, onde o gráfico é atualizado a cada 16ms, como é mostrado no código abaixo:
//+------------------------------------------------------------------+ //| Timer | //+------------------------------------------------------------------+ void CWndEvents::OnTimerEvent(void) { //--- Retorna, se o array está vazio if(CWndContainer::WindowsTotal()<1) return; //--- Verificação de eventos de todos os elementos pelo timer CheckElementsEventsTimer(); //--- Redesenha o gráfico m_chart.Redraw(); }
O consumo de CPU aumenta ainda mais, se somarmos os movimentos do cursor do mouse dentro da área do gráfico e a interação ativa com a interface gráfica da aplicação MQL. A tarefa é reduzir o funcionamento do timer do motor da biblioteca e eliminar o redesenho do gráfico cada vez que o evento movimento do mouse for acionado. Como isso pode ser feito?
Remova a linha responsável por redesenhar o gráfico (destacado em vermelho) do método CWndEvents::ChartEventMouseMove():
//+------------------------------------------------------------------+ //| Evento CHARTEVENT MOUSE MOVE | //+------------------------------------------------------------------+ void CWndEvents::ChartEventMouseMove(void) { //--- Sai, se este não é um evento do movimento do cursor if(m_id!=CHARTEVENT_MOUSE_MOVE) return; //--- Movendo a janela MovingWindow(); //--- Define o estado do gráfico SetChartState(); //--- Redesenha o gráfico m_chart.Redraw(); }
Como para o timer do motor da biblioteca, seu propósito atual se resume a mudança de cor dos controles quando eles estão abaixo do mouse e executam um avanço rápido de diferentes controles (listas, tabelas, calendário, etc.) Portanto, ele precisa ser atualizado frequentemente. Para economizar recursos, o timer será ativado quando o cursor do mouse começar a se mover, e logo que o movimento terminar, a sua operação será bloqueada após uma breve pausa.
A fim de implementar a ideia, é necessário fazer algumas adições à classe CMouse. As adições incluem um contador de chamadas para o timer do sistema e o CMouse::GapBetweenCalls(), que retorna a diferença entre as chamadas para os eventos de movimento do mouse.
class CMouse { private: //--- Contador de chamadas ulong m_call_counter; //--- public: //--- Retorna (1) o valor do contador armazenado durante a última chamada e (2) a diferença entre as chamadas para o manipulador de eventos de movimento do mouse ulong CallCounter(void) const { return(m_call_counter); } ulong GapBetweenCalls(void) const { return(::GetTickCount()-m_call_counter); } }; //+------------------------------------------------------------------+ //| Construtor | //+------------------------------------------------------------------+ CMouse::CMouse(void) : m_call_counter(::GetTickCount()) { }
A lógica é simples. Logo que o cursor do mouse começar a se mover, o processador de eventos da classe CMouse armazena o valor atual do timer do sistema:
//+------------------------------------------------------------------+ //| Manipular de eventos de movimento do cursor do mouse | //+------------------------------------------------------------------+ void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- Manipulação do evento do movimento do cursor if(id==CHARTEVENT_MOUSE_MOVE) { //--- Coordenadas e o estado do botão esquerdo do mouse m_x =(int)lparam; m_y =(int)dparam; m_left_button_state =(bool)int(sparam); //--- Armazena o valor do contador de chamadas m_call_counter=::GetTickCount(); //--- Obtém a localização do cursor if(!::ChartXYToTimePrice(0,m_x,m_y,m_subwin,m_time,m_level)) return; //--- Obtém a coordenada Y relativa if(m_subwin>0) m_y=m_y-m_chart.SubwindowY(m_subwin); } }
O timer do motor da biblioteca (a classe CWndEvents) deve conter uma condição: se o cursor do mouse não for movido por mais de 500ms, o gráfico não deve ser redesenhado. O botão esquerdo do mouse deve ser liberado naquele tempo, para evitar a situação em que a opção de avanço rápido para os controles funcione somente para 500ms.
//+------------------------------------------------------------------+ //| Timer | //+------------------------------------------------------------------+ void CWndEvents::OnTimerEvent(void) { //--- Sai, se o cursor do mouse está em repouso (diferença entre as chamadas for maior do que 500ms) e com o botão esquerdo do mouse é liberado if(m_mouse.GapBetweenCalls()>500 && !m_mouse.LeftButtonState()) return; //--- Retorna, se o array está vazio if(CWndContainer::WindowsTotal()<1) return; //--- Verificação de eventos de todos os elementos pelo timer CheckElementsEventsTimer(); //--- Redesenha o gráfico m_chart.Redraw(); }
Problema resolvido. Desabilitar o redesenho sempre que o cursor do mouse se mover não tem efeito sobre a qualidade do movimento de um formulário com controles, já que o intervalo de 16ms do timer é suficiente para redesenhar. O problema foi resolvido da maneira mais simples, mas não é a única maneira possível. A otimização do código da biblioteca será discutida nos próximos artigos da série, uma vez que existem outros métodos e opções que podem ajudar a reduzir o consumo da CPU de forma mais eficiente.
Otimização dos controles Lista Hierárquica e Navegador de Arquivos
Verificou-se que a inicialização levou muito tempo para a lista hierárquica (CTreeView), contendo um grande número de elementos. Isso também ocorreu no navegador de arquivos (CFileNavigator), que utiliza esse tipo de lista. Para resolver este problema, é necessário especificar o tamanho da reserva para o array como terceiro parâmetro da função ::ArrayResize() quando adicionar os elementos nos arrays.
Citação da referência da função ::ArrayResize():
…
Com a alocação de memória frequente, é recomendado o uso de um terceiro parâmetro que define uma reserva para reduzir o número de atribuições de memória física. Todas as chamadas subsequentes de ArrayResize não conduzem a realocação de memória física, mas apenas altera o tamanho da primeira dimensão do array dentro da memória reservada. Devemos lembrar que o terceiro parâmetro será usado somente durante a alocação de memória física...
…
Para efeito de comparação, abaixo estão os resultados dos testes com diferentes valores do tamanho da reserva para os arrays em uma lista hierárquica. O número de arquivos para o teste excede 15 000.
Fig. 5. Resultados dos testes para a formação de arrays com os valores do tamanho da reserva.
Define o tamanho da reserva para os arrays da lista hierárquica iguais a 10 000. As alterações apropriadas foram feitas para as classes CTreeView e CFileNavigator.
Novos ícones para as pastas e arquivos no navegador de arquivos.
Foi adicionado novos ícones para as pastas e arquivos do navegador de arquivos (a classe CFileNavigator), que são análogos aos utilizados no navegador de arquivos do sistema operacional Windows 10. Seu design elegante é mais adequado para as interfaces gráficas da biblioteca em desenvolvimento, mas se necessário, versões personalizadas podem ser usadas também.
Fig. 6. Novos ícones para as pastas e arquivos no navegador de arquivos.
Estas imagens estão disponíveis no final deste artigo.
Conclusão
A biblioteca para a criação de interfaces gráficas no atual estágio de desenvolvimento se parece com o esquema abaixo.
Fig. 6. Estrutura da biblioteca no atual estágio de desenvolvimento.
Os próximos artigos sobre interfaces gráficas continuarão com o desenvolvimento da biblioteca Easy And Fast. A biblioteca será expandida com controles adicionais, que possam ser necessárias para o desenvolvimento das aplicações MQL. Os controles existentes sofrerão melhorias e será adicionado novas funcionalidades.
Abaixo, você pode baixar a quarta versão (build 4) da biblioteca Easy And Fast.Se você estiver interessado, poderá contribuir para o desenvolvimento mais rápido deste projeto, sugerindo soluções para algumas tarefas nos comentários do artigo ou através de mensagens privadas.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/2763
- 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