Gráficos na biblioteca DoEasy (Parte 77): classe do objeto Sombra

Artyom Trishkin | 19 agosto, 2021

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:

  1. diretamente na tela do próprio objeto-forma,
  2. 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)                       // Размер одной стороны внешней области вокруг рабочего пространства
//+------------------------------------------------------------------+

à 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.

Complementos

*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