
Soluções simples para trabalhar com indicadores
Os indicadores há muito tempo se tornaram parte essencial de qualquer plataforma de negociação, sendo utilizados por quase todos os traders. Muitas vezes, não se usa apenas um indicador no gráfico, mas sim um sistema inteiro, tornando a conveniência na configuração dos indicadores um aspecto crítico para a negociação.
Neste artigo, explicarei como criar um painel simples para ajustar as configurações de um indicador diretamente no gráfico e quais modificações são necessárias no indicador para integrar esse painel. Neste artigo, mostrarei como criar um painel simples para alterar as configurações do indicador diretamente no gráfico e quais alterações devem ser feitas no indicador para que esse painel seja integrado. O artigo é destinado a iniciantes em MQL5, então procurei explicar cada linha de código. Para profissionais, caso decidam ler o artigo, garanto que não encontrarão nada de novo aqui.
Criando o painel
Este site já apresenta diversos painéis, incluindo artigos e códigos disponíveis no CodeBase. Então, por que não usar algo pronto? Existem muitos códigos de qualidade, incluindo bibliotecas excelentes, que permitem criar painéis de qualquer complexidade.
No entanto, a busca pela universalidade muitas vezes compromete a simplicidade de uso dessas bibliotecas. Por isso, escreveremos um painel específico para indicadores, essencialmente uma tabela em que a largura e a altura das células se ajustam automaticamente ao tamanho do texto, conforme o tamanho da fonte.
Na parte superior do painel, haverá uma barra para arrastar todo o conteúdo. Essa barra exibirá o nome do indicador, bem como ícones para fixar e minimizar o painel. Cada célula da tabela será descrita por uma única linha de código. Dessa forma, será possível criar colunas de diferentes larguras em linhas distintas, se necessário.
Nome do painel (indicador) "Fixar" "Minimizar" |
---|
Nome da configuração 1 | Campo de entrada |
Nome da configuração 2 | Campo de entrada |
O código que descreve a célula incluirá: nome do objeto, tipo do objeto, número da linha, texto da célula e sua largura em % da largura do painel. A largura do painel e das células será interdependente. A soma das porcentagens de todas as células em uma linha deve ser igual a 100%.
Suponhamos que precisamos inserir três objetos na mesma linha. Nesse caso, o número da linha será o mesmo para os três objetos, e a largura, por exemplo, poderá ser 30% + 30% + 40% = 100%. Na maioria dos casos, basta dividir a linha em duas partes: 50% para o nome do parâmetro de configuração e 50% para o campo de entrada.
Como já mencionei, o código do painel foi projetado para ser o mais simples possível. Portanto, planejei evitar a programação orientada a objetos (POO). No entanto, não foi possível abrir mão completamente da POO. Copiar muito código de um indicador para outro seria inconveniente. Por isso, organizei o código do painel como uma classe em um arquivo de inclusão.
Optei por usar uma classe em vez de funções separadas, principalmente porque, no destrutor, é mais conveniente excluir os objetos do painel. Caso contrário, seria necessário excluí-los no OnDeinit() do indicador, o que seria mais complicado.
Também haverá um arquivo de inclusão, Object.mqh, com métodos para desenhar objetos, no qual também incluí getters e setters para facilitar o acesso às funções. Não entrarei em detalhes sobre getters e setters. Quem ainda não conhece o conceito pode pesquisar no Yandex, pois ele explicará melhor do que eu.
Parcialmente, a ideia do painel foi inspirada nestes artigos: artigo 1, artigo 2.
Todos os arquivos de código descritos neste artigo estão anexados ao final. Recomendo baixá-los e organizá-los em pastas antes de começar a estudar o código. Tenho uma pasta separada chamada "Object" na pasta "include" para o arquivo "Object.mqh". Para o arquivo Panel.mqh, criei uma pasta chamada Panel na pasta include. Portanto, o caminho para esses arquivos no meu código considera as pastas aninhadas.
Iniciaremos conectando o arquivo de inclusão Object.mqh e declarando as variáveis input. Precisamos declarar variáveis para definir as cores do painel, o texto, os botões, as bordas, além de cores adicionais que serão aplicadas ao painel ao ocultar o indicador, o tamanho e o estilo da fonte e as margens do painel em relação às bordas do gráfico.
Configurações Input:
//+------------------------------------------------------------------+ #include <Object\\Object.mqh> //+------------------------------------------------------------------+ input group "--- Input Panel ---" input int shiftX = 3; // Panel offset along the X axis input int shiftY = 80; // Panel offset along the Y axis input bool NoPanel = false; // No panel input int fontSize = 9; // Font size input string fontType = "Arial"; /* Font style*/ //"Arial", "Consolas" input string PanelHiddenShown = "❐"; // Panel hidden/displayed input string PanelPin = "∇"; /* Pin the panel*/ // ⮂ ↕ ↔ ➽ 🖈 ∇ input string PanelUnpin = "_"; // Unpin the panel input color clrTitleBar = C'109,117,171'; // Panel title background color (1) input color clrTitleBar2 = clrGray; // Panel title background color (2) input color clrDashboard = clrDarkGray; // Panel background color input color clrTextDashboard = clrWhite; // Text color on the panel input color clrBorder = clrDarkGray; // Border color input color clrButton1 = C'143,143,171'; // Button background color (1) input color clrButton2 = C'213,155,156'; // Button background color (2) input color clrButton3 = clrGray; // Button background color (3) input color clrTextButton1 = clrBlack; // Button text color (1) input color clrTextButton2 = clrWhite; // Button text color (2) input color clrEdit1 = C'240,240,245'; // Input field background color (1) input color clrEdit2 = clrGray; // Input field background color (2) input color clrTextEdit1 = C'50,50,50'; // Input field text color (1) input color clrTextEdit2 = clrWhite; // Input field text color (2) //+------------------------------------------------------------------+
Em seguida, vem a própria classe CPanel:
//+------------------------------------------------------------------+ class CPanel { private: enum ENUM_FLAG //flags { FLAG_PANEL_HIDDEN = 1, // panel hidden FLAG_PANEL_SHOWN = 2, // panel displayed FLAG_IND_HIDDEN = 4, // indicator hidden FLAG_IND_SHOWN = 8, // indicator displayed FLAG_PANEL_FIX = 16, // panel pinned FLAG_PANEL_UNPIN = 32 // panel unpinned }; int sizeObject; int widthPanel, heightPanel; int widthLetter, row_height; int _shiftX, _shiftY; long mouseX, mouseY; long chartWidth, chartHeight; string previousMouseState; long mlbDownX, mlbDownY, XDistance, YDistance; string _PanelHiddenShown, _PanelPin, _PanelUnpin; struct Object { string name; string text; ENUM_OBJECT object; int line; int percent; int column; int border; color txtColr; color backClr; color borderClr; }; Object mObject[]; int prefixInd; string Chart_ID; string addedNames[]; long addedXDisDiffrence[], addedYDisDiffrence[]; int WidthHidthCalc(int line, string text = "", int percent = 50, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL); void Add(string name); // save the object name and anchor point void HideShow(bool hide = false); // hide//show void DestroyPanel(); // delete all objects public: CPanel(void); ~CPanel(void); string namePanel; // panel name string indName; // indicator name should match indicator short name string prefix; // prefix for panel object names bool hideObject; // To be used as a flag in indicators where graphical objects need to be hidden int sizeArr; double saveBuffer[]; // array for storing the coordinates of the panel anchor point, panel properties (flag states), and the latest indicator settings enum ENUM_BUTON // flags for allowing button creation { BUTON_1 = 1, BUTON_2 = 2 }; void Init(string name, string indName); void Resize(int size) {sizeArr = ArrayResize(saveBuffer, size + 3); ZeroMemory(saveBuffer);}; void Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0); bool OnEvent(int id, long lparam, double dparam, string sparam); int Save() {ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); return GetLastError();} bool Load(string name) {return (FileLoad("pnl\\" + (string)ChartID() + name, saveBuffer) > 0);} void Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1); void ApplySaved(); void HideShowInd(bool hide); }; //+------------------------------------------------------------------+ CPanel::CPanel(void) {} //+------------------------------------------------------------------+ CPanel::~CPanel(void) {DestroyPanel(); ChartRedraw();} //+------------------------------------------------------------------+
Os métodos da classe serão discutidos mais adiante, com exemplos.
Para exemplificar, escreveremos um indicador vazio:
#property indicator_chart_window #property indicator_plots 0 input int _param = 10; #include <Panel\\Panel.mqh> CPanel mPanel; int param = _param; //+------------------------------------------------------------------+ int OnInit() { string short_name = "Ind Pnl(" + (string)param + ")"; mPanel.Init("Ind Pnl", short_name); mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60); mPanel.Record("param", OBJ_EDIT, 1, IntegerToString(param), 40); mPanel.Create(0); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return(rates_total); } //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+
Ao executar esse indicador, o gráfico exibirá um painel como este:
Agora, usando esse indicador como exemplo, analisaremos detalhadamente o código do painel.
Logo após os parâmetros input do indicador, conectamos o arquivo com a classe do painel e declaramos a classe do painel.
#property indicator_chart_window #property indicator_plots 0 input int _param = 10; #include <Panel\\Panel.mqh> CPanel mPanel; int param = _param;
Normalmente, a classe é declarada no início do código. No entanto, como a classe do painel está em um arquivo de inclusão que também contém parâmetros input, se a declararmos logo no início do código, os parâmetros input do indicador ficarão abaixo dos parâmetros input do painel. Isso causará certo desconforto ao iniciar e configurar o indicador.
Como as variáveis input são constantes, não é possível alterá-las. No entanto, é possível criar uma cópia das variáveis input e, a partir disso, trabalhar com elas e modificá-las no campo de entrada do painel.
Em seguida, adicionaremos o código do painel na função OnInit() do indicador.
Mas, antes disso, quero destacar que, para o funcionamento correto do painel, o código do indicador precisa incluir um nome curto para o indicador, com os principais parâmetros input especificados.
string short_name = "Ind Pnl(" + (string)_param + ")";
Isso é necessário para que seja possível executar o indicador com diferentes configurações.
Lembro que, nos nomes dos indicadores, alguns símbolos não podem ser usados. Se você quiser separar os parâmetros por dois-pontos, é melhor substituí-los por ponto e vírgula.
O nome do painel pode ser igual ao nome do indicador, mas é mais conveniente nomear o painel sem levar em conta os parâmetros do indicador.
O primeiro método da classe CPanel que adicionaremos ao indicador é o método Init(), no qual passaremos dois nomes: o nome do painel e o nome do indicador.
mPanel.Init("Ind Pnl", short_name);
A primeira tarefa do método Init() é verificar se o painel está desativado nas configurações.
void CPanel::Init(string name, string short_name) { if(NoPanel) return;
Em seguida, inicializamos as variáveis:
namePanel = name; indName = short_name; MovePanel = true; sizeObject = 0; Chart_ID = (string)ChartID(); int lastX = 0, lastY = 0;
Definiremos a permissão para que todos os programas MQL5 no gráfico enviem notificações de eventos de movimentação e cliques do mouse (CHARTEVENT_MOUSE_MOVE), além de permitir notificações de criação de objetos gráficos (CHARTEVENT_OBJECT_CREATE):
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true);
Para calcular a largura do painel, é necessário primeiro definir o tipo e o tamanho da fonte, além de obter o tamanho de um caractere. Com base nesse tamanho, calcularemos as margens do texto em relação às bordas do painel.
Assim, teremos margens escaláveis vinculadas ao tamanho da fonte. A altura da célula será igual a uma vez e meia a altura de um caractere.
// set the font type and size TextSetFont(fontType, fontSize * -10); // get the width and height of one character TextGetSize("0", widthLetter, row_height); // calculate the cell height row_height += (int)(row_height / 2);
Nas configurações do painel, existem ícones para ocultar/exibir o painel ❐, bem como para fixar/desafixar ∇ e _.
Encontrei esses ícones na internet, e é possível alterá-los nas configurações.
Adicionaremos espaços aos símbolos para garantir o posicionamento correto em relação à borda do painel. Se não adicionarmos espaços, os ícones ficam muito próximos e clicar neles com o mouse se torna complicado.
string space = " "; _PanelHiddenShown = space + PanelHiddenShown + space; _PanelPin = space + PanelPin + space; _PanelUnpin = space + PanelUnpin + space;
O painel é composto por objetos gráficos. Para garantir que os nomes desses objetos sejam únicos, criaremos um prefixo para eles:
MathSrand((int)GetMicrosecondCount()); prefixInd = MathRand(); prefix = (string)prefixInd;
Quero chamar sua atenção para o fato de que, se houver vários indicadores com painel no gráfico, não se deve usar a função GetTickCount() ao criar o prefixo. Isso ocorre porque, ao alternar os timeframes, o intervalo de tempo é tão curto que, se usarmos milissegundos em vez de microssegundos, os prefixos de alguns painéis podem coincidir.
Durante o arrasto do painel, é utilizada a função OnChartEvent(), que determina a posição do mouse no gráfico e no objeto. No entanto, o painel pode se sobrepor a outro painel, gerando um conflito, pois ambos entenderão que o mouse está arrastando o seu respectivo painel. Como resultado, todos os painéis sob o mouse serão movidos simultaneamente. Para evitar esse conflito, criaremos uma variável global. O primeiro painel que registrar seu prefixo nessa variável global será a que o mouse moverá. O princípio é simples: quem chega primeiro, leva.
Durante a inicialização, atribuiremos o valor zero a essa variável. Enquanto o valor for zero, ela será considerada livre.
GlobalVariableTemp("CPanel"); GlobalVariableSet("CPanel", 0);
Ao mover, minimizar, fixar o painel ou alterar as configurações do indicador, é necessário armazenar essas mudanças em algum lugar. Isso permitirá carregar o painel e o indicador com as últimas configurações ao alternar de timeframe ou reiniciar o terminal. No indicador apresentado, não incluí o código para salvar as últimas configurações do painel e do indicador, mas mesmo sem esse código, o indicador registrará as mudanças de configuração do painel. Para isso, é necessário alocar memória para um array de configurações do painel.
sizeArr = ArraySize(saveBuffer); if(sizeArr == 0) Resize(0);
Embora passemos o número de configurações do indicador como zero, Resize(0); a própria função adiciona três células para armazenar as configurações do painel. Ou seja, para lembrar a posição do painel no gráfico, seu estado (fixado/desfixado, minimizado/maximizado) e o estado do indicador (exibido/oculto), usamos três células no array saveBuffer.
O código a seguir determina as coordenadas iniciais do ponto de ancoragem do painel. Essa ancoragem pode ser definida pelas configurações input ou pelas coordenadas salvas, caso o painel já tenha sido desenhado no gráfico. Há também outra possibilidade: usar um template no qual o indicador com o painel já estava salvo.
No entanto, com o template, as coisas se complicam. Ao salvar um template contendo um indicador com painel, não é possível salvar diretamente as coordenadas exatas do painel no momento da criação do template.
Mas, se adicionarmos o indicador ao gráfico, salvarmos o template e depois aplicarmos esse template, perceberemos que ele salva um objeto do tipo OBJ_LABEL, uma etiqueta de texto.
Salvamos o template:
Aplicamos o template:
É exatamente essas etiquetas de texto que usamos para determinar a posição do painel no momento da criação do template.
string delPrefix = ""; int j = 0, total = ObjectsTotal(0, 0, OBJ_LABEL); for(int i = 0; i < total; i++) { string nameObject = ObjectName(0, i, 0, OBJ_LABEL); if(StringFind(nameObject, "TitleText " + indName) >= 0) // if the template contains objects with the name of this indicator { lastX = (int)GetXDistance(nameObject);// define the X coordinates of the panel in the template lastY = (int)GetYDistance(nameObject);// define the Y coordinates of the panel in the template StringReplace(nameObject, "TitleText " + indName, ""); // remember the object prefix for its subsequent deletion delPrefix = nameObject; } }
As coordenadas do ponto de ancoragem do objeto — a etiqueta de texto com o nome que inclui o prefixo seguido do nome do indicador — são armazenadas nas variáveis lastX e lastY. Essas coordenadas correspondem ao texto que exibe o nome do painel.
Lembro que o nome do painel pode ser diferente do nome do indicador. Depois de encontrar o texto desejado, extraímos o prefixo dele e o armazenamos.
O código a seguir, usando o prefixo armazenado na etapa anterior, remove do gráfico as etiquetas de texto residuais que foram salvas no template.
if(delPrefix != "")// delete obsolete objects saved in the template ObjectsDeleteAll(0, delPrefix);
Em seguida, ocorre a verificação e a seleção da opção apropriada para o ponto de ancoragem do painel.
if(lastX != 0 || lastY != 0)// if we use a template { lastX = lastX - widthLetter / 2; lastY = lastY - (int)(row_height / 8); saveBuffer[sizeArr - 1] = _shiftX = lastX; saveBuffer[sizeArr - 2] = _shiftY = lastY; } else// if data from the file is used if(saveBuffer[sizeArr - 1] != 0 || saveBuffer[sizeArr - 2] != 0) { _shiftX = (int)saveBuffer[sizeArr - 1]; _shiftY = (int)saveBuffer[sizeArr - 2]; } else// if this is the first launch of the indicator { saveBuffer[sizeArr - 1] = _shiftX = shiftX; saveBuffer[sizeArr - 2] = _shiftY = shiftY; }
No final do método Init(), registraremos no array de estruturas os objetos do painel que não precisam ser editados, pois serão os mesmos para todos os painéis.
São dois retângulos, o texto com o nome do painel e os ícones para ocultar/exibir e fixar/desafixar o painel.
Record("TitleBar"); Record("MainDashboardBody"); Record("TitleText " + indName, OBJ_LABEL, 0, namePanel, 100); Record("PinUnpin", OBJ_LABEL, 0, _PanelPin, 0); Record("CollapseExpand", OBJ_LABEL, 0, _PanelHiddenShown, 0);
Após analisar o método Init(), passamos para o próximo método, Record().
No método Record(), preenche-se a estrutura do objeto futuro. Geralmente, a maior parte da estrutura é preenchida com valores padrão. No entanto, o conjunto de parâmetros passados para essa função permite alterar alguns valores padrão e, por exemplo, definir uma cor diferente para o objeto.
//+------------------------------------------------------------------+ void CPanel::Record(string name, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL, int line = -1, string text = "", int percent = 50, color txtColr = 0, color backClr = 0, color borderClr = 0) { if(NoPanel) return; int column = WidthHidthCalc(line + 1, text, percent, object); ArrayResize(mObject, sizeObject + 1); mObject[sizeObject].column = column; // column mObject[sizeObject].name = prefix + name; // object name mObject[sizeObject].object = object; // object type mObject[sizeObject].line = line + 1; // line index mObject[sizeObject].text = text; // text (if any) mObject[sizeObject].percent = percent; // percentage of panel width mObject[sizeObject].txtColr = txtColr; // text color mObject[sizeObject].backClr = backClr; // base color mObject[sizeObject].borderClr = borderClr; // border color mObject[sizeObject].border = 0; // offset from the panel edge sizeObject++; } //+------------------------------------------------------------------+
No início do método Record(), há uma chamada ao método WidthHidthCalc(), responsável por calcular a largura e a altura do painel.
Vamos detalhar o método WidthHidthCalc().
Esse método calcula a largura do painel com base no elemento mais largo. Por exemplo, se no indicador "Ind Pnl", mencionado anteriormente, alterarmos o nome para algo mais extenso.
Era assim:
mPanel.Init("Ind Pnl", short_name);
Alteramos para:
mPanel.Init("Ind Pnl 0000000000000000000", short_name);
Obtemos o seguinte painel:
Da mesma forma, se mudarmos o nome de uma configuração do indicador, o resultado será o seguinte.
Era assim:
mPanel.Record("paramText", OBJ_LABEL, 1, "param", 60);
Alteramos para:
mPanel.Record("paramText 0000000000000000000", OBJ_LABEL, 1, "param", 60);
Resultado:
O painel ajusta-se automaticamente ao tamanho do texto. Todos os cálculos de largura e altura do painel são realizados pela função WidthHidthCalc().
Primeiramente, obtemos a largura do texto da célula.
A largura do texto com o nome do painel e dos ícones de ocultar/exibir é calculada de forma ligeiramente diferente das outras células.
int CPanel::WidthHidthCalc(int line, string text = "", int percent = 50, ENUM_OBJECT object = OBJ_RECTANGLE_LABEL) { static int lastLine = -1, column = 0; int width, height; if(line == 1) TextGetSize(text + _PanelPin + _PanelHiddenShown, width, height); // get the width and height of the text for the line with the panel name else TextGetSize(text, width, height); // get the text width and height
O texto deve ter uma margem em relação à borda da célula, e essa margem será igual a meio caractere. Já calculamos a largura de um caractere na função Init() e armazenamos na variável widthLetter.
Para garantir a margem de ambos os lados do texto, devemos adicionar à largura do texto a largura de mais um caractere. Para o texto do objeto "Botão" (OBJ_BUTTON), será necessário adicionar ainda outro caractere para a margem das bordas do botão.
Depois de conhecermos o tamanho total da linha na célula, considerando as margens, podemos calcular o tamanho final do painel com base nas porcentagens atribuídas a cada célula.
Registramos o maior valor encontrado. Posteriormente, todas as células serão calculadas com base nessa maior largura de painel.
double indent = 0; if(object == OBJ_BUTTON) indent += widthLetter; if(text != "" && percent != 0) { // calculate the width of the panel based on the text size and the percentage allocated for this text int tempWidth = (int)MathCeil((width + widthLetter + indent) * 100 / percent); if(widthPanel < tempWidth) widthPanel = tempWidth; }
O cálculo da largura do painel no indicador de teste será apresentado da seguinte forma.
Primeiro, calcula-se a largura do nome, considerando os ícones. O nome "Ind Pnl" + " ∇ " + " ❐ " resulta em uma largura de 71 px, mais a largura de um caractere (7 px), totalizando 78 px — esse valor corresponde a 100% da largura do painel.
O texto da célula — "param" — tem uma largura de 36 px, considerando as margens adicionadas de 7 px, totalizando 43 px. Essa célula ocupa 60% da largura do painel, então a largura total do painel será 43 * 100 / 60 = 72 px. Como esse valor é menor do que a largura necessária para o nome do painel, a largura final do painel será determinada pela célula com o nome do painel.
Em seguida, determina-se o número da coluna e, se for uma nova linha, adiciona-se a altura do painel.
if(lastLine != line)// if this is a new row in the panel, then increase the height of the entire panel { heightPanel = row_height * line; lastLine = line; column = 0; // reset the number of columns in the new row } else column++; // add a new column return column; }
Assim, analisamos detalhadamente o funcionamento de dois dos dez métodos da classe CPanel.
Depois que o programa define os tamanhos futuros do painel e grava os parâmetros dos objetos no array de estruturas mObject[], passamos para o próximo método — Create(). Esse método constrói o painel com base nos tamanhos calculados anteriormente.
No início do método, como de costume, verifica-se se o painel é necessário. Em seguida, vem o código para registrar dois botões predefinidos. Um botão serve para ocultar o indicador, e o outro para removê-lo. Dependendo das flags selecionadas, é possível escolher entre as seguintes opções: 0 - sem botões, 1 - um botão para ocultar/exibir o indicador, 2 - um botão para remover o indicador, 3 - ambos os botões serão criados.
Por que esses botões estão aqui e não no código do indicador? Simplesmente para reduzir a quantidade de código a ser incluída nos indicadores.
Depois, ocorre a inicialização das variáveis. Este código específico é útil se você quiser usar o painel de uma forma diferente da originalmente planejada, como um painel flutuante para ajustar parâmetros de objetos, de modo que ele apareça no local do clique do mouse no gráfico.
void CPanel::Create(uint Button = BUTON_1 | BUTON_2, int shiftx = -1, int shifty = -1) { if(NoPanel) return; if((Button & BUTON_1) == BUTON_1)// if we need to create buttons Record("hideButton", OBJ_BUTTON, mObject[sizeObject - 1].line, "Ind Hide", 50); if((Button & BUTON_2) == BUTON_2)// if we need to create buttons Record("delButton", OBJ_BUTTON, mObject[sizeObject - 2].line, "Ind Del", 50, clrTextButton1, clrButton2); ENUM_ANCHOR_POINT ap = ANCHOR_LEFT_UPPER; int X = 0, Y = 0, xSize = 0, ySize = 0; if(shiftx != -1 && shifty != -1) { _shiftX = shiftx; _shiftY = shifty; }
Explicando melhor sobre esse código — o painel pode ser adaptado como um painel informativo, um painel de negociação ou um painel de configuração de objetos gráficos. Tudo de forma simples, sem sofisticação, focado apenas na funcionalidade:
Desviei um pouco do assunto, então vamos voltar à análise do método Create(). Em seguida, o código cria dois retângulos: um para o cabeçalho e outro para o corpo do painel.
// header rectangle RectLabelCreate(0, mObject[0].name, 0, _shiftX, _shiftY, widthPanel, row_height, (mObject[0].backClr == 0 ? clrTitleBar : mObject[0].backClr), BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[0].borderClr == 0 ? clrBorder2 : mObject[0].borderClr), STYLE_SOLID, 1, false, false, true, 1, indName); Add(mObject[0].name);// remember the object's anchor point // panel rectangle RectLabelCreate(0, mObject[1].name, 0, _shiftX, row_height - 1 + _shiftY, widthPanel, heightPanel - row_height, (mObject[1].backClr == 0 ? clrDashboard : mObject[1].backClr), BORDER_FLAT, CORNER_LEFT_UPPER, (mObject[1].borderClr == 0 ? clrBorder1 : mObject[1].borderClr), STYLE_SOLID, 1, false, false, true, 0, indName); Add(mObject[1].name);
Após a criação de cada objeto, chama-se a função Add(), que registra nos arrays o nome e as coordenadas desse objeto em relação ao canto superior esquerdo do gráfico.
//+------------------------------------------------------------------+ void CPanel::Add(string name)// save the object name and anchor point { int size = ArraySize(addedNames); ArrayResize(addedNames, size + 1); ArrayResize(addedXDisDiffrence, size + 1); ArrayResize(addedYDisDiffrence, size + 1); addedNames[size] = name; addedXDisDiffrence[size] = GetXDistance(addedNames[0]) - GetXDistance(name); addedYDisDiffrence[size] = GetYDistance(addedNames[0]) - GetYDistance(name); } //+------------------------------------------------------------------+
Esses arrays de coordenadas serão usados posteriormente para mover o painel.
Voltando ao código do método Create(), todos os objetos são criados em um laço, seguindo a mesma ordem em que foram registrados no array de estruturas mObject[]. Primeiro, calcula-se as coordenadas e dimensões, depois cria-se o objeto.
Como o painel é especializado, ele utiliza apenas três tipos de objetos, o que é mais do que suficiente para garantir sua funcionalidade.
Ao preencher o retângulo do cabeçalho com texto, foi necessário recorrer a exceções e definir um ponto de ancoragem diferente para os ícones de fixação e minimização do painel, distinto do ponto de ancoragem dos demais objetos do painel. Isso simplificou o posicionamento desses ícones, já que eles são ancorados no canto superior direito.
for(int i = 2; i < sizeObject; i++) { // calculate the coordinates of the object anchor point if(mObject[i].column != 0) { X = mObject[i - 1].border + widthLetter / 2; mObject[i].border = mObject[i - 1].border + (int)MathCeil(widthPanel * mObject[i].percent / 100); } else { X = _shiftX + widthLetter / 2; mObject[i].border = _shiftX + (int)MathCeil(widthPanel * mObject[i].percent / 100); } Y = row_height * (mObject[i].line - 1) + _shiftY + (int)(row_height / 8); //--- switch(mObject[i].object) { case OBJ_LABEL: ap = ANCHOR_LEFT_UPPER; // unlike all other objects, the "pin" and "collapse" objects' anchor points are implemented in the upper right corner. if(i == 3) { int w, h; TextGetSize(_PanelHiddenShown, w, h); X = _shiftX + widthPanel - w; ap = ANCHOR_RIGHT_UPPER; } if(i == 4) { X = _shiftX + widthPanel; ap = ANCHOR_RIGHT_UPPER; } LabelCreate(0, mObject[i].name, 0, X, Y, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize, (mObject[i].txtColr == 0 ? clrTextDashboard : mObject[i].txtColr), 0, ap, false, false, true, 1); break; case OBJ_EDIT: xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter; ySize = row_height - (int)(row_height / 4); EditCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, mObject[i].text, fontType, fontSize, ALIGN_LEFT, false, CORNER_LEFT_UPPER, (mObject[i].txtColr == 0 ? clrTextEdit1 : mObject[i].txtColr), (mObject[i].backClr == 0 ? clrEdit1 : mObject[i].backClr), (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, true, 1); break; case OBJ_BUTTON: xSize = (int)(widthPanel * mObject[i].percent / 100) - widthLetter; ySize = row_height - (int)(row_height / 4); ButtonCreate(0, mObject[i].name, 0, X, Y, xSize, ySize, CORNER_LEFT_UPPER, mObject[i].text, fontType, fontSize, (mObject[i].txtColr == 0 ? clrTextButton1 : mObject[i].txtColr), (mObject[i].backClr == 0 ? clrButton1 : mObject[i].backClr), (mObject[i].borderClr == 0 ? clrBorder1 : mObject[i].borderClr), false, false, false, true, 1); break; } Add(mObject[i].name); }
Depois que todos os objetos do painel forem criados, removeremos o array de estruturas mObject[], pois ele não será mais necessário.
ArrayFree(mObject); ApplySaved(); ChartRedraw();
Geralmente, crio uma função separada se o código for reutilizado várias vezes. No entanto, quando a operação está logicamente agrupada, prefiro destacá-la como um método separado. Foi o que fiz com a função ApplySaved(). Ela verifica se já existem dados salvos do painel e os aplica, se existirem, ou salva novos dados caso contrário.
ApplySaved()
Se for a primeira execução do indicador nesse gráfico, o array saveBuffer[] será preenchido com as configurações iniciais.
Se o array saveBuffer[] já contiver dados salvos, aplicaremos essas informações em vez das configurações iniciais.
//+------------------------------------------------------------------+ void CPanel::ApplySaved() { // collapse the panel immediately after the indicator is launched, if this is saved in the file if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN) CPanel::OnEvent(CHARTEVENT_OBJECT_CLICK, 0, 0, addedNames[4]); else saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_SHOWN; // hide the indicator immediately after the indicator is launched, if this is saved in the file if(((uint)saveBuffer[sizeArr - 3] & FLAG_IND_HIDDEN) == FLAG_IND_HIDDEN) { HideShowInd(true); SetButtonState(prefix + "hideButton", true); hideObject = true; } else { saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_IND_SHOWN; hideObject = false; } // pin the panel immediately after the indicator is launched, if this is saved in the file if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_FIX) == FLAG_PANEL_FIX) SetText(addedNames[3], _PanelUnpin); else saveBuffer[sizeArr - 3] = (uint)saveBuffer[sizeArr - 3] | FLAG_PANEL_UNPIN; int Err = Save(); if(Err != 0) Print("!!! Save Error = ", Err, "; Chart_ID + indName =", Chart_ID + indName); } //+------------------------------------------------------------------+
Como você deve ter notado, a função ApplySaved() utiliza outras funções como Save(), HideShowInd() e OnEvent(). Se você está lendo isso, por favor, escreva "percebi" nos comentários. Estou curioso para saber se alguém realmente lê essas explicações.
Vamos descrever essas funções por ordem. Na função Save(), salvamos as configurações obtidas. Para evitar bagunçar a pasta Files, criaremos uma pasta separada, chamada pnl, especificamente para armazenar as configurações salvas dos painéis.
Aqui está como a função Save() se apresenta:
int Save() { ResetLastError(); FileSave("pnl\\" + Chart_ID + indName, saveBuffer); return GetLastError(); }
HideShowInd()
A função HideShowInd() tem como único objetivo alterar a cor do cabeçalho do painel, bem como a cor e o texto do botão. Do array saveBuffer, removemos a flag anterior e registramos a nova.
A variável hideObject será necessária apenas nos indicadores que utilizam objetos gráficos, como setas, ícones, texto, entre outros. Ao criar um novo objeto no indicador, verificaremos o estado dessa variável e, dependendo do seu valor, ocultaremos automaticamente os objetos recém-criados ou os exibiremos normalmente.
//+------------------------------------------------------------------+ void CPanel::HideShowInd(bool hide) { // change the color and text of the buttons depending on the state of the panel, hidden/displayed, as well as the header color if(hide) { SetColorBack(prefix + "TitleBar", clrTitleBar2); SetColorBack(prefix + "hideButton", clrButton3); SetText(prefix + "hideButton", "Ind Show"); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_SHOWN) | FLAG_IND_HIDDEN; hideObject = true; } else { SetColorBack(prefix + "TitleBar", clrTitleBar); SetColorBack(prefix + "hideButton", clrButton1); SetText(prefix + "hideButton", "Ind Hide"); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_IND_HIDDEN) | FLAG_IND_SHOWN; hideObject = false; } Save(); ChartRedraw(); } //+------------------------------------------------------------------+
Essa função é ativada apenas ao clicar no botão de ocultar/exibir indicador Ind Show/Ind Hide, caso esteja em uso.
Exemplo de ocultação de um dos dois indicadores RSI
O código também inclui a função HideShow(), responsável por ocultar ou exibir objetos. Essa função controla a minimização do painel e a exibição dos objetos na frente do gráfico.
A função recebe um argumento que indica se o painel está minimizado ou não (true/false). Se o painel estiver minimizado, exibiremos apenas quatro objetos: o retângulo com o nome, o próprio nome do painel e dois ícones — o de fixar e o de minimizar o painel.
Se a flag for true, ou seja, se o painel estiver minimizado, ocultaremos e exibiremos cinco objetos em sequência. Por que cinco e não quatro? Isso acontece porque, entre os objetos necessários, há um extra — o retângulo principal do painel. Criamos esse retângulo antes do nome e dos ícones, por isso ele precisa ser ocultado separadamente.
Se a flag for false, todos os objetos do painel serão ocultados e exibidos em sequência, trazendo-os para o primeiro plano.
//+------------------------------------------------------------------+ void CPanel::HideShow(bool hide = false) // hide and immediately display objects to bring to the foreground { int size = hide ? 5 : ArraySize(addedNames); for(int i = 0; i < size; i++) { SetHide(addedNames[i]); SetShow(addedNames[i]); } if(hide) SetHide(addedNames[1]); } //+------------------------------------------------------------------+
Veja como o painel minimizado aparece ao lado do painel normal:
A próxima função que analisaremos é a OnEvent().
No início da função, verificamos se o painel está ativado nas configurações:
bool CPanel::OnEvent(int id, long lparam, double dparam, string sparam) { if(NoPanel) return false;
Agora, vamos analisar o código responsável pelo movimento do painel. Tentarei explicar seu funcionamento com o máximo de detalhes possível.
Quando ocorre o evento de "movimento do mouse" e a flag que permite mover o painel está ativada, armazenamos as coordenadas do mouse.
Se for um clique do botão esquerdo, ou seja, se o valor do argumento sparam for igual a 1, e o mouse não estiver previamente pressionado, lemos o valor da variável global.
Essa variável global é compartilhada por todos os painéis em execução no terminal. Durante o movimento do painel, verificamos se essa variável contém o prefixo de outro painel. Se não contiver, registramos nela o prefixo do painel em movimento, impedindo que outros painéis, mesmo que estejam sob ou sobre ele, sejam movidos simultaneamente.
Se a variável global contiver zero ou o prefixo deste painel, lemos as coordenadas atuais do ponto de ancoragem do painel.
Em seguida, verificamos se o clique foi feito no retângulo do cabeçalho do painel. Se for o caso, obtemos as dimensões do gráfico em pixels, desativamos a rolagem do gráfico e registramos o prefixo do painel na variável global. Esse registro do prefixo funciona como uma flag que autoriza o movimento do painel e garante que apenas esse painel será movido.
Por fim, ocultamos ou exibimos o painel para trazê-lo ao primeiro plano, passando o argumento (true/false) para indicar se o painel está atualmente oculto ou completamente visível.
if(id == CHARTEVENT_MOUSE_MOVE && ((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN) { mouseX = (long)lparam; mouseY = (long)dparam; if(previousMouseState != "1" && sparam == "1") { int gvg = (int)GlobalVariableGet("Panel"); if(gvg == prefixInd || gvg == 0) { XDistance = GetXDistance(addedNames[0]); YDistance = GetYDistance(addedNames[0]); mlbDownX = mouseX; mlbDownY = mouseY; if(mouseX >= XDistance && mouseX <= XDistance + widthPanel && mouseY >= YDistance && mouseY <= YDistance + row_height) { chartWidth = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); chartHeight = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); ChartSetInteger(0, CHART_MOUSE_SCROLL, false); GlobalVariableSet("Panel", prefixInd); HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN); // hide/display the panel so that it is in the foreground } } }
Depois que o usuário clica no retângulo do nome do painel e começa a mover o mouse, o código seguinte é ativado.
Primeiramente, verificamos se o movimento do painel está permitido. Se a variável global contiver o prefixo desse painel, ele poderá ser movido. Em seguida, localizamos as coordenadas do ponto de ancoragem do painel. Ao mover o painel conforme as coordenadas do mouse, verificamos a nova posição. Se houver risco de o painel ultrapassar os limites do gráfico, ajustamos ligeiramente os valores das coordenadas de ancoragem para mantê-lo dentro da área visível do gráfico
No laço de repetição, movemos todos os objetos do painel.
Registramos as novas coordenadas do ponto de ancoragem do painel no array, para posterior gravação no arquivo. Em seguida, redesenhamos o gráfico.
if((int)GlobalVariableGet("Panel") == prefixInd) { // disable the ability to go beyond the chart for the panel long posX = XDistance + mouseX - mlbDownX; if(posX < 0) posX = 0; else if(posX + widthPanel > chartWidth) posX = chartWidth - widthPanel; long posY = YDistance + mouseY - mlbDownY; if(posY < 0) posY = 0; else if(posY + row_height > chartHeight) posY = chartHeight - row_height; // move the panel int size = ArraySize(addedNames); for(int i = 0; i < size; i++) { SetXDistance(addedNames[i], posX - addedXDisDiffrence[i]); SetYDistance(addedNames[i], posY - addedYDisDiffrence[i]); } saveBuffer[sizeArr - 1] = (double)(posX); saveBuffer[sizeArr - 2] = (double)(posY); ChartRedraw(0); }
A última ação durante o movimento do painel ocorre quando o botão do mouse é liberado, fazendo com que o valor de sparam deixe de ser igual a um.
Restauramos a possibilidade de rolar o gráfico, zeramos a variável global e gravamos no arquivo as novas coordenadas do ponto de ancoragem do painel.
if(sparam != "1" && (int)GlobalVariableGet("Panel") == prefixInd) { ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); } previousMouseState = sparam; }
Após analisar detalhadamente o mecanismo de arrastar o painel, vamos agora examinar as ações ao clicar nos ícones de fixação/desfixação do painel ou de minimizar/maximizar o painel.
Tudo isso é tratado na mesma função OnEvent().
Quando ocorre o evento de clique do mouse em um objeto gráfico, a variável sparam armazena o nome do objeto clicado. Se esse nome coincidir com o objeto "❐", verificamos o próximo objeto. Se ele estiver visível, ocultamos os objetos do painel; se estiver invisível, exibimos todos os objetos do painel. Em seguida, alteramos a flag de visibilidade do painel e a registramos no array para posterior gravação no arquivo.
Ao clicar em um objeto, às vezes o código anterior, responsável pelo movimento do painel, é acionado. Como o ícone "❐" está localizado na área de arrasto do painel, isso frequentemente desativa a rolagem do gráfico. Para evitar isso, permitimos novamente a rolagem e zeramos a variável global.
Salvamos as alterações no arquivo para que sejam refletidas no gráfico e o redesenhamos.
else if(id == CHARTEVENT_OBJECT_CLICK) { if(sparam == addedNames[4]) // prefix+"CollapseExpand" { if(GetShow(addedNames[5]) == OBJ_ALL_PERIODS)// if the panel is visible, hide it { SetHide(addedNames[1]); for(int i = 5; i < sizeObject; i++) SetHide(addedNames[i]); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_SHOWN) | FLAG_PANEL_HIDDEN; } else// if the panel is hidden, display it { for(int i = 0; i < sizeObject; i++) SetShow(addedNames[i]); saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_HIDDEN) | FLAG_PANEL_SHOWN; } ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); ChartRedraw(0); }
O código seguinte é semelhante ao descrito acima, com a única diferença no nome do objeto "∇".
else if(sparam == addedNames[3]) // prefix+"PinUnpin" { if(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN) { saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_UNPIN) | FLAG_PANEL_FIX; SetText(addedNames[3], _PanelUnpin); } else { saveBuffer[sizeArr - 3] = ((uint)saveBuffer[sizeArr - 3] & ~FLAG_PANEL_FIX) | FLAG_PANEL_UNPIN; SetText(addedNames[3], _PanelPin); } ChartSetInteger(0, CHART_MOUSE_SCROLL, true); GlobalVariableSet("Panel", 0); Save(); ChartRedraw(0); }
O código se encerra com o botão de exclusão do indicador:
else if(sparam == prefix + "delButton") // handle the indicator deletion button ChartIndicatorDelete(0, ChartWindowFind(), indName); }
O último evento a ser tratado é a criação de um objeto gráfico.
Os objetos recém-criados geralmente são posicionados no primeiro plano, podendo cobrir o painel. Para evitar isso, primeiro registramos o estado de seleção do objeto, depois ocultamos/exibimos o painel para trazê-lo ao primeiro plano e, finalmente, restauramos o estado de seleção do objeto. Por que todo esse cuidado? Isso ocorre porque ocultar e exibir programaticamente os objetos do painel faz com que os novos objetos percam a seleção.
Esse fenômeno é descrito com mais detalhes neste artigo.
else if(id == CHARTEVENT_OBJECT_CREATE)//https://www.mql5.com/ru/articles/13179 "Making a dashboard to display data in indicators and EAs" { bool select = GetSelect(sparam); HideShow(((uint)saveBuffer[sizeArr - 3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN);// hide/display the panel so that it is in the foreground SetSelect(sparam, select);// restore the state of the extreme object } return true; }
Com isso, concluímos a descrição do código do painel.
Quais alterações são necessárias no código do indicador
Diferentes indicadores exigem diferentes ajustes. Não foi possível criar um código universal que pudesse ser simplesmente inserido no indicador para obter um painel de controle pronto. Cada indicador precisa ser ajustado individualmente.
Neste artigo, apresentarei alguns exemplos que ajudarão você a modificar outros indicadores de forma semelhante.
Para padronizar, adicionei a palavra "Pnl" aos nomes dos indicadores que foram modificados.
Indicador Custom Moving Average
Para controlar o indicador a partir do painel, é necessário permitir a alteração das variáveis de entrada. No entanto, as variáveis input são constantes e não podem ser modificadas.
Para resolver esse problema, podemos copiar as variáveis input para variáveis comuns, que podem ser alteradas. Para minimizar as mudanças no código do indicador, declararemos novas variáveis com os mesmos nomes das variáveis input atuais, mas com um sublinhado (_) no início.
Era assim:
Fucou assim:
Logo após os parâmetros input, incluímos o arquivo Panel.mqh e declaramos uma instância da classe CPanel mPanel;
Se a diretiva #include for escrita antes dos parâmetros input, todos os parâmetros input do arquivo incluído aparecerão acima dos parâmetros input do indicador, o que pode ser inconveniente ao executar o indicador.
Se tudo for feito corretamente e o indicador for iniciado, veremos algo assim:
Se as configurações do painel não forem necessárias, basta remover todas as ocorrências da palavra "input" no arquivo Panel.mqh e usar as configurações padrão.
Na função OnInit(), adicionamos o seguinte código.
Em seguida, verificamos se o painel está ativado nesse indicador e carregamos as configurações salvas anteriormente, caso não seja a primeira execução do indicador nesse gráfico. Se for a primeira execução, ajustamos o tamanho do array para o número de parâmetros input, neste caso três, e gravamos no array os valores desses parâmetros input.
if(!NoPanel) { if(mPanel.Load(short_name)) { InpMAPeriod = (int)mPanel.saveBuffer[0]; InpMAShift = (int)mPanel.saveBuffer[1]; InpMAMethod = (int)mPanel.saveBuffer[2]; } else { mPanel.Resize(3); mPanel.saveBuffer[0] = InpMAPeriod; mPanel.saveBuffer[1] = InpMAShift; mPanel.saveBuffer[2] = InpMAMethod; }
Nome do painel, nome do indicador
O restante é preenchido de maneira uniforme: nome do objeto, tipo do objeto, número da linha no painel, o próprio objeto e a porcentagem da largura do painel.
mPanel.Init("Moving Average", short_name); mPanel.Record("MAPeriodText", OBJ_LABEL, 1, "MAPeriod:", 50); mPanel.Record("MAPeriod", OBJ_EDIT, 1, IntegerToString(InpMAPeriod), 50); mPanel.Record("MAShiftText", OBJ_LABEL, 2, "MAShift:", 50); mPanel.Record("MAShift", OBJ_EDIT, 2, IntegerToString(InpMAShift), 50); mPanel.Record("MAMethodText", OBJ_LABEL, 3, "MAMethod:", 50); mPanel.Record("MAMethod", OBJ_EDIT, 3, IntegerToString(InpMAMethod), 50); mPanel.Create(); }
Se executarmos o indicador neste estágio, obteremos o seguinte painel:
Agora que temos o painel, resta escrever o código para a comunicação com o usuário.
Adicionamos mais uma função ao indicador — OnChartEvent().
O método responsável pelo movimento do painel já foi descrito anteriormente. Quando ocorre o evento "término da edição de texto no objeto gráfico Edit", verificamos o prefixo do objeto Edit editado. Se o prefixo corresponder ao prefixo do nosso painel, verificamos qual parâmetro do indicador foi alterado, registramos as mudanças na variável e no array para posterior salvamento.
Em seguida, salvamos todas as mudanças no arquivo e reiniciamos o indicador.
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "MAPeriod") { mPanel.saveBuffer[0] = InpMAPeriod = (int)StringToInteger(GetText(sparam)); } else if(sparam == mPanel.prefix + "MAShift") { mPanel.saveBuffer[1] = InpMAShift = (int)StringToInteger(GetText(sparam)); PlotIndexSetInteger(0, PLOT_SHIFT, InpMAShift); } else if(sparam == mPanel.prefix + "MAMethod") { mPanel.saveBuffer[2] = InpMAMethod = (int)StringToInteger(GetText(sparam)); } mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); }
O tratamento do botão de ocultar/exibir o indicador no indicador MA é extremamente simples.
Para ocultar a linha da média móvel, basta definir o estilo de construção gráfica como DRAW_NONE e, para exibi-la novamente, usar DRAW_LINE.
if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE); mPanel.HideShowInd(false); } }
Com isso, as mudanças no indicador Custom Moving Average estão concluídas. O indicador modificado agora se chama Custom Moving Average Pnl.
Indicador ParabolicSAR
A modificação do indicador ParabolicSAR é praticamente idêntica à do Custom Moving Average, com apenas pequenas diferenças.
No indicador ParabolicSAR, não é necessário criar novas variáveis com os mesmos nomes das variáveis input, pois elas já existem.
Portanto, basta incluir o arquivo de inclusão:
Na função OnInit(), adicionamos o seguinte código:
if(!NoPanel) { if(mPanel.Load(short_name)) { ExtSarStep = mPanel.saveBuffer[0]; ExtSarMaximum = mPanel.saveBuffer[1]; } else { mPanel.Resize(2); mPanel.saveBuffer[0] = ExtSarStep; mPanel.saveBuffer[1] = ExtSarMaximum; } mPanel.Init("ParabolicSAR", short_name); mPanel.Record("SARStepText", OBJ_LABEL, 1, "SARStep:", 50); mPanel.Record("SARStep", OBJ_EDIT, 1, DoubleToString(ExtSarStep, 3), 50); mPanel.Record("SARMaximumText", OBJ_LABEL, 2, "SARMax:", 50); mPanel.Record("SARMaximum", OBJ_EDIT, 2, DoubleToString(ExtSarMaximum, 2), 50); mPanel.Create(); }
Em seguida, incluímos a função OnChartEvent() no código do indicador.
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "SARStep") mPanel.saveBuffer[0] = ExtSarStep = StringToDouble(GetText(sparam)); else if(sparam == mPanel.prefix + "SARMaximum") mPanel.saveBuffer[1] = ExtSarMaximum = StringToDouble(GetText(sparam)); mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW); PlotIndexSetInteger(0, PLOT_ARROW, 159); mPanel.HideShowInd(false); } } //+------------------------------------------------------------------+
E com isso, as mudanças no indicador ParabolicSAR estão concluídas.
Indicador RSI
No indicador RSI, o processo é exatamente o mesmo dos dois indicadores anteriores.
No escopo global, logo após as configurações input, inserimos o seguinte:
#include <Panel\\Panel.mqh>
CPanel mPanel;
Depois, em OnInit():
if(!NoPanel) { if(mPanel.Load(short_name)) { ExtPeriodRSI = (int)mPanel.saveBuffer[0]; } else { mPanel.Resize(1); mPanel.saveBuffer[0] = ExtPeriodRSI; } mPanel.Init("RSI", short_name); mPanel.Record("PeriodRSIText", OBJ_LABEL, 1, "PeriodRSI:", 60); mPanel.Record("PeriodRSI", OBJ_EDIT, 1, IntegerToString(ExtPeriodRSI), 40); mPanel.Create(); }
A função OnChartEvent() será um pouco diferente dos indicadores anteriores.
O tratamento do objeto de campo de entrada é feito exatamente como nos outros indicadores. No entanto, a lógica de ocultar/exibir o indicador é diferente, pois os exemplos anteriores tratavam de indicadores no gráfico principal, enquanto o RSI é um indicador de subjanela.
Ao clicar no botão "Ind Hide", definimos a altura da janela do indicador como zero, alteramos a cor do painel e também a cor e o texto do botão.
Ao clicar novamente no botão, agora renomeado para "Ind Show", definimos o valor de CHART_HEIGHT_IN_PIXELS como -1, além de ajustar a cor do painel, a cor e o texto do botão.
Citação do manual:
"A configuração programática da propriedade CHART_HEIGHT_IN_PIXELS impede que o usuário edite o tamanho da janela ou subjanela. Para remover essa restrição, defina o valor da propriedade como -1."
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { if(mPanel.OnEvent(id, lparam, dparam, sparam)) { if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) if(sparam == mPanel.prefix + "PeriodRSI") { mPanel.saveBuffer[0] = ExtPeriodRSI = (int)StringToInteger(GetText(sparam)); mPanel.Save(); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") // hide the subwindow indicator { if(GetButtonState(sparam)) { ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), 0); mPanel.HideShowInd(true); } else { ChartSetInteger(0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), -1); mPanel.HideShowInd(false); } } } } //+------------------------------------------------------------------+
Outro indicador
Existem indicadores que não utilizam estilos gráficos, mas desenham objetos gráficos, como setas. Esse é outro cenário para o tratamento do botão "Ocultar/Exibir Indicador". Vamos analisá-lo em detalhes.
Não procurei por um indicador de setas, então escrevi um indicador de fractais, onde os ícones superiores são exibidos usando a construção gráfica PLOT_ARROW, e os ícones inferiores são desenhados como objetos OBJ_ARROW.
Aqui está o código completo do indicador.
As configurações incluem o tamanho das extremidades do fractal, bem como o número de dias em que desenharemos os objetos OBJ_ARROW. Foi necessário limitar o número de dias, pois um excesso de objetos pode desacelerar significativamente o gráfico.
Assim como nos indicadores anteriores, logo após as variáveis input, incluímos o arquivo Panel.mqh e declaramos uma instância da classe CPanel.
Duplicamos as variáveis input como variáveis comuns.
#property indicator_chart_window #property indicator_plots 1 #property indicator_buffers 1 #property indicator_type1 DRAW_ARROW #property indicator_color1 clrRed #property indicator_label1 "Fractals" input int _day = 10; // day input int _barLeft = 1; // barLeft input int _barRight = 1; // barRight #include <Panel\\Panel.mqh> CPanel mPanel; double buff[]; int day = _day, barLeft = _barLeft, barRight = _barRight; datetime limitTime = 0;
Na função OnInit(), tudo é feito da mesma forma que nos indicadores anteriores.
//+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0, buff, INDICATOR_DATA); PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, 0.0); PlotIndexSetInteger(0, PLOT_ARROW, 217); PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, -5); string short_name = StringFormat("Fractals(%d,%d)", _barLeft, _barRight); IndicatorSetString(INDICATOR_SHORTNAME, short_name); if(!NoPanel) { if(mPanel.Load(short_name)) { day = (int)mPanel.saveBuffer[0]; barLeft = (int)mPanel.saveBuffer[1]; barRight = (int)mPanel.saveBuffer[2]; } else { mPanel.Resize(3); mPanel.saveBuffer[0] = day; mPanel.saveBuffer[1] = barLeft; mPanel.saveBuffer[2] = barRight; } mPanel.Init("Fractals", short_name); mPanel.Record("dayText", OBJ_LABEL, 1, "Days:", 50); mPanel.Record("day", OBJ_EDIT, 1, IntegerToString(day), 50); mPanel.Record("barLeftText", OBJ_LABEL, 2, "barLeft:", 50); mPanel.Record("barLeft", OBJ_EDIT, 2, IntegerToString(barLeft), 50); mPanel.Record("barRightText", OBJ_LABEL, 3, "barRight:", 50); mPanel.Record("barRight", OBJ_EDIT, 3, IntegerToString(barRight), 50); mPanel.Create(); } return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
A principal diferença em relação à função OnChartEvent() dos indicadores anteriores é que, sempre que os parâmetros do indicador forem alterados, será necessário excluir do gráfico os objetos desenhados pelo indicador.
Ao clicar no botão para ocultar o indicador, ocultamos todos os objetos desenhados pelo indicador em um laço. Além disso, definimos o estilo gráfico como DRAW_NONE.
No processo inverso, além de definir o estilo gráfico como DRAW_ARROW, também precisamos especificar o número da seta no conjunto Wingdings. Em seguida, tornamos visíveis todos os objetos anteriormente ocultos em um laço.
//+------------------------------------------------------------------+ void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) { mPanel.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPanel.prefix) >= 0) { if(sparam == mPanel.prefix + "day") mPanel.saveBuffer[0] = day = (int)StringToInteger(GetText(sparam)); else if(sparam == mPanel.prefix + "barLeft") mPanel.saveBuffer[1] = barLeft = (int)StringToInteger(GetText(sparam)); else if(sparam == mPanel.prefix + "barRight") mPanel.saveBuffer[2] = barRight = (int)StringToInteger(GetText(sparam)); mPanel.Save(); ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW); ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT); } if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") { if(GetButtonState(sparam)) { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE); for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "DN_") >= 0) SetHide(name); } mPanel.HideShowInd(true); } else { PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_ARROW); PlotIndexSetInteger(0, PLOT_ARROW, 217); for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string name = ObjectName(0, i); if(StringFind(name, "DN_") >= 0) SetShow(name); } mPanel.HideShowInd(false); } } } //+------------------------------------------------------------------+
Também precisamos adicionar ao código do indicador uma verificação após cada novo objeto desenhado para flag, indicando a necessidade de ocultar o indicador e, se esse sinalizador for verdadeiro, então ocultar o objeto recém-desenhado.
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
int limit = prev_calculated - 1;
if(prev_calculated <= 0)
{
ArrayInitialize(buff, 0);
datetime itime = iTime(_Symbol, PERIOD_D1, day);
limitTime = itime <= 0 ? limitTime : itime;
if(limitTime <= 0)
return 0;
int shift = iBarShift(_Symbol, PERIOD_CURRENT, limitTime);
limit = MathMax(rates_total - shift, barRight + barLeft);
}
for(int i = limit; i < rates_total && !IsStopped(); i++)
{
bool condition = true;
for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
if(high[j - 1] >= high[j])
{
condition = false;
break;
}
if(condition)
for(int j = i - barRight + 1; j <= i; j++)
if(high[j - 1] <= high[j])
{
condition = false;
break;
}
if(condition)
buff[i - barRight] = high[i - barRight];
condition = true;
for(int j = i - barRight - barLeft + 1; j <= i - barRight; j++)
if(low[j - 1] <= low[j])
{
condition = false;
break;
}
if(condition)
for(int j = i - barRight + 1; j <= i; j++)
if(low[j - 1] >= low[j])
{
condition = false;
break;
}
if(condition)
{
string name = mPanel.prefix + "DN_" + (string)time[i - barRight];
ObjectCreate(0, name, OBJ_ARROW, 0, time[i - barRight], low[i - barRight]);
ObjectSetInteger(0, name, OBJPROP_ARROWCODE, 218);
ObjectSetInteger(0, name, OBJPROP_COLOR, clrBlue);
if(mPanel.hideObject)
SetHide(name);
}
}
return(rates_total);
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW);
}
//+------------------------------------------------------------------+
Durante o desenvolvimento deste artigo, apresentei um exemplo de uso do painel como acesso rápido às configurações de objetos. Acredito que vale a pena explicar o código desse indicador.
Indicador Setting Objects Pnl
Nesse indicador, não precisamos criar um objeto da classe do painel na função OnInit(), pois o painel será chamado para diferentes objetos. Portanto, ele será criado dinamicamente usando o operador new.
Declaramos o descritor do objeto da classe. Se houver um clique em qualquer objeto gráfico do gráfico enquanto a tecla Shift estiver pressionada, inicializamos o descritor do objeto da classe criado anteriormente.
Criamos o painel da mesma forma que nos indicadores anteriores, com uma única diferença — passamos as coordenadas atuais do mouse no gráfico como argumentos para o método Create().
As mudanças nos campos de entrada são tratadas da mesma forma que nos indicadores, com uma diferença: aqui, não é necessário salvar as alterações em um arquivo.
Após concluir a edição, o painel pode ser removido clicando no botão "Del Pnl", o que também excluirá o descritor.
Como diferentes objetos podem ter diferentes propriedades, o painel deve ser desenhado levando isso em conta. Se estivermos editando uma linha de tendência, por exemplo, não será necessário um campo para preenchimento do objeto no painel.
Portanto, para linhas de tendência, esse campo não será criado. Ele será adicionado apenas para objetos que suportam preenchimento. Nessa situação, não podemos saber antecipadamente o número exato de linhas no painel, então introduziremos a variável line, que armazenará o número da linha atual e será incrementada conforme necessário.
#property indicator_chart_window #property indicator_plots 0 #define FREE(P) if(CheckPointer(P) == POINTER_DYNAMIC) delete (P) #include <Panel\\Panel.mqh> CPanel * mPl; //+------------------------------------------------------------------+ int OnCalculate(const int, const int, const int, const double &price[]) {return(0);} //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { static bool panel = false; if(panel) mPl.OnEvent(id, lparam, dparam, sparam); if(id == CHARTEVENT_OBJECT_CLICK) if(!panel) { if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) < 0) { int line = 1; mPl = new CPanel(); ENUM_OBJECT ObjectType = (ENUM_OBJECT)GetType(sparam); mPl.Init(EnumToString(ObjectType), sparam); mPl.Record("Color_Text", OBJ_LABEL, line, "Color", 50); mPl.Record("Color", OBJ_EDIT, line, ColorToString((color)GetColor(sparam)), 50); line++; mPl.Record("StyleText", OBJ_LABEL, line, "Style", 50); mPl.Record("Style", OBJ_EDIT, line, IntegerToString(GetStyle(sparam)), 50); line++; mPl.Record("WidthText", OBJ_LABEL, line, "Width", 50); mPl.Record("Width", OBJ_EDIT, line, IntegerToString(GetWidth(sparam)), 50); line++; if(ObjectType == OBJ_RECTANGLE || ObjectType == OBJ_RECTANGLE_LABEL || ObjectType == OBJ_TRIANGLE || ObjectType == OBJ_ELLIPSE) { mPl.Record("FillText", OBJ_LABEL, line, "Fill", 50); mPl.Record("Fill", OBJ_EDIT, line, IntegerToString(GetFill(sparam)), 50); line++; } mPl.Record("delButton", OBJ_BUTTON, line, "Del Pnl", 100); mPl.Create(0, (int)lparam, (int)dparam); panel = true; } } else if(sparam == mPl.prefix + "delButton") { FREE(mPl); panel = false; } if(id == CHARTEVENT_OBJECT_ENDEDIT) if(StringFind(sparam, mPl.prefix) >= 0) { if(sparam == mPl.prefix + "Color") SetColor(mPl.indName, StringToColor(GetText(sparam))); else if(sparam == mPl.prefix + "Style") SetStyle(mPl.indName, (int)StringToInteger(GetText(sparam))); else if(sparam == mPl.prefix + "Width") SetWidth(mPl.indName, (int)StringToInteger(GetText(sparam))); else if(sparam == mPl.prefix + "Fill") SetFill(mPl.indName, (int)StringToInteger(GetText(sparam))); ChartRedraw(); } } //+------------------------------------------------------------------+ void OnDeinit(const int reason) {FREE(mPl);} //+------------------------------------------------------------------+
O painel de configurações de objetos é ativado pressionando a tecla Shift junto com o clique esquerdo do mouse no objeto.
Conclusão
Vantagens:
- No geral, o resultado foi uma solução prática e fácil de usar.
Desvantagens:
- Não foi possível integrar a funcionalidade de "ocultar" o indicador diretamente na classe CPanel.
- Se as variáveis input não forem incluídas no nome curto do indicador, não será possível executar vários indicadores com o painel, devido à coincidência de nomes.
- Se o indicador for executado no gráfico, suas configurações forem alteradas pelo painel e o template for salvo, ao carregar esse template, as configurações carregadas não serão as últimas usadas, mas sim as definidas no momento da inicialização do indicador.
- Não é possível anexar o painel a todos os indicadores.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/14672





- 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
São muitas letras.
elogios e respeito ao autor!
São muitas cartas.
elogios e respeito ao autor!
Obrigado.
notado;)
Uau!
Sinceramente, eu não esperava por isso. Você me surpreendeu agradavelmente!!!
Ótimo trabalho, Alexander! Respeito!