Gráficos na biblioteca DoEasy (Parte 77): classe do objeto Sombra
Sumário
Ideia
No último artigo, quando estávamos criando o objeto-forma, abordamos um pouco o tópico de criação de sombra. Fizemos um complemento de teste do objeto para construir sombras sobre ele. Hoje iremos desenvolver e retrabalhar esta ideia para usar em conjunto com o objeto-forma como componente constante. Nosso objeto-forma poderá criar este objeto, desenhar uma forma de sombra nele e exibi-lo na tela, logo que precisemos disso.
Consideramos duas maneiras de desenhar sombras em objetos:
- diretamente na tela do próprio objeto-forma,
- num objeto separado "deitado" sob o objeto-forma.
Para facilitar a implementação, escolhemos a segunda variante. Uma desvantagem dessa abordagem é que precisaremos controlar um objeto adicional, mas, por outro lado, poderemos, por exemplo, alterar rapidamente as coordenadas do local da sombra, é só mover o elemento gráfico onde desenhada a sombra.
No caso de desenhar uma sombra no objeto-forma, seria necessário redesenhar toda a forma junto com a sombra (ou apagar a sombra desenhada, recalcular suas coordenadas e desenhar novamente), e isso dá mais trabalho. Além disso, a sombra deve ser aplicada aos objetos que estão sob ela - para recalcular a fusão de cores e transparência nos locais onde a sombra é sobreposta aos objetos e, pixel por pixel, redesenhar o fundo no qual está localizada a sombra projetada pelo objeto. No caso de usar um objeto separado para a sombra, não precisamos fazer isso, uma vez que ela já terá sua própria cor e transparência, que será sobreposta aos objetos subjacentes, o terminal irá calcular isso para nós.
Sem dúvida, o método de desenho de sombras diretamente na tela da forma tem vantagens, mas ainda assim iremos escolher a segunda variante devido à sua simplicidade a nível de implementação e controle. Na primeira implementação do objeto de sombra, usaremos o método de desfoque gaussiano usando a Biblioteca de análise numérica ALGLIB. Algumas explicações de como usá-la para sombreamento foram descritos no artigo "Estudando a classe CCanvas. Anti-alising e sombras" escrito por Vladimir Karputov. Vamos levar em consideração os métodos de desfoque gaussiano descritos em seu artigo.
O objeto de sombra será uma nova classe herdada da classe do objeto do elemento gráfico da mesma forma que criamos o objeto-forma - todos esses objetos são herdeiros do elemento gráfico base, como muitos outros. No objeto-forma, criaremos métodos para criar rapidamente um objeto de sombra e alterar suas propriedades. Bem, como de costume, iremos modificar as classes da biblioteca já escritas.
Aprimorando as classes da biblioteca
Comecemos inserindo no arquivo \MQL5\Include\DoEasy\Data.mqh os índices das novas mensagens da biblioteca:
MSG_LIB_SYS_FAILED_DRAWING_ARRAY_RESIZE, // Failed to change the array size of drawn buffers MSG_LIB_SYS_FAILED_COLORS_ARRAY_RESIZE, // Failed to change the color array size MSG_LIB_SYS_FAILED_ARRAY_RESIZE, // Failed to change the array size MSG_LIB_SYS_FAILED_ADD_BUFFER, // Failed to add buffer object to the list MSG_LIB_SYS_FAILED_CREATE_BUFFER_OBJ, // Failed to create \"Indicator buffer\" object
...
//--- CChartObjCollection MSG_CHART_COLLECTION_TEXT_CHART_COLLECTION, // Chart collection MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ, // Failed to create a new chart object MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART, // Failed to add a chart object to the collection MSG_CHART_COLLECTION_ERR_CHARTS_MAX, // Cannot open new chart. Number of open charts at maximum MSG_CHART_COLLECTION_CHART_OPENED, // Chart opened MSG_CHART_COLLECTION_CHART_CLOSED, // Chart closed MSG_CHART_COLLECTION_CHART_SYMB_CHANGED, // Chart symbol changed MSG_CHART_COLLECTION_CHART_TF_CHANGED, // Chart timeframe changed MSG_CHART_COLLECTION_CHART_SYMB_TF_CHANGED, // Chart symbol and timeframe changed //--- CForm MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT,// No shadow object. Create it using the CreateShadowObj() method MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ, // Failed to create new shadow object //--- CShadowObj MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE, // Error! Image size too small or blur too extensive }; //+------------------------------------------------------------------+
e escrevemos os textos das mensagens correspondentes aos índices recém-adicionados:
{"Не удалось изменить размер массива рисуемых буферов","Failed to resize drawing buffers array"}, {"Не удалось изменить размер массива цветов","Failed to resize color array"}, {"Не удалось изменить размер массива ","Failed to resize array "}, {"Не удалось добавить объект-буфер в список","Failed to add buffer object to list"}, {"Не удалось создать объект \"Индикаторный буфер\"","Failed to create object \"Indicator buffer\""},
...
//--- CChartObjCollection {"Коллекция чартов","Chart collection"}, {"Не удалось создать новый объект-чарт","Failed to create new chart object"}, {"Не удалось добавить объект-чарт в коллекцию","Failed to add chart object to collection"}, {"Нельзя открыть новый график, так как количество открытых графиков уже максимальное","You cannot open a new chart, since the number of open charts is already maximum"}, {"Открыт график","Open chart"}, {"Закрыт график","Closed chart"}, {"Изменён символ графика","Changed chart symbol"}, {"Изменён таймфрейм графика","Changed chart timeframe"}, {"Изменён символ и таймфрейм графика","Changed the symbol and timeframe of the chart"}, //--- CForm {"Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()","There is no shadow object. You must first create it using the CreateShadowObj () method"}, {"Не удалось создать новый объект для тени","Failed to create new object for shadow"}, //--- CShadowObj {"Ошибка! Размер изображения очень маленький или очень большое размытие","Error! Image size is very small or very large blur"}, }; //+---------------------------------------------------------------------+
No último artigo, deixamos um espaço vazio ao redor do objeto-forma com um tamanho de cinco pixels de cada lado para desenhar a sombra. Acontece que precisamos de mais espaço para um desfoque gaussiano normal. Empiricamente, descobri que, com um raio de desfoque de 4 pixels, precisamos deixar 16 pixels de espaço vazio em cada lado. Menos pixels quando há desfoque faz com que apareçam artefatos (contaminação de fundo onde a sombra já é completamente transparente e realmente ausente) ao longo das bordas da tela em que a sombra é desenhada.
No arquivo \MQL5\Include\DoEasy\Defines.mqh inserimos um tamanho de espaço livre padrão para a sombra igual a 16 (em vez de 5 anteriormente):
//--- Canvas parameters #define PAUSE_FOR_CANV_UPDATE (16) // Canvas update frequency #define NULL_COLOR (0x00FFFFFF) // Zero for the canvas with the alpha channel #define OUTER_AREA_SIZE (16) // Размер одной стороны внешней области вокруг рабочего пространства //+------------------------------------------------------------------+
Já à enumeração de tipos de elementos gráficos adicionamos um novo tipo, Shadow Object:
//+------------------------------------------------------------------+ //| The list of graphical element types | //+------------------------------------------------------------------+ enum ENUM_GRAPH_ELEMENT_TYPE { GRAPH_ELEMENT_TYPE_ELEMENT, // Element GRAPH_ELEMENT_TYPE_SHADOW_OBJ, // Shadow object GRAPH_ELEMENT_TYPE_FORM, // Form GRAPH_ELEMENT_TYPE_WINDOW, // Window }; //+------------------------------------------------------------------+
Ao criar um novo objeto para a sombra, especificaremos exatamente o tipo, o que permitirá no futuro selecionar rapidamente todos os objetos-sombra e manipulá-los ao mesmo tempo.
Como hoje iremos criar um objeto de sombra, ele terá suas próprias propriedades que afetarão a aparência da sombra projetada pelo objeto-forma.
Adicionamos esses parâmetros às configurações de estilo da forma no arquivo \MQL5\Include\DoEasy\GraphINI.mqh:
//+------------------------------------------------------------------+ //| List of form style parameter indices | //+------------------------------------------------------------------+ enum ENUM_FORM_STYLE_PARAMS { FORM_STYLE_FRAME_WIDTH_LEFT, // Form frame width to the left FORM_STYLE_FRAME_WIDTH_RIGHT, // Form frame width to the right FORM_STYLE_FRAME_WIDTH_TOP, // Form frame width on top FORM_STYLE_FRAME_WIDTH_BOTTOM, // Form frame width below FORM_STYLE_FRAME_SHADOW_OPACITY, // Shadow opacity FORM_STYLE_FRAME_SHADOW_BLUR, // Shadow blur FORM_STYLE_DARKENING_COLOR_FOR_SHADOW, // Form shadow color darkening FORM_STYLE_FRAME_SHADOW_X_SHIFT, // Shadow X axis shift FORM_STYLE_FRAME_SHADOW_Y_SHIFT, // Shadow Y axis shift }; #define TOTAL_FORM_STYLE_PARAMS (9) // Number of form style parameters //+------------------------------------------------------------------+ //| Array containing form style parameters | //+------------------------------------------------------------------+ int array_form_style[TOTAL_FORM_STYLES][TOTAL_FORM_STYLE_PARAMS]= { //--- "Flat form" style parameters { 3, // Form frame width to the left 3, // Form frame width to the right 3, // Form frame width on top 3, // Form frame width below 80, // Shadow opacity 4, // Shadow blur 80, // Form shadow color darkening 2, // Shadow X axis shift 2, // Shadow Y axis shift }, //--- "Embossed form" style parameters { 4, // Form frame width to the left 4, // Form frame width to the right 4, // Form frame width on top 4, // Form frame width below 80, // Shadow opacity 4, // Shadow blur 80, // Form shadow color darkening 2, // Shadow X axis shift 2, // Shadow Y axis shift }, }; //+------------------------------------------------------------------+
Desfocar a sombra é o parâmetro que definirá o raio de desfoque da imagem.
O tom da cor da sombra da forma é o parâmetro que indicará quantos pontos é preciso escurecer a cor da sombra. Este parâmetro é importante quando a cor da sombra depende da cor de fundo do gráfico. Nesse caso, a cor de fundo do gráfico é convertida em cinza e escurecida pelo valor especificado.
Os deslocamentos de sombra ao longo dos eixos X e Y indicam o quanto a sombra será deslocada do centro do objeto que a projeta. Zero indica uma sombra ao redor do objeto. Valores positivos para o deslocamento da sombra para a direita para baixo em relação ao objeto, já valores negativos para o deslocamento da sombra para a esquerda para cima.
Assim, como alteramos o número de parâmetros, precisamos indicar isso explicitamente. Inserimos o novo valor de 9 em vez do anterior valor, que era 5.
E vamos adicionar mais um parâmetro às configurações do esquema de cores, "Cor que contorna o retângulo da forma"
Para uma exibição mais clara das formas, desenharemos bordas ao redor delas (não deve ser confundida com a borda da forma) - um retângulo simples que com sua cor destaca a forma contra o fundo externo. A cor deste retângulo será definida nesta configuração.
//+------------------------------------------------------------------+ //| List of indices of color scheme parameters | //+------------------------------------------------------------------+ enum ENUM_COLOR_THEME_COLORS { COLOR_THEME_COLOR_FORM_BG, // Form background color COLOR_THEME_COLOR_FORM_FRAME, // Form frame color COLOR_THEME_COLOR_FORM_RECT_OUTER, // Form outline rectangle color COLOR_THEME_COLOR_FORM_SHADOW, // Form shadow color }; #define TOTAL_COLOR_THEME_COLORS (4) // Number of parameters in the color theme //+------------------------------------------------------------------+ //| The array containing color schemes | //+------------------------------------------------------------------+ color array_color_themes[TOTAL_COLOR_THEMES][TOTAL_COLOR_THEME_COLORS]= { //--- Parameters of the "Blue steel" color scheme { C'134,160,181', // Form background color C'134,160,181', // Form frame color clrDimGray, // Form outline rectangle color clrGray, // Form shadow color }, //--- Parameters of the "Light cyan gray" color scheme { C'181,196,196', // Form background color C'181,196,196', // Form frame color clrGray, // Form outline rectangle color clrGray, // Form shadow color }, }; //+------------------------------------------------------------------+
Modificamos a classe do elemento gráfico no arquivo \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh.
Na seção pública da classe, temos um método ChangeColorLightness() que altera a luminosidade.
A cor a ser alterada é passada para o método, em formato ARGB. Isso nem sempre é conveniente, por isso declaramos um método sobrecarregado, ao qual iremos transferir a cor em formato color e opacidade:
//--- Update the coordinates (shift the canvas) bool Move(const int x,const int y,const bool redraw=false); //--- Change the brightness of (1) ARGB and (2) COLOR by a specified amount uint ChangeColorLightness(const uint clr,const double change_value); color ChangeColorLightness(const color colour,const uchar opacity,const double change_value);
Também precisaremos de métodos para alterar a saturação da cor. Por exemplo, para fazer uma cor cinza a partir de qualquer cor, precisamos mudar seu componente Saturation (S nos formatos HSL, HSI, HSV, HSB) para a esquerda, para zero. Assim, a cor ficará completamente dessaturada - estará numa escala de cinza, que é o que precisamos para pintar a sombra.
Vamos declarar dois métodos sobrecarregados que mudam a saturação de cor:
//--- Change the brightness of (1) ARGB and (2) COLOR by a specified amount uint ChangeColorLightness(const uint clr,const double change_value); color ChangeColorLightness(const color colour,const double change_value); //--- Change the saturation of (1) ARGB and (2) COLOR by a specified amount uint ChangeColorSaturation(const uint clr,const double change_value); color ChangeColorSaturation(const color colour,const double change_value); protected:
Fora do corpo da classe, vamos escrever a implementação dos métodos declarados.
Método que altera a saturação de uma cor ARGB:
//+------------------------------------------------------------------+ //| Change the ARGB color saturation by a specified amount | //+------------------------------------------------------------------+ uint CGCnvElement::ChangeColorSaturation(const uint clr,const double change_value) { if(change_value==0.0) return clr; double a=GETRGBA(clr); double r=GETRGBR(clr); double g=GETRGBG(clr); double b=GETRGBB(clr); double h=0,s=0,l=0; CColors::RGBtoHSL(r,g,b,h,s,l); double ns=s+change_value*0.01; if(ns>1.0) ns=1.0; if(ns<0.0) ns=0.0; CColors::HSLtoRGB(h,ns,l,r,g,b); return ARGB(a,r,g,b); } //+------------------------------------------------------------------+
Aqui: decompomos a cor obtida como um valor uint em seus componentes - canal alfa, vermelho, verde e azul.
Usando o método RGBtoHSL() da classe CColors descrita no artigo 75, convertemos a cor RGB em modelo de cor HSL, onde precisamos de seu componente S - saturação de cor. Em seguida, calculamos a nova saturação simplesmente adicionando ao valor de saturação da cor o valor passado ao método e multiplicado por 0,01. O resultado obtido é verificado para sair da faixa de valores aceitáveis (0 a 1) e novamente usando a classe CColors e seu método HSLtoRGB convertemos os componentes da cor H, o novo S e L no formato RGB.
Retornamos a cor RGB resultante com a adição do canal alfa da cor inicial.
Por que estamos multiplicando por 0,01 o valor de alteração da saturação passada ao método? Apenas por conveniência. Como no modelo de cores HSL, os valores dos componentes mudam na faixa de 0 a 1, então, em primeiro lugar, é mais conveniente transferir esses valores em múltiplos de 100 (1 em vez de 0,01, 10 em vez de 0,1, 100 em vez de 1), e em segundo lugar, nos nossos estilos de formas, onde pode haver valores para a mudança na saturação de cor para quaisquer formulários ou textos, todos os valores são escritos em valores inteiros, e isso a razão é muito mais significativa do que a primeira.
Método que altera a saturação de uma cor COLOR para o valor especificado:
//+------------------------------------------------------------------+ //| Change the COLOR saturation by a specified amount | //+------------------------------------------------------------------+ color CGCnvElement::ChangeColorSaturation(const color colour,const double change_value) { if(change_value==0.0) return colour; uint clr=::ColorToARGB(colour,0); double r=GETRGBR(clr); double g=GETRGBG(clr); double b=GETRGBB(clr); double h=0,s=0,l=0; CColors::RGBtoHSL(r,g,b,h,s,l); double ns=s+change_value*0.01; if(ns>1.0) ns=1.0; if(ns<0.0) ns=0.0; CColors::HSLtoRGB(h,ns,l,r,g,b); return CColors::RGBToColor(r,g,b); } //+------------------------------------------------------------------+
A lógica do método é semelhante à do discutido acima. A única diferença é que precisamos do parâmetro opacidade apenas para converter a cor e sua opacidade numa cor ARGB; além disso, o canal alfa não é usado em nenhum outro lugar. Por isso, ao implementar a conversão, podemos ignorá-lo e passar zero. Em seguida, extraímos os componentes R, G e B da cor ARGB, convertemo-los para o modelo de cor HSL, corrigimos o componente S usando o valor passado para o método, convertemos o modelo HSL de volta em RGB e retornamos o modelo de cor RGB convertido em cor em formato de color.
Método que altera o brilho de uma cor COLOR consoante ao valor especificado:
//+------------------------------------------------------------------+ //| Change the COLOR brightness by a specified amount | //+------------------------------------------------------------------+ color CGCnvElement::ChangeColorLightness(const color colour,const double change_value) { if(change_value==0.0) return colour; uint clr=::ColorToARGB(colour,0); double r=GETRGBR(clr); double g=GETRGBG(clr); double b=GETRGBB(clr); double h=0,s=0,l=0; CColors::RGBtoHSL(r,g,b,h,s,l); double nl=l+change_value*0.01; if(nl>1.0) nl=1.0; if(nl<0.0) nl=0.0; CColors::HSLtoRGB(h,s,nl,r,g,b); return CColors::RGBToColor(r,g,b); } //+------------------------------------------------------------------+
O método é idêntico ao anterior, exceto que mudamos o componente L do modelo de cor HSL.
Como em todos os métodos considerados multiplicamos o valor para alterar o componente de cor por 0,01, precisamos modificar o método que escrevemos anteriormente que altera o brilho da cor ARGB:
//+------------------------------------------------------------------+ //| Change the ARGB color brightness by a specified value | //+------------------------------------------------------------------+ uint CGCnvElement::ChangeColorLightness(const uint clr,const double change_value) { if(change_value==0.0) return clr; double a=GETRGBA(clr); double r=GETRGBR(clr); double g=GETRGBG(clr); double b=GETRGBB(clr); double h=0,s=0,l=0; CColors::RGBtoHSL(r,g,b,h,s,l); double nl=l+change_value*0.01; if(nl>1.0) nl=1.0; if(nl<0.0) nl=0.0; CColors::HSLtoRGB(h,s,nl,r,g,b); return ARGB(a,r,g,b); } //+------------------------------------------------------------------+
Na seção pública da classe, no bloco de métodos para acesso simplificado às propriedades do objeto, temos declarado um método que define um sinalizador de necessidade de usar uma sombra para a forma. Mas, por algum motivo, não há implementação desse método. Vamos corrigir essa omissão:
//--- Set the flag of (1) object moveability, (2) activity, (3) element ID, (4) element index in the list and (5) shadow presence void SetMovable(const bool flag) { this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,flag); } void SetActive(const bool flag) { this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,flag); } void SetID(const int id) { this.SetProperty(CANV_ELEMENT_PROP_ID,id); } void SetNumber(const int number) { this.SetProperty(CANV_ELEMENT_PROP_NUM,number); } void SetShadow(const bool flag) { this.m_shadow=flag; } //--- Return the shift (1) of the left, (2) right, (3) top and (4) bottom edge of the element active area
Todos os objetos-formas que fizemos até o momento, embora tenham alguma tridimensionalidade devido aos realces nas bordas iluminadas e escurecimento nas não iluminadas, isso não será suficiente como formatação. Vamos prosseguir e adicionar a capacidade de criar um fundo com a ilusão de tridimensionalidade. Para fazer isso, precisamos de um preenchimento gradiente do fundo com pelo menos duas cores - do mais escuro ao mais claro. Para isso, bastará uma pequena mudança no brilho da cor original e uma mistura suave desta última com a mais clara, mais uma sombra, assim a aparência da forma "brilhará com novas cores":
Já implementamos dois métodos de limpeza de forma e preenchimento com cor. Para preencher o fundo com uma cor gradiente declaramos mais um método Erase():
//+------------------------------------------------------------------+ //| The methods of filling, clearing and updating raster data | //+------------------------------------------------------------------+ //--- Clear the element filling it with color and opacity void Erase(const color colour,const uchar opacity,const bool redraw=false); //--- Clear the element with a gradient fill void Erase(color &colors[],const uchar opacity,const bool vgradient=true,const bool cycle=false,const bool redraw=false); //--- Clear the element completely void Erase(const bool redraw=false); //--- Update the element void Update(const bool redraw=false) { this.m_canvas.Update(redraw); }
Fora do corpo da classe, vamos escrever sua implementação:
//+------------------------------------------------------------------+ //| Clear the element with a gradient fill | //+------------------------------------------------------------------+ void CGCnvElement::Erase(color &colors[],const uchar opacity,const bool vgradient=true,const bool cycle=false,const bool redraw=false) { //--- Check the size of the color array int size=::ArraySize(colors); //--- If there are less than two colors in the array if(size<2) { //--- if the array is empty, erase the background completely and leave if(size==0) { this.Erase(redraw); return; } //--- in case of one color, fill the background with this color and opacity, and leave this.Erase(colors[0],opacity,redraw); return; } //--- Declare the receiver array color out[]; //--- Set the gradient size depending on the filling direction (vertical/horizontal) int total=(vgradient ? this.Height() : this.Width()); //--- and get the set of colors in the receive array CColors::Gradient(colors,out,total,cycle); total=::ArraySize(out); //--- In the loop by the number of colors in the array for(int i=0;i<total;i++) { //--- depending on the filling direction switch(vgradient) { //--- Horizontal gradient - draw vertical segments from left to right with the color from the array case false : DrawLineVertical(i,0,this.Height()-1,out[i],opacity); break; //--- Vertical gradient - draw horizontal segments downwards with the color from the array default: DrawLineHorizontal(0,this.Width()-1,i,out[i],opacity); break; } } //--- If specified, update the canvas this.Update(redraw); } //+------------------------------------------------------------------+
Toda a lógica do método é descrita nos comentários da listagem. Ao método são transferidos uma matriz de cores preenchida, o valor de opacidade, o sinalizador de gradiente vertical (se true, o enchimento será realizado de cima para baixo, se false, da esquerda para a direita), o sinalizador de looping (se definido, o preenchimento terminará com a mesma cor com que começou) e o sinalizador de necessidade de redesenhar a tela após o preenchimento. Para obter uma matriz de cores, é usado o método Gradient() da classe CColors.
Concluímos as alterações e adições às classes da biblioteca. Agora vamos escrever uma nova classe para o objeto-sombra, que herdará a classe do objeto do elemento gráfico.
Classe do objeto de sombra
No diretório de objetos gráficos da biblioteca \MQL5\Include\DoEasy\Objects\Graph\ criamos o novo arquivo ShadowObj.mqh da classe CShadowObj.
No arquivo devem estar integrados o arquivo de classe do elemento gráfico e o arquivo de biblioteca de análise numérica ALGLIB. A classe deve ser herdada da classe do objeto do elemento gráfico:
//+------------------------------------------------------------------+ //| ShadowObj.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "GCnvElement.mqh" #include <Math\Alglib\alglib.mqh> //+------------------------------------------------------------------+ //| Shadow object class | //+------------------------------------------------------------------+ class CShadowObj : public CGCnvElement { }
Na seção privada da classe declaramos variáveis para armazenar a cor e opacidade da sombra e métodos para funcionamento da classe:
//+------------------------------------------------------------------+ //| Shadow object class | //+------------------------------------------------------------------+ class CShadowObj : public CGCnvElement { private: color m_color_shadow; // Shadow color uchar m_opacity_shadow; // Shadow opacity //--- Gaussian blur bool GaussianBlur(const uint radius); //--- Return the array of weight ratios bool GetQuadratureWeights(const double mu0,const int n,double &weights[]); //--- Draw the object shadow form void DrawShadowFigureRect(const int w,const int h); public:
Aqui o método DrawShadowFigureRect() desenha uma forma não desfocada com as dimensões de um objeto-forma que projeta uma sombra desenhada por este objeto.
O método GetQuadratureWeights() com ajuda das Bibliotecas ALGLIB calcula e retorna uma matriz de pesos usada para desfocar uma forma desenhada pelo método DrawShadowFigureRect().
O desfoque desta figura é realizado pelo método GaussianBlur().
Todos os métodos serão considerados a seguir.
Na seção pública da classe, declaramos o construtor paramétrico, os métodos que retornam sinalizadores para manter as propriedades do objeto (enquanto ambos os métodos retornam true), o método para desenhar sombra, e vamos escrever métodos de acesso simplificado às propriedades do objeto-sombra:
public: CShadowObj(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h); //--- Supported object properties (1) integer and (2) string ones virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property) { return true; } //--- Draw an object shadow void DrawShadow(const int shift_x,const int shift_y,const uchar blur_value); //+------------------------------------------------------------------+ //| Methods of simplified access to object properties | //+------------------------------------------------------------------+ //--- (1) Set and (2) return the shadow color void SetColorShadow(const color colour) { this.m_color_shadow=colour; } color ColorShadow(void) const { return this.m_color_shadow; } //--- (1) Set and (2) return the shadow opacity void SetOpacityShadow(const uchar opacity) { this.m_opacity_shadow=opacity; } uchar OpacityShadow(void) const { return this.m_opacity_shadow; } }; //+------------------------------------------------------------------+
Vamos dar uma olhada mais de perto na estrutura dos métodos de classe.
Construtor paramétrico:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CShadowObj::CShadowObj(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h) : CGCnvElement(GRAPH_ELEMENT_TYPE_SHADOW_OBJ,chart_id,subwindow,name,x,y,w,h) { CGCnvElement::SetColorBackground(clrNONE); CGCnvElement::SetOpacity(0); CGCnvElement::SetActive(false); this.m_opacity_shadow=127; color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100); this.m_color_shadow=CGCnvElement::ChangeColorLightness(gray,255,-50); this.m_shadow=false; this.m_visible=true; CGCnvElement::Erase(); } //+------------------------------------------------------------------+
Ao construtor são transferidos o identificador do gráfico, o número da subjanela na qual é criado o objeto de sombra, o nome do objeto, as coordenadas do canto superior esquerdo e as dimensões. Na lista de inicialização, ao construtor privado da classe do elemento gráfico transferimos o tipo de elemento (objeto-sombra) e o resto dos parâmetros passados nos argumentos do método.
No corpo do construtor, definimos a cor de fundo do objeto ausente, sua total transparência e o sinalizador de objeto inativo (a sombra do objeto não deve reagir de forma alguma às influências externas sobre ela). Definimos como 127 a opacidade padrão da sombra a ser desenhada na tela - uma sombra translúcida. Em seguida, calculamos a cor de sombra padrão. Esta será a cor de fundo do gráfico, sombreada em 50 unidades em uma centena. Aqui, primeiro convertemos a cor de fundo do gráfico em cinza e, em seguida, escurecemos a cor resultante. O objeto sobre o qual a sombra é desenhada, por sua vez, não deve projetá-la, portanto definimos o sinalizador de sombra como false, o sinalizador de visibilidade do objeto é definido como true e limpamos a tela.
Método que desenha a sombra de um objeto:
//+------------------------------------------------------------------+ //| Draw the object shadow | //+------------------------------------------------------------------+ void CShadowObj::DrawShadow(const int shift_x,const int shift_y,const uchar blur_value) { //--- Calculate the height and width of the drawn rectangle int w=this.Width()-OUTER_AREA_SIZE*2; int h=this.Height()-OUTER_AREA_SIZE*2; //--- Draw a filled rectangle with calculated dimensions this.DrawShadowFigureRect(w,h); //--- Calculate the blur radius, which cannot exceed a quarter of the OUTER_AREA_SIZE constant int radius=(blur_value>OUTER_AREA_SIZE/4 ? OUTER_AREA_SIZE/4 : blur_value); //--- If failed to blur the shape, exit the method (GaussianBlur() displays the error on the journal) if(!this.GaussianBlur(radius)) return; //--- Shift the shadow object by X/Y offsets specified in the method arguments and update the canvas CGCnvElement::Move(this.CoordX()+shift_x,this.CoordY()+shift_y); CGCnvElement::Update(); } //+------------------------------------------------------------------+
Toda a lógica do método é descrita nos comentários ao código. O método primeiro desenha na tela um retângulo regular preenchido com a cor da sombra. A largura e a altura desse retângulo são calculadas para corresponder ao tamanho do objeto-forma que projeta essa sombra. Em seguida, desfocamos o retângulo desenhado usando o método Gaussiano, deslocamos o objeto de sombra em relação ao objeto-forma que projeta essa sombra e atualizamos a tela do objeto-sombra.
Método que desenha a forma da sombra do objeto:
//+------------------------------------------------------------------+ //| Draw the object shadow form | //+------------------------------------------------------------------+ void CShadowObj::DrawShadowFigureRect(const int w,const int h) { CGCnvElement::DrawRectangleFill(OUTER_AREA_SIZE,OUTER_AREA_SIZE,OUTER_AREA_SIZE+w-1,OUTER_AREA_SIZE+h-1,this.m_color_shadow,this.m_opacity_shadow); CGCnvElement::Update(); } //+------------------------------------------------------------------+
Aqui: desenhamos um retângulo nas coordenadas X e Y iguais ao valor da constante OUTER_AREA_SIZE. A segunda coordenada X e Y é calculada como o deslocamento da primeira coordenada + largura (altura) menos 1. Depois de desenhar a forma, a tela é atualizada.
Método para desfocar a forma desenhada de acordo com o Gaussiano:
//+------------------------------------------------------------------+ //| Gaussian blur | //| https://www.mql5.com/en/articles/1612#chapter4 | //+------------------------------------------------------------------+ bool CShadowObj::GaussianBlur(const uint radius) { //--- int n_nodes=(int)radius*2+1; uint res_data[]; // Array for storing graphical resource data uint res_w=this.Width(); // Graphical resource width uint res_h=this.Height(); // Graphical resource height //--- Read graphical resource data. If failed, return false ::ResetLastError(); if(!::ResourceReadImage(this.NameRes(),res_data,res_w,res_h)) { CMessage::OutByID(MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES); return false; } //--- Check the blur amount. If the blur radius exceeds half of the width or height, return 'false' if(radius>=res_w/2 || radius>=res_h/2) { ::Print(DFUN,CMessage::Text(MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE)); return false; } //--- Decompose image data from the resource into a, r, g, b color components int size=::ArraySize(res_data); //--- arrays for storing A, R, G and B color components //--- for horizontal and vertical blur uchar a_h_data[],r_h_data[],g_h_data[],b_h_data[]; uchar a_v_data[],r_v_data[],g_v_data[],b_v_data[]; //--- Change the size of component arrays according to the array size of the graphical resource data if(::ArrayResize(a_h_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"a_h_data\""); return false; } if(::ArrayResize(r_h_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"r_h_data\""); return false; } if(::ArrayResize(g_h_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"g_h_data\""); return false; } if(ArrayResize(b_h_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"b_h_data\""); return false; } if(::ArrayResize(a_v_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"a_v_data\""); return false; } if(::ArrayResize(r_v_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"r_v_data\""); return false; } if(::ArrayResize(g_v_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"g_v_data\""); return false; } if(::ArrayResize(b_v_data,size)==-1) { CMessage::OutByID(MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"b_v_data\""); return false; } //--- Declare the array for storing blur weight ratios and, //--- if failed to get the array of weight ratios, return 'false' double weights[]; if(!this.GetQuadratureWeights(1,n_nodes,weights)) return false; //--- Set components of each image pixel to the color component arrays for(int i=0;i<size;i++) { a_h_data[i]=GETRGBA(res_data[i]); r_h_data[i]=GETRGBR(res_data[i]); g_h_data[i]=GETRGBG(res_data[i]); b_h_data[i]=GETRGBB(res_data[i]); } //--- Blur the image horizontally (along the X axis) uint XY; // Pixel coordinate in the array double a_temp=0.0,r_temp=0.0,g_temp=0.0,b_temp=0.0; int coef=0; int j=(int)radius; //--- Loop by the image width for(uint Y=0;Y<res_h;Y++) { //--- Loop by the image height for(uint X=radius;X<res_w-radius;X++) { XY=Y*res_w+X; a_temp=0.0; r_temp=0.0; g_temp=0.0; b_temp=0.0; coef=0; //--- Multiply each color component by the weight ratio corresponding to the current image pixel for(int i=-1*j;i<j+1;i=i+1) { a_temp+=a_h_data[XY+i]*weights[coef]; r_temp+=r_h_data[XY+i]*weights[coef]; g_temp+=g_h_data[XY+i]*weights[coef]; b_temp+=b_h_data[XY+i]*weights[coef]; coef++; } //--- Save each rounded color component calculated according to the ratios to the component arrays a_h_data[XY]=(uchar)::round(a_temp); r_h_data[XY]=(uchar)::round(r_temp); g_h_data[XY]=(uchar)::round(g_temp); b_h_data[XY]=(uchar)::round(b_temp); } //--- Remove blur artifacts to the left by copying adjacent pixels for(uint x=0;x<radius;x++) { XY=Y*res_w+x; a_h_data[XY]=a_h_data[Y*res_w+radius]; r_h_data[XY]=r_h_data[Y*res_w+radius]; g_h_data[XY]=g_h_data[Y*res_w+radius]; b_h_data[XY]=b_h_data[Y*res_w+radius]; } //--- Remove blur artifacts to the right by copying adjacent pixels for(uint x=res_w-radius;x<res_w;x++) { XY=Y*res_w+x; a_h_data[XY]=a_h_data[(Y+1)*res_w-radius-1]; r_h_data[XY]=r_h_data[(Y+1)*res_w-radius-1]; g_h_data[XY]=g_h_data[(Y+1)*res_w-radius-1]; b_h_data[XY]=b_h_data[(Y+1)*res_w-radius-1]; } } //--- Blur vertically (along the Y axis) the image already blurred horizontally int dxdy=0; //--- Loop by the image height for(uint X=0;X<res_w;X++) { //--- Loop by the image width for(uint Y=radius;Y<res_h-radius;Y++) { XY=Y*res_w+X; a_temp=0.0; r_temp=0.0; g_temp=0.0; b_temp=0.0; coef=0; //--- Multiply each color component by the weight ratio corresponding to the current image pixel for(int i=-1*j;i<j+1;i=i+1) { dxdy=i*(int)res_w; a_temp+=a_h_data[XY+dxdy]*weights[coef]; r_temp+=r_h_data[XY+dxdy]*weights[coef]; g_temp+=g_h_data[XY+dxdy]*weights[coef]; b_temp+=b_h_data[XY+dxdy]*weights[coef]; coef++; } //--- Save each rounded color component calculated according to the ratios to the component arrays a_v_data[XY]=(uchar)::round(a_temp); r_v_data[XY]=(uchar)::round(r_temp); g_v_data[XY]=(uchar)::round(g_temp); b_v_data[XY]=(uchar)::round(b_temp); } //--- Remove blur artifacts at the top by copying adjacent pixels for(uint y=0;y<radius;y++) { XY=y*res_w+X; a_v_data[XY]=a_v_data[X+radius*res_w]; r_v_data[XY]=r_v_data[X+radius*res_w]; g_v_data[XY]=g_v_data[X+radius*res_w]; b_v_data[XY]=b_v_data[X+radius*res_w]; } //--- Remove blur artifacts at the bottom by copying adjacent pixels for(uint y=res_h-radius;y<res_h;y++) { XY=y*res_w+X; a_v_data[XY]=a_v_data[X+(res_h-1-radius)*res_w]; r_v_data[XY]=r_v_data[X+(res_h-1-radius)*res_w]; g_v_data[XY]=g_v_data[X+(res_h-1-radius)*res_w]; b_v_data[XY]=b_v_data[X+(res_h-1-radius)*res_w]; } } //--- Set the twice blurred (horizontally and vertically) image pixels to the graphical resource data array for(int i=0;i<size;i++) res_data[i]=ARGB(a_v_data[i],r_v_data[i],g_v_data[i],b_v_data[i]); //--- Display the image pixels on the canvas in a loop by the image height and width from the graphical resource data array for(uint X=0;X<res_w;X++) { for(uint Y=radius;Y<res_h-radius;Y++) { XY=Y*res_w+X; CGCnvElement::GetCanvasObj().PixelSet(X,Y,res_data[XY]); } } //--- Done return true; } //+------------------------------------------------------------------+
A lógica do método é descrita nos comentários ao código. Mais sobre isso pode ser lido no artigo do qual foi retirado o método.
Método que retorna uma matriz de coeficientes corretores:
//+------------------------------------------------------------------+ //| Return the array of weight ratios | //| https://www.mql5.com/en/articles/1612#chapter3_2 | //+------------------------------------------------------------------+ bool CShadowObj::GetQuadratureWeights(const double mu0,const int n,double &weights[]) { CAlglib alglib; double alp[]; double bet[]; ::ArrayResize(alp,n); ::ArrayResize(bet,n); ::ArrayInitialize(alp,1.0); ::ArrayInitialize(bet,1.0); //--- double out_x[]; int info=0; alglib.GQGenerateRec(alp,bet,mu0,n,info,out_x,weights); if(info!=1) { string txt=(info==-3 ? "internal eigenproblem solver hasn't converged" : info==-2 ? "Beta[i]<=0" : "incorrect N was passed"); ::Print("Call error in CGaussQ::GQGenerateRec: ",txt); return false; } return true; } //+------------------------------------------------------------------+
Usando a Biblioteca de análise numérica ALGLIB o método calcula os coeficientes corretores de desfoque e os grava na matriz weights passada a ele por referência. Você pode ler mais sobre o método nesta seção do artigo.
Assim concluímos a criação da primeira versão da classe do objeto sombra.
Agora precisamos tornar possível criar e desenhar rapidamente uma sombra diretamente a partir do objeto-forma.
Abrimos o arquivo \MQL5\Include\DoEasy\Objects\Graph\Form.mqh da classe do objeto-forma e fazemos as alterações necessárias.
Para fazer a classe do objeto-forma ver a classe de objeto-sombra, integramos o arquivo da classe sombra recém-criada:
//+------------------------------------------------------------------+ //| Form.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "GCnvElement.mqh" #include "ShadowObj.mqh" //+------------------------------------------------------------------+ //| Form object class | //+------------------------------------------------------------------+
Da seção privada da classe removemos a variável que armazena a cor da sombra da forma:
color m_color_shadow; // Form shadow color
A cor da sombra agora está armazenada na classe do objeto de sombra.
Como resultado, nosso objeto-forma permitirá criar novos objetos de elementos gráficos diretamente dentro de si mesma e anexá-los à sua lista de objetos dependentes. Isso quer dizer que esses objetos recém-criados dependerão completamente e pertencerão ao objeto-forma. O objeto-forma será capaz de manipulá-los. Para criar tais objetos, também precisaremos criar seus nomes, que devem conter o nome do objeto-forma com a adição de seu próprio nome no final. Para fazer isso, à seção privada da classe adicionamos o método que cria o nome do objeto dependente:
//--- Initialize the variables void Initialize(void); //--- Return the name of the dependent object string CreateNameDependentObject(const string base_name) const { return ::StringSubstr(this.NameObj(),::StringLen(::MQLInfoString(MQL_PROGRAM_NAME))+1)+"_"+base_name; } //--- Create a new graphical object
No último artigo, ao descrever o objeto-forma, já fizemos algo assim para criar o nome para o objeto:
... extraímos a terminação do nome do objeto (o nome consiste no nome do programa e no nome do objeto quando foi criado). Precisamos extrair o nome do objeto quando ele é criado e adicionar o nome passado ao método para ele.
Assim, a partir, por exemplo, do nome "Program_name_Form01" nós extraímos a substring "Form01" e adicionamos o nome passado ao método a esta string. Se criarmos um objeto de sombra e passarmos o nome "Sombra", o nome do objeto será "Form01_Sombra" e o nome final do objeto, "Program_name_Form01_Sombra".
Agora, isso será feito em um método separado, pois será necessário mais de uma vez.
Na seção privada, declaramos um método para criar o objeto-sombra:
//--- Create a new graphical object CGCnvElement *CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const int element_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity); //--- Create a shadow object void CreateShadowObj(const color colour,const uchar opacity); public:
Da seção pública da classe removemos a declaração deste método:
//--- Create a new attached element bool CreateNewElement(const int element_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity); //--- Create a shadow object void CreateShadow(const uchar opacity); //--- Draw an object shadow
Agora, esse método não estará disponível publicamente, e a cor da sombra junto com sua opacidade serão adicionalmente transferidas para ele.
O método público que desenha a sombra do objeto agora também terá mais argumentos:
//--- Create a new attached element bool CreateNewElement(const int element_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity); //--- Draw an object shadow void DrawShadow(const int shift_x,const int shift_y,const color colour,const uchar opacity=127,const uchar blur=4); //--- Draw the form frame
Isso é feito para que, em vez da criação preliminar do objeto de sombra, e já após sua renderização, possamos chamar imediatamente o método de desenho de sombra. A lógica aqui é simples - se chamarmos o método para desenhar a sombra, então precisaremos dele. E se ainda não tivermos criado um objeto de sombra, o novo método primeiro criará esse objeto e, em seguida, desenhará uma sombra sobre ele e exibirá na tela.
No bloco de métodos para acesso simplificado às propriedades do objeto removemos a implementação dos métodos para definir e retornar a cor da sombra:
//+------------------------------------------------------------------+ //| Methods of simplified access to object properties | //+------------------------------------------------------------------+ //--- (1) Set and (2) get the form frame color void SetColorFrame(const color colour) { this.m_color_frame=colour; } color ColorFrame(void) const { return this.m_color_frame; } //--- (1) Set and (2) return the form shadow color void SetColorShadow(const color colour) { this.m_color_shadow=colour; } color ColorShadow(void) const { return this.m_color_shadow; }
Agora, esses métodos serão movidos para fora do corpo da classe (é necessário verificar a presença de objeto-sombra), e apenas resta sua declaração, bem como adicionar a declaração de métodos para definir e retornar a opacidade da sombra:
//+------------------------------------------------------------------+ //| Methods of simplified access to object properties | //+------------------------------------------------------------------+ //--- (1) Set and (2) get the form frame color void SetColorFrame(const color colour) { this.m_color_frame=colour; } color ColorFrame(void) const { return this.m_color_frame; } //--- (1) Set and (2) return the form shadow color void SetColorShadow(const color colour); color ColorShadow(void) const; //--- (1) Set and (2) return the form shadow opacity void SetOpacityShadow(const uchar opacity); uchar OpacityShadow(void) const; }; //+------------------------------------------------------------------+
No método para criar um novo elemento gráfico substituímos essas strings
int pos=::StringLen(::MQLInfoString(MQL_PROGRAM_NAME));
string pref=::StringSubstr(NameObj(),pos+1);
string name=pref+"_"+obj_name;
chamando o método para criar o nome do objeto dependente:
//+------------------------------------------------------------------+ //| Create a new graphical object | //+------------------------------------------------------------------+ CGCnvElement *CForm::CreateNewGObject(const ENUM_GRAPH_ELEMENT_TYPE type, const int obj_num, const string obj_name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable, const bool activity) { string name=this.CreateNameDependentObject(obj_name); CGCnvElement *element=new CGCnvElement(type,this.ID(),obj_num,this.ChartID(),this.SubWindow(),name,x,y,w,h,colour,opacity,movable,activity); if(element==NULL) ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),": ",name); return element; } //+------------------------------------------------------------------+
Depois de criar o objeto-sombra, este precisa dos parâmetros padrão imediatamente.
Por isso, modificamos um pouco o método para criar o objeto-sombra:
//+------------------------------------------------------------------+ //| Create the shadow object | //+------------------------------------------------------------------+ void CForm::CreateShadowObj(const color colour,const uchar opacity) { //--- If the shadow flag is disabled or the shadow object already exists, exit if(!this.m_shadow || this.m_shadow_obj!=NULL) return; //--- Calculate the shadow object coordinates according to the offset from the top and left int x=this.CoordX()-OUTER_AREA_SIZE; int y=this.CoordY()-OUTER_AREA_SIZE; //--- Calculate the width and height in accordance with the top, bottom, left and right offsets int w=this.Width()+OUTER_AREA_SIZE*2; int h=this.Height()+OUTER_AREA_SIZE*2; //--- Create a new shadow object and set the pointer to it in the variable this.m_shadow_obj=new CShadowObj(this.ChartID(),this.SubWindow(),this.CreateNameDependentObject("Shadow"),x,y,w,h); if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ)); return; } //--- Set the properties for the created shadow object this.m_shadow_obj.SetID(this.ID()); this.m_shadow_obj.SetNumber(-1); this.m_shadow_obj.SetOpacityShadow(opacity); this.m_shadow_obj.SetColorShadow(colour); this.m_shadow_obj.SetMovable(true); this.m_shadow_obj.SetActive(false); this.m_shadow_obj.SetVisible(false); //--- Move the form object to the foreground this.BringToTop(); } //+------------------------------------------------------------------+
Alteramos o método que desenha a sombra de modo que, na ausência de um objeto-sombra, ele seja criado e, em seguida, uma sombra sobre ele:
//+------------------------------------------------------------------+ //| Draw the shadow | //+------------------------------------------------------------------+ void CForm::DrawShadow(const int shift_x,const int shift_y,const color colour,const uchar opacity=127,const uchar blur=4) { //--- If the shadow flag is disabled, exit if(!this.m_shadow) return; //--- If there is no shadow object, create it if(this.m_shadow_obj==NULL) this.CreateShadowObj(colour,opacity); //--- If the shadow object exists, draw the shadow on it, //--- set the shadow object visibility flag and //--- move the form object to the foreground if(this.m_shadow_obj!=NULL) { this.m_shadow_obj.DrawShadow(shift_x,shift_y,blur); this.m_shadow_obj.SetVisible(true); this.BringToTop(); } } //+------------------------------------------------------------------+
A lógica do método é descrita nos comentários ao código e não deve complicar as coisas.
No método de configuração do esquema de cores, adicionamos um sinalizador de seleção de uso da sombra e de presença do objeto-sombra criado antes de definir o objeto de sombra com sua cor de desenho:
//+------------------------------------------------------------------+ //| Set a color scheme | //+------------------------------------------------------------------+ void CForm::SetColorTheme(const ENUM_COLOR_THEMES theme,const uchar opacity) { this.SetOpacity(opacity); this.SetColorBackground(array_color_themes[theme][COLOR_THEME_COLOR_FORM_BG]); this.SetColorFrame(array_color_themes[theme][COLOR_THEME_COLOR_FORM_FRAME]); if(this.m_shadow && this.m_shadow_obj!=NULL) this.SetColorShadow(array_color_themes[theme][COLOR_THEME_COLOR_FORM_SHADOW]); } //+------------------------------------------------------------------+
No método que define o estilo da forma, adicionamos um novo parâmetro de entrada que indica que a cor de fundo do gráfico deve ser usada para criar a cor da sombra, e adicionamos o desenho de sombra:
//+------------------------------------------------------------------+ //| Set the form style | //+------------------------------------------------------------------+ void CForm::SetFormStyle(const ENUM_FORM_STYLE style, const ENUM_COLOR_THEMES theme, const uchar opacity, const bool shadow=false, const bool use_bg_color=true, const bool redraw=false) { //--- Set opacity parameters and the size of the form frame side this.m_shadow=shadow; this.m_frame_width_top=array_form_style[style][FORM_STYLE_FRAME_WIDTH_TOP]; this.m_frame_width_bottom=array_form_style[style][FORM_STYLE_FRAME_WIDTH_BOTTOM]; this.m_frame_width_left=array_form_style[style][FORM_STYLE_FRAME_WIDTH_LEFT]; this.m_frame_width_right=array_form_style[style][FORM_STYLE_FRAME_WIDTH_RIGHT]; //--- Create the shadow object this.CreateShadowObj(clrNONE,(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_OPACITY]); //--- Set a color scheme this.SetColorTheme(theme,opacity); //--- Calculate a shadow color with color darkening color clr=array_color_themes[theme][COLOR_THEME_COLOR_FORM_SHADOW]; color gray=CGCnvElement::ChangeColorSaturation(ChartColorBackground(),-100); color color_shadow=CGCnvElement::ChangeColorLightness((use_bg_color ? gray : clr),-fabs(array_form_style[style][FORM_STYLE_DARKENING_COLOR_FOR_SHADOW])); this.SetColorShadow(color_shadow); //--- Draw a rectangular shadow int shift_x=array_form_style[style][FORM_STYLE_FRAME_SHADOW_X_SHIFT]; int shift_y=array_form_style[style][FORM_STYLE_FRAME_SHADOW_Y_SHIFT]; this.DrawShadow(shift_x,shift_y,color_shadow,this.OpacityShadow(),(uchar)array_form_style[style][FORM_STYLE_FRAME_SHADOW_BLUR]); //--- Fill in the form background with color and opacity this.Erase(this.ColorBackground(),this.Opacity()); //--- Depending on the selected form style, draw the corresponding form frame and the outer bounding frame switch(style) { case FORM_STYLE_BEVEL : this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_BEVEL); break; //---FORM_STYLE_FLAT default: this.DrawFormFrame(this.m_frame_width_top,this.m_frame_width_bottom,this.m_frame_width_left,this.m_frame_width_right,this.ColorFrame(),this.Opacity(),FRAME_STYLE_FLAT); break; } this.DrawRectangle(0,0,Width()-1,Height()-1,array_color_themes[theme][COLOR_THEME_COLOR_FORM_RECT_OUTER],this.Opacity()); } //+------------------------------------------------------------------+
A lógica do método é descrita nos comentários. Resumindo: primeiro criamos um objeto de sombra. Após configurar a paleta de cores, calculamos a cor para desenhar a sombra. Se o sinalizador para usar a cor de fundo estiver definido, para desenhar a sombra usaremos a cor de fundo do gráfico, convertida para monocromática e escurecida pelo valor do parâmetro escurecimento, escrito no estilo de formulário no arquivo GraphINI.mqh. Se o sinalizador não estiver definido, usaremos a cor sombreada da mesma forma, definida nos esquemas de cores das formas no arquivo GraphINI.mqh. Em seguida, chamamos o método para desenhar uma sombra, método esse que desenhará uma sombra apenas se o sinalizador de sombra no objeto de forma estiver definido.
Em todos os métodos onde é usado iluminar/escurecer bordas de formas, substituímos os valores especificados em números reais
//--- Darken the horizontal sides of the frame for(int i=0;i<width;i++) { this.m_canvas.PixelSet(x+i,y,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y),-0.05)); this.m_canvas.PixelSet(x+i,y+height-1,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y+height-1),-0.07)); }
pelos seus valores inteiros correspondentes, mas cem vezes maior (nos métodos chamados nestas linhas, inserimos a divisão do valor passado para eles por 100):
//--- Darken the horizontal sides of the frame for(int i=0;i<width;i++) { this.m_canvas.PixelSet(x+i,y,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y),-5)); this.m_canvas.PixelSet(x+i,y+height-1,CGCnvElement::ChangeColorLightness(this.GetPixel(x+i,y+height-1),-7)); }
Isso já foi feito em todos os métodos em que foi necessário substituir esses valores, e não vamos repetir o mesmo - os códigos podem ser encontrados nos arquivos anexados ao artigo.
Método para definir a cor da sombra da forma:
//+------------------------------------------------------------------+ //| Set the form shadow color | //+------------------------------------------------------------------+ void CForm::SetColorShadow(const color colour) { if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT)); return; } this.m_shadow_obj.SetColorShadow(colour); } //+------------------------------------------------------------------+
Aqui, primeiro verificamos a existência do objeto-sombra e, apenas se houver um, definimos sua cor de sombra. Caso contrário, imprimimos uma mensagem no log sobre a ausência do objeto-sombra e a proposta de criá-lo primeiro.
Método que retorna a cor da sombra da forma:
//+------------------------------------------------------------------+ //| Return the form shadow color | //+------------------------------------------------------------------+ color CForm::ColorShadow(void) const { if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT)); return clrNONE; } return this.m_shadow_obj.ColorShadow(); } //+------------------------------------------------------------------+
Aqui, da mesma forma, primeiro verificamos a existência do objeto, e só então retornamos a sua cor de sombra.
Métodos para definir e retornar a opacidade da sombra:
//+------------------------------------------------------------------+ //| Set the form shadow opacity | //+------------------------------------------------------------------+ void CForm::SetOpacityShadow(const uchar opacity) { if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT)); return; } this.m_shadow_obj.SetOpacityShadow(opacity); } //+------------------------------------------------------------------+ //| Return the form shadow opacity | //+------------------------------------------------------------------+ uchar CForm::OpacityShadow(void) const { if(this.m_shadow_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT)); return 0; } return this.m_shadow_obj.OpacityShadow(); } //+------------------------------------------------------------------+
A lógica desses métodos é idêntica aos dois acima.
Agora estamos prontos para testar a criação do objeto-sombra para as formas.
Teste
Vamos verificar a criação de sombras para objetos-formas. Duas formas serão criadas com parâmetros escritos em estilos de formas e esquemas de cores (tudo o que fizemos no último artigo), e a terceira forma será criada "à mão", que será outro exemplo de como desenhar a forma. Como os objetos de sombra para formas são desenhados após a criação da própria forma, iremos verificar qual dos objetos reage ao clique do mouse: se o objeto-forma é mais alto do que o objeto no qual sua sombra é desenhada, o clique na forma irá mostrar seu nome no log. Se o objeto de sombra ainda for mais alto do que a forma, então, no log, veremos o nome do objeto de sombra da forma.
Para teste pegamos no Expert Advisor do último artigo e o salvamos na nova pasta \MQL5\Experts\TestDoEasy\Part77\ com o novo nome TestDoEasyPart77.mq5.
À lista de parâmetros de entrada do Expert Advisor adicionamos uma configuração para escolher a cor de sombra - a cor de fundo do gráfico ou a cor especificada que pode ser definida no seguinte parâmetro de entrada. À lista de variáveis globais adicionamos uma matriz que irá armazenar as cores para preencher a forma com um gradiente:
//+------------------------------------------------------------------+ //| TestDoEasyPart77.mq5 | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" //--- includes #include <Arrays\ArrayObj.mqh> #include <DoEasy\Services\Select.mqh> #include <DoEasy\Objects\Graph\Form.mqh> //--- defines #define FORMS_TOTAL (3) // Number of created forms //--- input parameters sinput bool InpMovable = true; // Movable forms flag sinput ENUM_INPUT_YES_NO InpUseColorBG = INPUT_YES; // Use chart background color to calculate shadow color sinput color InpColorForm3 = clrCadetBlue; // Third form shadow color (if not background color) //--- global variables CArrayObj list_forms; color array_clr[]; //+------------------------------------------------------------------+
No manipulador OnInit() adicionamos a criação do terceiro objeto-forma:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set the permissions to send cursor movement and mouse scroll events ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_MOVE,true); ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_WHEEL,true); //--- Set EA global variables ArrayResize(array_clr,2); array_clr[0]=C'26,100,128'; // Original ≈Dark-azure color array_clr[1]=C'35,133,169'; // Lightened original color //--- Create the specified number of form objects list_forms.Clear(); int total=FORMS_TOTAL; for(int i=0;i<total;i++) { //--- When creating an object, pass all the required parameters to it CForm *form=new CForm("Form_0"+(string)(i+1),300,40+(i*80),100,(i<2 ? 70 : 30)); if(form==NULL) continue; //--- Set activity and moveability flags for the form form.SetActive(true); form.SetMovable(false); //--- Set the form ID equal to the loop index and the index in the list of objects form.SetID(i); form.SetNumber(0); // (0 - main form object) Auxiliary objects may be attached to the main one. The main object is able to manage them //--- Set the partial opacity for the middle form and the full one for the rest uchar opacity=(i==1 ? 250 : 255); //--- Set the form style and its color theme depending on the loop index if(i<2) { ENUM_FORM_STYLE style=(ENUM_FORM_STYLE)i; ENUM_COLOR_THEMES theme=(ENUM_COLOR_THEMES)i; //--- Set the form style and theme form.SetFormStyle(style,theme,opacity,true,false); } //--- If this is the first (top) form if(i==0) { //--- Draw a concave field slightly shifted from the center of the form downwards form.DrawFieldStamp(3,10,form.Width()-6,form.Height()-13,form.ColorBackground(),form.Opacity()); form.Update(true); } //--- If this is the second (middle) form if(i==1) { //--- Draw a concave semi-transparent "tainted glass" field in the center form.DrawFieldStamp(10,10,form.Width()-20,form.Height()-20,clrWheat,200); form.Update(true); } //--- If this is the third (bottom) form if(i==2) { //--- Set the opacity of 200 form.SetOpacity(200); //--- The form background color is set as the first color from the color array form.SetColorBackground(array_clr[0]); //--- Form outlining frame color form.SetColorFrame(clrDarkBlue); //--- Draw the shadow drawing flag form.SetShadow(true); //--- Calculate the shadow color as the chart background color converted to the monochrome one color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100); //--- If the settings specify the usage of the chart background color, replace the monochrome color with 20 units //--- Otherwise, use the color specified in the settings for drawing the shadow color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,255,-20) : InpColorForm3); //--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes //--- Set the shadow opacity to 200, while the blur radius is equal to 4 form.DrawShadow(3,3,clr,200,4); //--- Fill the form background with a vertical gradient form.Erase(array_clr,form.Opacity()); //--- Draw an outlining rectangle at the edges of the form form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity()); //--- Display the text describing the gradient type and update the form form.Text(form.Width()/2,form.Height()/2,TextByLanguage("V-Градиент","V-Gradient"),C'211,233,149',255,TEXT_ANCHOR_CENTER); form.Update(true); } //--- Add objects to the list if(!list_forms.Add(form)) { delete form; continue; } } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Toda a lógica para a criação de uma forma é escrita nos comentários do código. Este é outro exemplo de como você pode criar seus próprios objetos-forma.
No manipulador OnChartEvent() inserimos a exibição, no log, do nome do objeto gráfico ao clicar nele:
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- If clicking on an object if(id==CHARTEVENT_OBJECT_CLICK) { Print(sparam); } } //+------------------------------------------------------------------+
Vamos compilar o Expert Advisor, executá-lo no gráfico e alterar as configurações de sombra padrão:
Infelizmente, a imagem GIF não permite ver toda a paleta de cores.
Esta é a aparência de uma forma com um fundo gradiente em formato PNG:
Ao clicar em cada uma das formas, no log são exibidos seus nomes, não seus objetos-sombras:
TestDoEasyPart77_Form_01 TestDoEasyPart77_Form_02 TestDoEasyPart77_Form_03
Isso nos diz que o objeto de sombra após sua criação a partir do objeto de forma ainda se move para o fundo para não "interferir" no trabalho com a forma criada.
O que vem agora?
No próximo artigo, continuaremos o desenvolvimento da classe do objeto-forma e começaremos a "animar" gradualmente nossas imagens estáticas.
Todos os arquivos da versão atual da biblioteca e o arquivo do EA de teste para MQL5 estão anexados abaixo. Você pode baixá-los e testar tudo sozinho.
Se você tiver perguntas, comentários e sugestões, poderá expressá-los nos comentários do artigo.
*Artigos desta série:
Gráficos na biblioteca DoEasy (Parte 73): objeto-forma de um elemento gráfico
Gráficos na biblioteca DoEasy (Parte 74): elemento gráfico básico baseado na classe CCanvas
Gráficos na biblioteca DoEasy (Parte 75): métodos para trabalhar com primitivas e texto num elemento gráfico básico
Gráficos na biblioteca DoEasy (Parte 76): objeto Forma e temas de cores predefinidos
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/9575
- 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