English Русский 中文 Español Deutsch 日本語
Gráficos na Biblioteca DoEasy (Parte 78): princípios de animação dentro da biblioteca Corte de imagens

Gráficos na Biblioteca DoEasy (Parte 78): princípios de animação dentro da biblioteca Corte de imagens

MetaTrader 5Exemplos | 6 setembro 2021, 08:28
596 0
Artyom Trishkin
Artyom Trishkin

Sumário


Ideia

Qualquer interface gráfica a priori implica a presença de imagens não estáticas. Por exemplo, os dados exibidos em tabelas podem mudar ao longo do tempo, os elementos da interface gráfica podem reagir à interação do usuário, que pode querer, entre outras coisas, usar efeitos visuais variados, etc.

Em nossa biblioteca, também criaremos métodos para gerar diversos efeitos visuais, equiparemos a biblioteca da capacidade de trabalhar com animação de sprites. Esta última é baseada no uso de uma sequência mutável de imagens estáticas - quadro a quadro, como num filme.
A classe CCanvas permite desenhar imagens na tela. A partir de uma série de imagens desenhadas e salvas em matrizes, podemos construir uma determinada sequência que acabará por ser uma imagem animada. Mas se apenas desenharmos cada imagem na tela, uma por uma, elas simplesmente se sobreporão, originando uma sobreposição caótica de pixels de imagens diferentes, como, por exemplo, nesta imagem (nesse caso, simplesmente exibimos o texto em diferentes locais do objeto-forma):


Para evitar que isso aconteça, antes de exibir o texto na tela, primeiro precisamos apagar completamente a imagem anterior, redesenhar o plano de fundo e, em seguida, exibir o texto nele (como fizemos num dos artigos anteriores - colocamos o texto numa forma que exibia o método de âncora de texto). Essa ideia é sólida, mas apenas enquanto o tamanho e a complexidade da forma redesenhada forem pequenos. Na verdade, também podemos: pré-salvar na memória (na matriz) aquela parte do fundo na qual iremos sobrepor o texto, sobrepor o texto, e depois, quando for necessário mover o texto para novas coordenadas, apagar na matriz o texto desenhado com a imagem de fundo salva anteriormente (isso restaurará o fundo) e, em seguida, desenhar o texto num novo local (antes disso, salvando na matriz parte do fundo do local aonde iremos mover o texto). Assim, lembraremos constantemente o fundo da parte da forma onde a imagem será sobreposta e, em seguida, restaurá-lo-emos se a imagem precisar ser alterada.

Esta é a unidade básica de ações por trás da ideia de animação de sprites que iremos gerar na biblioteca:

  1. Salvamos o fundo nas coordenadas desejadas
  2. Exibimos a imagem nessas coordenadas
  3. Ao redesenhar a imagem, restauramos o fundo (que apagará a imagem desenhada)

Para isso, hoje criaremos uma pequena classe na qual: serão armazenadas as coordenadas/dimensões da imagem, será criado um método para salvar numa matriz uma parte da imagem de fundo com essas coordenadas e com as dimensões especificadas. Também precisaremos de um segundo método que restaurará o fundo salvo na matriz (neste caso, as dimensões e coordenadas já estarão salvas nas variáveis de classe quando o fundo for salvo na matriz).

Para que criar uma classe em vez de dois métodos desse tipo para um objeto-forma? A resposta é simples, caso precisemos exibir apenas um texto ou uma imagem animada, então, sim, bastarão esses dois métodos. Mas se precisarmos exibir vários textos em diferentes locais da forma, já a classe será mais útil, uma vez que para cada imagem animada específica, teremos nossas próprias instâncias da classe que podrão ser controladas separadamente.

Dada abordagem nos permitirá desenhar algo a partir da imagem já desenhada no fundo. Para isso, salvaremos não apenas o fundo, mas também a imagem desenhada nele, que por sua vez poderá ser removida dele.

Com base nesta ideia, criaremos uma classe para gerar, armazenar e exibir animações de sprites no objeto-forma - cada instância da classe conterá uma sequência de imagens que poderão ser adicionadas dinamicamente a uma lista, manipuladas por imagens, etc.


Aprimorando as classes da biblioteca

Como de costume, no início do arquivo \MQL5\Include\DoEasy\Data.mqh adicionamos os índices das novas mensagens:

//--- 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

//--- CGCnvElement
   MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY,                  // Error! Empty array
   
//--- 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
   MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ,          // Failed to create new pixel copier object
   MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST,            // Pixel copier object with ID already present in the list 
   MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST,             // No pixel copier object with ID in the list 

//--- CShadowObj
   MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE,               // Error! Image size too small or blur too extensive
   
  };
//+------------------------------------------------------------------+

e os textos que correspondem aos índices recém-adicionados:

//--- 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"},

//--- CGCnvElement
   {"Ошибка! Пустой массив","Error! Empty array"},

//--- CForm
   {"Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()","There is no shadow object. You must first create it using the CreateShadowObj () method"},
   {"Не удалось создать новый объект для тени","Failed to create new object for shadow"},
   {"Не удалось создать новый объект-копировщик пикселей","Failed to create new pixel copier object"},
   {"В списке уже есть объект-копировщик пикселей с идентификатором ","There is already a pixel copier object in the list with ID "},
   {"В списке нет объекта-копировщика пикселей с идентификатором ","No pixel copier object with ID "},
   
//--- CShadowObj
   {"Ошибка! Размер изображения очень маленький или очень большое размытие","Error! Image size is very small or very large blur"},
      
  };
//+---------------------------------------------------------------------+


Visto que, mais tarde, desenharemos imagens ou texto (em objetos-formas prontos herdados quer de um objeto-elemento gráfico quer de outros objetos das nossas interfaces gráficas), sempre precisaremos dispor da aparência inicial do objeto em questão para que a qualquer momento possamos restaurá-lo à sua forma original.
Claro, podemos redesenhá-lo novamente, mas será muito mais rápido apenas copiar uma matriz para outra.

Para fazer isso, no arquivo \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh da classe do objeto-elemento gráfico devemos fazer algumas alterações.

Na seção protegida da classe declaramos a matriz em que serão salvos - logo sua criação - todos os pixels do objeto inicial (sua aparência), bem como um método que salvará o recurso gráfico da instância da classe CCanvas nesta matriz:

//+------------------------------------------------------------------+
//| Class of the graphical element object                            |
//+------------------------------------------------------------------+
class CGCnvElement : public CGBaseObj
  {
protected:
   CCanvas           m_canvas;                                 // CCanvas class object
   CPause            m_pause;                                  // Pause class object
   bool              m_shadow;                                 // Shadow presence
   color             m_chart_color_bg;                         // Chart background color
   uint              m_data_array[];                           // Array for storing resource data copy
//--- Return the cursor position relative to the (1) entire element and (2) the element's active area
   bool              CursorInsideElement(const int x,const int y);
   bool              CursorInsideActiveArea(const int x,const int y);
//--- Create (1) the object structure and (2) the object from the structure
   virtual bool      ObjectToStruct(void);
   virtual void      StructToObject(void);
   
//--- Save the graphical resource to the array
   bool              ResourceCopy(const string source);

private:

Assim, sacrificando uma pequena quantidade de memória, podemos, se necessário, restaurar rapidamente a aparência de qualquer elemento da interface do programa à sua forma original, simplesmente copiando uma matriz para outra.

Para sabermos sempre em que coordenadas foi exibido o último texto desenhado (o que nos tornará mais fácil encontrar as coordenadas em que necessitamos inserir o fundo inicial apagado por este texto), na secção privada da classe vamos declarar dois variáveis para armazenar as coordenadas X e as coordenadas Y do último texto desenhado:

   long              m_long_prop[ORDER_PROP_INTEGER_TOTAL];    // Integer properties
   double            m_double_prop[ORDER_PROP_DOUBLE_TOTAL];   // Real properties
   string            m_string_prop[ORDER_PROP_STRING_TOTAL];   // String properties
   
   ENUM_TEXT_ANCHOR  m_text_anchor;                            // Current text alignment
   int               m_text_x;                                 // Text last X coordinate
   int               m_text_y;                                 // Text last Y coordinate
   color             m_color_bg;                               // Element background color
   uchar             m_opacity;                                // Element opacity
   
//--- Return the index of the array the order's (1) double and (2) string properties are located at


Na seção pública da classe, escrevemos um método que retorne um ponteiro para a instância atual da classe e declaramos um método para salvar a imagem na matriz especificada:

//--- Return the flag of the object supporting this property
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property)          { return true;    }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_DOUBLE property)           { return false;   }
   virtual bool      SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property)           { return true;    }

//--- Return itself
   CGCnvElement     *GetObject(void)                                                   { return &this;   }

//--- Compare CGCnvElement objects with each other by all possible properties (for sorting the lists by a specified object property)
   virtual int       Compare(const CObject *node,const int mode=0) const;
//--- Compare CGCnvElement objects with each other by all properties (to search equal objects)
   bool              IsEqual(CGCnvElement* compared_obj) const;

//--- (1) Save the object to file and (2) upload the object from the file
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   
//--- Create the element
   bool              Create(const long chart_id,
                            const int wnd_num,
                            const string name,
                            const int x,
                            const int y,
                            const int w,
                            const int h,
                            const color colour,
                            const uchar opacity,
                            const bool redraw=false);
                                
//--- Return the pointer to a canvas object
   CCanvas          *GetCanvasObj(void)                                                { return &this.m_canvas;                        }
//--- Set the canvas update frequency
   void              SetFrequency(const ulong value)                                   { this.m_pause.SetWaitingMSC(value);            }
//--- Update the coordinates (shift the canvas)
   bool              Move(const int x,const int y,const bool redraw=false);

//--- Save an image to the array
   bool              ImageCopy(const string source,uint &array[]);
   

O método que permite a uma classe retornar um ponteiro para si mesma será necessário para que o ponteiro para essa classe possa ser transferido para a classe-copiador de pixeis que consideraremos a seguir. Além disso, será necessário um método que replique o recurso gráfico da instância CCanvas para copiar rapidamente a aparência da forma para a matriz dentro de qualquer programa baseado nesta biblioteca.

No bloco de código de métodos para trabalhar com texto, adicionamos dois métodos para retornar as coordenadas X e Y do último texto desenhado:

//+------------------------------------------------------------------+
//| Methods of working with text                                     |
//+------------------------------------------------------------------+
//--- Return (1) alignment type (anchor method), the last (2) X and (3) Y text coordinate
   ENUM_TEXT_ANCHOR  TextAnchor(void)                       const { return this.m_text_anchor;                                         }
   int               TextLastX(void)                        const { return this.m_text_x;                                              }
   int               TextLastY(void)                        const { return this.m_text_y;                                              }
//--- Set the current font

Os métodos simplesmente retornam os valores das variáveis correspondentes.

Para que esses valores estejam sempre atualizados, no método que exibe o texto com a fonte atual, escrevemos nessas variáveis as coordenadas passadas em argumentos do método:

//--- Display the text in the current font
   void              Text(int         x,                          // X coordinate of the text anchor point
                          int         y,                          // Y coordinate of the text anchor point
                          string      text,                       // Display text
                          const color clr,                        // Color
                          const uchar opacity=255,                // Opacity
                          uint        alignment=0)                // Text anchoring method
                       { 
                        this.m_text_anchor=(ENUM_TEXT_ANCHOR)alignment;
                        this.m_text_x=x;
                        this.m_text_y=y;
                        this.m_canvas.TextOut(x,y,text,::ColorToARGB(clr,opacity),alignment);
                       }


O texto desenhado pode ter nove pontos de ancoragem:


Por exemplo, se o ponto de ancoragem do texto estiver na parte inferior direita (Right|Bottom), esta será a coordenada XY inicial. Em nossa biblioteca, todas as coordenadas iniciais correspondem ao canto superior esquerdo do retângulo (Left|Top) Se salvarmos a imagem com as coordenadas iniciais do texto, então o texto ficará na parte inferior direita da imagem salva, o que não nos permitirá salvar corretamente a área do fundo em que o texto estará sobreposto.

Por isso, precisamos calcular os deslocamentos das coordenadas do retângulo de contorno do texto, onde é necessário salvar o fundo na matriz para sua posterior restauração. Também podemos calcular a largura e a altura do texto com antecedência - antes de desenhar o texto. Basta especificar o próprio texto, para o método TextSize() da classe CCanvas nos retornar a largura e a altura do retângulo à direita.

Na seção pública da classe, declaramos um método que retorna os deslocamentos das coordenadas X e Y, dependendo de como o texto está alinhado:

//--- Return coordinate offsets relative to the text anchor point
   void              TextGetShiftXY(const string text,            // Text for calculating the size of its outlining rectangle
                                    const ENUM_TEXT_ANCHOR anchor,// Text anchor point, relative to which the offsets are calculated
                                    int &shift_x,                 // X coordinate of the rectangle upper left corner
                                    int &shift_y);                // Y coordinate of the rectangle upper left corner

  };
//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+

O método será discutido a seguir.

No construtor paramétrico da classe inicializamos as coordenadas do último texto desenhado:

//+------------------------------------------------------------------+
//| Parametric constructor                                           |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const int      element_id,
                           const int      element_num,
                           const long     chart_id,
                           const int      wnd_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=true,
                           const bool     activity=true,
                           const bool     redraw=false) : m_shadow(false)
  {
   this.m_chart_color_bg=(color)::ChartGetInteger(chart_id,CHART_COLOR_BACKGROUND);
   this.m_name=this.m_name_prefix+name;
   this.m_chart_id=chart_id;
   this.m_subwindow=wnd_num;
   this.m_type=element_type;
   this.SetFont("Calibri",8);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.m_color_bg=colour;
   this.m_opacity=opacity;
   if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,colour,opacity,redraw))
     {
      this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Graphical resource name
      this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         // Chart ID
      this.SetProperty(CANV_ELEMENT_PROP_WND_NUM,CGBaseObj::SubWindow());        // Chart subwindow index
      this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,CGBaseObj::Name());            // Element object name
      this.SetProperty(CANV_ELEMENT_PROP_TYPE,element_type);                     // Graphical element type
      this.SetProperty(CANV_ELEMENT_PROP_ID,element_id);                         // Element ID
      this.SetProperty(CANV_ELEMENT_PROP_NUM,element_num);                       // Element index in the list
      this.SetProperty(CANV_ELEMENT_PROP_COORD_X,x);                             // Element's X coordinate on the chart
      this.SetProperty(CANV_ELEMENT_PROP_COORD_Y,y);                             // Element's Y coordinate on the chart
      this.SetProperty(CANV_ELEMENT_PROP_WIDTH,w);                               // Element width
      this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,h);                              // Element height
      this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,0);                      // Active area offset from the left edge of the element
      this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,0);                       // Active area offset from the upper edge of the element
      this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,0);                     // Active area offset from the right edge of the element
      this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,0);                    // Active area offset from the bottom edge of the element
      this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,movable);                       // Element moveability flag
      this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,activity);                       // Element activity flag
      this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.RightEdge());                // Element right border
      this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.BottomEdge());              // Element bottom border
      this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_X,this.ActiveAreaLeft());     // X coordinate of the element active area
      this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_Y,this.ActiveAreaTop());      // Y coordinate of the element active area
      this.SetProperty(CANV_ELEMENT_PROP_ACT_RIGHT,this.ActiveAreaRight());      // Right border of the element active area
      this.SetProperty(CANV_ELEMENT_PROP_ACT_BOTTOM,this.ActiveAreaBottom());    // Bottom border of the element active area
     }
   else
     {
      ::Print(CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.m_name);
     }
  }
//+------------------------------------------------------------------+

No construtor protegido da classe, inicializamos essas variáveis da mesma maneira:

//+------------------------------------------------------------------+
//| Protected constructor                                            |
//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           const long    chart_id,
                           const int     wnd_num,
                           const string  name,
                           const int     x,
                           const int     y,
                           const int     w,
                           const int     h) : m_shadow(false)
  {
   this.m_chart_color_bg=(color)::ChartGetInteger(chart_id,CHART_COLOR_BACKGROUND);
   this.m_name=this.m_name_prefix+name;
   this.m_chart_id=chart_id;
   this.m_subwindow=wnd_num;
   this.m_type=element_type;
   this.SetFont("Calibri",8);
   this.m_text_anchor=0;
   this.m_text_x=0;
   this.m_text_y=0;
   this.m_color_bg=NULL_COLOR;
   this.m_opacity=0;
   if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,this.m_color_bg,this.m_opacity,false))
     {
      ...

Agora, demos uma olhada na implementação dos métodos acima declarados.

Implementação do método que salva a imagem numa matriz:

//+------------------------------------------------------------------+
//| Save the image to the array                                      |
//+------------------------------------------------------------------+
bool CGCnvElement::ImageCopy(const string source,uint &array[])
  {
   ::ResetLastError();
   int w=0,h=0;
   if(!::ResourceReadImage(this.NameRes(),array,w,h))
     {
      CMessage::ToLog(source,MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES,true);
      return false;
     }
   return true;
  }
//+------------------------------------------------------------------+

Ao método são passados o nome do método/função a partir do qual foi chamado (isso é necessário para entender onde ocorre o erro, se houver) e uma referência à matriz em que os dados do recurso gráfico (pixels da imagem) devem estar escritos.

Com ajuda da função ResourceReadImage() lemos na matriz os dados do recurso gráfico criado pela classe CCanvas e que contém a imagem da forma. Se houver um erro ao ler o recurso, exibiremos uma mensagem sobre isso e retornamos false. Se tudo correr bem, retornamos true, enquanto todos os pixels da imagem armazenada no recurso serão gravados na matriz passada ao método.

Método que salva o recurso gráfico na matriz:

//+------------------------------------------------------------------+
//| Save the graphical resource to the array                         |
//+------------------------------------------------------------------+
bool CGCnvElement::ResourceCopy(const string source)
  {
   return this.ImageCopy(DFUN,this.m_data_array);
  }
//+------------------------------------------------------------------+

O método retorna o resultado da chamada do método acima. Isto é, ele não é diferente, exceto que neste caso os dados do recurso gráfico não são escritos na matriz passada por referência, mas, sim, na matriz especial que declaramos anteriormente para armazenar uma cópia da imagem de todo o objeto-forma.

Método que retorna deslocamentos de coordenadas em relação ao ponto de ancoragem do texto:

//+------------------------------------------------------------------+
//| Return coordinate offsets relative to the text anchor point      |
//+------------------------------------------------------------------+
void CGCnvElement::TextGetShiftXY(const string text,const ENUM_TEXT_ANCHOR anchor,int &shift_x,int &shift_y)
  {
   int tw=0,th=0;
   this.TextSize(text,tw,th);
   switch(anchor)
     {
      case TEXT_ANCHOR_LEFT_TOP :
        shift_x=0; shift_y=0;
        break;
      case TEXT_ANCHOR_LEFT_CENTER :
        shift_x=0; shift_y=-th/2;
        break;
      case TEXT_ANCHOR_LEFT_BOTTOM :
        shift_x=0; shift_y=-th;
        break;
      case TEXT_ANCHOR_CENTER_TOP :
        shift_x=-tw/2; shift_y=0;
        break;
      case TEXT_ANCHOR_CENTER :
        shift_x=-tw/2; shift_y=-th/2;
        break;
      case TEXT_ANCHOR_CENTER_BOTTOM :
        shift_x=-tw/2; shift_y=-th;
        break;
      case TEXT_ANCHOR_RIGHT_TOP :
        shift_x=-tw; shift_y=0;
        break;
      case TEXT_ANCHOR_RIGHT_CENTER :
        shift_x=-tw; shift_y=-th/2;
        break;
      case TEXT_ANCHOR_RIGHT_BOTTOM :
        shift_x=-tw; shift_y=-th;
        break;
      default:
        shift_x=0; shift_y=0;
        break;
     }
  }
//+------------------------------------------------------------------+

Aqui primeiro obtemos o tamanho do texto passado para o método (os tamanhos serão escritos nas variáveis declaradas) e, em seguida, calculamos quantos pixels deslocar as coordenadas X e Y em relação às coordenadas iniciais do texto dependendo do modo de ancoragem do texto que é passado para o método.

Agora podemos finalizar a classe do objeto-sombra. Como acabamos de adicionar métodos para ler um recurso gráfico e uma matriz constante na qual podemos armazenar uma cópia de tal recurso, é levantada a questão de remover variáveis, matrizes e blocos de código desnecessários da classe do objeto-sombra.

No arquivo \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh fazemos modificações.

Do método de desfoque por Gauss removemos a matriz e variáveis desnecessárias:

//+------------------------------------------------------------------+
//| 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

No bloco de leitura dos dados do recurso gráfico substituímos as linhas para chamada do método escrito por nós acima:

//--- 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'

//--- Read graphical resource data. If failed, return false
   if(!CGCnvElement::ResourceCopy(DFUN))
      return false;

Em todo o código, em vez das variáveis excluídas res_w e res_h vamos usar os métodos da classe do objeto-elemento gráfico Width() e Height(), e em vez da matriz res_data vamos usar uma m_data_array, que agora é usada para armazenar uma cópia do recurso gráfico.

Em geral, todas as modificações resultaram apenas na substituição de variáveis desnecessárias e excluídas por métodos de classes do objeto-elemento gráfico:

//+------------------------------------------------------------------+
//| Gaussian blur                                                    |
//| https://www.mql5.com/en/articles/1612#chapter4                   |
//+------------------------------------------------------------------+
bool CShadowObj::GaussianBlur(const uint radius)
  {
//---
   int n_nodes=(int)radius*2+1;
//--- Read graphical resource data. If failed, return false
   if(!CGCnvElement::ResourceCopy(DFUN))
      return false;
   
//--- Check the blur amount. If the blur radius exceeds half of the width or height, return 'false'
   if((int)radius>=this.Width()/2 || (int)radius>=this.Height()/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(this.m_data_array);
//--- 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::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"a_h_data\"");
      return false;
     }
   if(::ArrayResize(r_h_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"r_h_data\"");
      return false;
     }
   if(::ArrayResize(g_h_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"g_h_data\"");
      return false;
     }
   if(ArrayResize(b_h_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"b_h_data\"");
      return false;
     }
   if(::ArrayResize(a_v_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"a_v_data\"");
      return false;
     }
   if(::ArrayResize(r_v_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"r_v_data\"");
      return false;
     }
   if(::ArrayResize(g_v_data,size)==-1)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE);
      ::Print(DFUN_ERR_LINE,": \"g_v_data\"");
      return false;
     }
   if(::ArrayResize(b_v_data,size)==-1)
     {
      CMessage::ToLog(DFUN,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(this.m_data_array[i]);
      r_h_data[i]=GETRGBR(this.m_data_array[i]);
      g_h_data[i]=GETRGBG(this.m_data_array[i]);
      b_h_data[i]=GETRGBB(this.m_data_array[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(int Y=0;Y<this.Height();Y++)
     {
      //--- Loop by the image height
      for(uint X=radius;X<this.Width()-radius;X++)
        {
         XY=Y*this.Width()+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*this.Width()+x;
         a_h_data[XY]=a_h_data[Y*this.Width()+radius];
         r_h_data[XY]=r_h_data[Y*this.Width()+radius];
         g_h_data[XY]=g_h_data[Y*this.Width()+radius];
         b_h_data[XY]=b_h_data[Y*this.Width()+radius];
        }
      //--- Remove blur artifacts to the right by copying adjacent pixels
      for(int x=int(this.Width()-radius);x<this.Width();x++)
        {
         XY=Y*this.Width()+x;
         a_h_data[XY]=a_h_data[(Y+1)*this.Width()-radius-1];
         r_h_data[XY]=r_h_data[(Y+1)*this.Width()-radius-1];
         g_h_data[XY]=g_h_data[(Y+1)*this.Width()-radius-1];
         b_h_data[XY]=b_h_data[(Y+1)*this.Width()-radius-1];
        }
     }

//--- Blur vertically (along the Y axis) the image already blurred horizontally
   int dxdy=0;
   //--- Loop by the image height
   for(int X=0;X<this.Width();X++)
     {
      //--- Loop by the image width
      for(uint Y=radius;Y<this.Height()-radius;Y++)
        {
         XY=Y*this.Width()+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)this.Width();
            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*this.Width()+X;
         a_v_data[XY]=a_v_data[X+radius*this.Width()];
         r_v_data[XY]=r_v_data[X+radius*this.Width()];
         g_v_data[XY]=g_v_data[X+radius*this.Width()];
         b_v_data[XY]=b_v_data[X+radius*this.Width()];
        }
      //--- Remove blur artifacts at the bottom by copying adjacent pixels
      for(int y=int(this.Height()-radius);y<this.Height();y++)
        {
         XY=y*this.Width()+X;
         a_v_data[XY]=a_v_data[X+(this.Height()-1-radius)*this.Width()];
         r_v_data[XY]=r_v_data[X+(this.Height()-1-radius)*this.Width()];
         g_v_data[XY]=g_v_data[X+(this.Height()-1-radius)*this.Width()];
         b_v_data[XY]=b_v_data[X+(this.Height()-1-radius)*this.Width()];
        }
     }
     
//--- Set the twice blurred (horizontally and vertically) image pixels to the graphical resource data array
   for(int i=0;i<size;i++)
      this.m_data_array[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(int X=0;X<this.Width();X++)
     {
      for(uint Y=radius;Y<this.Height()-radius;Y++)
        {
         XY=Y*this.Width()+X;
         this.m_canvas.PixelSet(X,Y,this.m_data_array[XY]);
        }
     }
//--- Done
   return true;
  }
//+------------------------------------------------------------------+

Agora estamos prontos para criar uma classe, cujo objeto nos permitirá controlar o desenho de elementos gráficos na tela para que posteriormente possamos restaurar facilmente o fundo da imagem sobre a qual será sobreposto o novo desenho. Este será o link a partir do qual podemos criar uma classe para trabalhar com animação de sprites.


Classe para copiar e colar partes de uma imagem

A classe de objeto-forma menor objeto na hierarquia de herança em que podemos trabalhar com animação.
Como a classe para salvar e restaurar uma parte da imagem será pequena, vamos colocá-la diretamente no arquivo da classe do objeto-forma \MQL5\Include\DoEasy\Objects\Graph\Form.mqh. Vamos chamar a classe de copiador de pixeis, o que descreve claramente sua essência.

Cada objeto da classe-copiador de pixeis terá seu próprio identificador, com ele será possível determinar com qual desenho está trabalhando o objeto, e será possível acessar o objeto da classe requerida pelo seu identificador para que seja possível usar separadamente cada objeto animado. Por exemplo, se precisamos gerenciar e alterar simultaneamente três imagens, duas das quais são texto e outra, uma imagem, então, ao criar um objeto-copiador para cada imagem, basta atribuir a eles diferentes identificadores - text1 = ID0, text2 = ID1, imagem = ID2, e assim em cada um dos objetos serão armazenados todos os outros parâmetros para trabalhar com ela, isto é:

  • a matriz de pixels em que será salva aquela parte da imagem de fundo na qual a imagem é sobreposta,
  • as coordenadas X e Y do canto superior esquerdo da área retangular da parte do fundo em que a imagem é sobreposta,
  • a largura e altura da área retangular,
  • e a largura e a altura calculadas desta área.

Precisamos da largura e altura calculadas para saber exatamente que dimensões terá a área retangular de cópia caso esse retângulo ultrapasse a área da forma cujos pixels devem ser salvos. Desse modo, ao restaurar o fundo, já não precisaremos recalcular a largura e a altura da área de fundo retangular realmente copiada, senão que simplesmente usaremos os valores já calculados armazenados nas variáveis ºdo objeto.

Na seção privada da classe declaramos um ponteiro para a classe do objeto-elemento gráfico (vamos passá-lo para o objeto recém-criado da classe-copiador de pixeis para que possamos usar os dados da forma em que criaremos a instância do objeto-copiador), a matriz na qual escreveremos a parte da imagem da forma que precisará ser salva e restaurada, bem como todas as variáveis descritas acima:

//+------------------------------------------------------------------+
//|                                                         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"
//+------------------------------------------------------------------+
//| Pixel copier class                                               |
//+------------------------------------------------------------------+
class CPixelCopier : public CObject
  {
private:
   CGCnvElement     *m_element;                             // Pointer to the graphical element
   uint              m_array[];                             // Pixel array
   int               m_id;                                  // ID
   int               m_x;                                   // X coordinate of the upper left corner
   int               m_y;                                   // Y coordinate of the upper left corner
   int               m_w;                                   // Copied image width
   int               m_h;                                   // Copied image height
   int               m_wr;                                  // Calculated copied image width
   int               m_hr;                                  // Calculated copied image height
public:

Na seção pública da classe, escrevemos o método para comparar dois objetos-copiadores, os métodos para definir e obter as propriedades do objeto, os construtores de classe - padrão e paramétrico, e declaramos dois métodos, um para salvar parte do fundo e outro para restaurá-lo:

public:
//--- Compare CPixelCopier objects by a specified property (to sort the list by an object property)
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CPixelCopier *obj_compared=node;
                        return(mode==0 ? (this.ID()>obj_compared.ID() ? 1 : this.ID()<obj_compared.ID() ? -1 : 0) : WRONG_VALUE);
                       }
   
//--- Set the properties
   void              SetID(const int id)                       { this.m_id=id;      }
   void              SetCoordX(const int value)                { this.m_x=value;    }
   void              SetCoordY(const int value)                { this.m_y=value;    }
   void              SetWidth(const int value)                 { this.m_w=value;    }
   void              SetHeight(const int value)                { this.m_h=value;    }
//--- Get the properties
   int               ID(void)                            const { return this.m_id;  }
   int               CoordX(void)                        const { return this.m_x;   }
   int               CoordY(void)                        const { return this.m_y;   }
   int               Width(void)                         const { return this.m_w;   }
   int               Height(void)                        const { return this.m_h;   }
   int               WidthReal(void)                     const { return this.m_wr;  }
   int               HeightReal(void)                    const { return this.m_hr;  }

//--- Copy the part or the entire image to the array
   bool              CopyImgDataToArray(const uint x_coord,const uint y_coord,uint width,uint height);
//--- Copy the part or the entire image from the array to the canvas
   bool              CopyImgDataToCanvas(const int x_coord,const int y_coord);

//--- Constructors
                     CPixelCopier (void){;}
                     CPixelCopier (const int id,
                                   const int x,
                                   const int y,
                                   const int w,
                                   const int h,
                                   CGCnvElement *element) : m_id(id), m_x(x),m_y(y),m_w(w),m_wr(w),m_h(h),m_hr(h) { this.m_element=element; }
                    ~CPixelCopier (void){;}
  };
//+------------------------------------------------------------------+

Consideremos os métodos com mais detalhes.

Método que compara dois objetos-copiadores:

//--- Compare CPixelCopier objects by a specified property (to sort the list by an object property)
   virtual int       Compare(const CObject *node,const int mode=0) const
                       {
                        const CPixelCopier *obj_compared=node;
                        return(mode==0 ? (this.ID()>obj_compared.ID() ? 1 : this.ID()<obj_compared.ID() ? -1 : 0) : WRONG_VALUE);
                       }

Neste caso, tudo é padrão, como em outras classes da biblioteca. Se o modo de comparação (mode) for igual a 0 (por padrão), serão comparados os identificadores de dois objetos - o atual e aquele cujo o ponteiro foi passado ao método. Se o identificador do objeto atual for maior do que o que está sendo comparado, será retornado 1, se for menor, -1, já se for igual, 0. Em todos os outros casos (se mode != 0), será devolvido -1. Isto é, este método atualmente só pode comparar identificadores de objetos.

Na lista de inicialização do construtor paramétrico da classe, todas as variáveis-membros da classe recebem os valores passados nos argumentos, já no corpo da classe a variável-ponteiro para a classe do objeto-elemento gráfico recebe o valor do ponteiro que também foi passado nos argumentos:

CPixelCopier (const int id,
              const int x,
              const int y,
              const int w,
              const int h,
              CGCnvElement *element) : m_id(id), m_x(x),m_y(y),m_w(w),m_wr(w),m_h(h),m_hr(h) { this.m_element=element; }

Agora, o objeto-copiador recém-criado "saberá" qual objeto o criou e terá acesso a seus métodos e parâmetros.

Método que copia para a matriz parte ou toda a imagem:

//+------------------------------------------------------------------+
//| Copy part or all of the image to the array                       |
//+------------------------------------------------------------------+
bool CPixelCopier::CopyImgDataToArray(const uint x_coord,const uint y_coord,uint width,uint height)
  {
//--- Assign coordinate values, passed to the method, to the variables
   int x1=(int)x_coord;
   int y1=(int)y_coord;
//--- If X coordinates goes beyond the form on the right or Y coordinate goes beyond the form at the bottom,
//--- there is nothing to copy, the copied area is outside the form. Return 'false'
   if(x1>this.m_element.Width()-1 || y1>this.m_element.Height()-1)
      return false;
//--- Assign the width and height values of the copied area to the variables
//--- If the passed width and height are equal to zero, assign the form width and height to them
   this.m_wr=int(width==0  ? this.m_element.Width()  : width);
   this.m_hr=int(height==0 ? this.m_element.Height() : height);
//--- If X and Y coordinates are equal to zero (the upper left corner of the form), as well as the width and height are equal to the form width and height,
//--- the copied area is equal to the entire form area. Copy the entire form (returning it from the method) using the ImageCopy() method
   if(x1==0 && y1==0 && this.m_wr==this.m_element.Width() && this.m_hr==this.m_element.Height())
      return this.m_element.ImageCopy(DFUN,this.m_array);

//--- Calculate the right X coordinate and lower Y coordinate of the rectangle area
   int x2=int(x1+this.m_wr-1);
   int y2=int(y1+this.m_hr-1);
//--- If the calculated X coordinate goes beyond the form, the right edge of the form will be used as the coordinate
   if(x2>=this.m_element.Width()-1)
      x2=this.m_element.Width()-1;
//--- If the calculated Y coordinate goes beyond the form, the bottom edge of the form will be used as the coordinate
   if(y2>=this.m_element.Height()-1)
      y2=this.m_element.Height()-1;
//--- Calculate the copied width and height
   this.m_wr=x2-x1+1;
   this.m_hr=y2-y1+1;
//--- Define the necessary size of the array, which is to store all image pixels with calculated width and height
   int size=this.m_wr*this.m_hr;
//--- If failed to set the array size, inform of that and return 'false'
   if(::ArrayResize(this.m_array,size)!=size)
     {
      CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE,true);
      return false;
     }
//--- Set the index in the array for recording the image pixel
   int n=0;
//--- In a loop by the calculated height of the copied area, starting from the specified Y coordinate
   for(int y=y1;y<y1+this.m_hr;y++)
     {
      //--- in a loop by the calculated width of the copied area, starting from the specified X coordinate
      for(int x=x1;x<x1+this.m_wr;x++)
        {
         //--- Copy the next image pixel to the array and increase the array index
         this.m_array[n]=this.m_element.GetCanvasObj().PixelGet(x,y);
         n++;
        }
     }
//--- Successful - return 'true'
   return true;
  }
//+------------------------------------------------------------------+

Cada linha do método é detalhada nos comentários do código. Resumindo, se as coordenadas iniciais da área copiada estão fora da forma, não haverá nada para copiar e retornaremos false. Se as coordenadas iniciais da área copiada coincidirem com as da forma e, ademais, a largura e a altura da área copiada forem iguais a zero ou coincidirem com as da forma, copiaremos toda imagem da forma. Se for necessário salvar apenas uma parte da imagem, primeiro calcularemos a largura e a altura copiadas para que não ultrapassem as da forma e, depois, copiaremos todos os pixels da imagem da forma que conseguem entrar na área copiada.

Método que copia da matriz para a tela parte ou toda a imagem:

//+------------------------------------------------------------------+
//| Copy the part or the entire image from the array to the canvas   |
//+------------------------------------------------------------------+
bool CPixelCopier::CopyImgDataToCanvas(const int x_coord,const int y_coord)
  {
//--- If the array of saved pixels is empty, inform of that and return 'false'
   int size=::ArraySize(this.m_array);
   if(size==0)
     {
      CMessage::ToLog(DFUN,MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY,true);
      return false;
     }
//--- Set the index of the array for reading the image pixel
   int n=0;
//--- In a loop by the previously calculated height of the copied area, starting from the specified Y coordinate
   for(int y=y_coord;y<y_coord+this.m_hr;y++)
     {
      //--- in a loop by the previously calculated width of the copied area, starting from the specified X coordinate
      for(int x=x_coord;x<x_coord+this.m_wr;x++)
        {
         //--- Restore the next image pixel from the array and increase the array index
         this.m_element.GetCanvasObj().PixelSet(x,y,this.m_array[n]);
         n++;
        }
     }
   return true;
  }
//+------------------------------------------------------------------+

A lógica do método também é descrita em detalhes nos comentários do código. Neste caso, ao contrário do método que salva uma parte da imagem, já não precisamos calcular as coordenadas e tamanhos da área copiada, uma vez que todos eles são salvos nas variáveis da classe após o primeiro método funcionar. Só precisamos - num loop pela altura - copiar para a tela cada linha da área restaurada pixel por pixel, restabelecendo assim a parte da imagem que foi salva pelo método anterior.

Agora precisamos acessar a classe recém-escrita desde a classe do objeto-forma.

Visto que criaremos dinamicamente o número necessário de objetos-copiadores, na classe do objeto-forma precisamos declarar uma lista desses objetos. Cada objeto-copiador recém-criado será adicionado a ela. Assim, podemos usá-la para obter ponteiros para os objetos necessários e trabalhar com eles.

Na seção privada da classe declaramos esta lista:

//+------------------------------------------------------------------+
//| Form object class                                                |
//+------------------------------------------------------------------+
class CForm : public CGCnvElement
  {
private:
   CArrayObj         m_list_elements;                          // List of attached elements
   CArrayObj         m_list_pc_obj;                            // List of pixel copier objects
   CShadowObj       *m_shadow_obj;                             // Pointer to the shadow object
   color             m_color_frame;                            // Form frame color
   int               m_frame_width_left;                       // Form frame width to the left
   int               m_frame_width_right;                      // Form frame width to the right
   int               m_frame_width_top;                        // Form frame width at the top
   int               m_frame_width_bottom;                     // Form frame width at the bottom


Como não podemos ter vários objetos-copiadores com os mesmos identificadores, precisamos de um método que retorne um sinalizador indicando que o objeto com o identificador especificado existe na lista. Declaramos este método:

//--- Create a shadow object
   void              CreateShadowObj(const color colour,const uchar opacity);
   
//--- Return the flag indicating the presence of the copier object with the specified ID in the list
   bool              IsPresentPC(const int id);

public:

Na seção pública da classe, escrevemos um método que retorna um ponteiro para o objeto-forma atual e um método que retorna uma lista de objetos-copiadores:

public:
   //--- Constructors
                     CForm(const long chart_id,
                           const int subwindow,
                           const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CForm(const int subwindow,
                           const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CForm(const string name,
                           const int x,
                           const int y,
                           const int w,
                           const int h);
                     CForm() { this.Initialize(); }
//--- Destructor
                    ~CForm();
                           
//--- Supported form 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;                   }
   
//--- Return (1) itself, the list of (2) attached objects, (3) pixel copier objects and (4) the shadow object
   CForm            *GetObject(void)                                          { return &this;                  }
   CArrayObj        *GetList(void)                                            { return &this.m_list_elements;  }
   CArrayObj        *GetListPC(void)                                          { return &this.m_list_pc_obj;    }
   CGCnvElement     *GetShadowObj(void)                                       { return this.m_shadow_obj;      }

Em seguida, declaramos um método que cria um novo objeto-copiador de pixeis de imagem:

//--- Create a new pixel copier object
   CPixelCopier     *CreateNewPixelCopier(const int id,const int x_coord,const int y_coord,const int width,const int height);

//--- 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);

Antes do bloco de código com métodos de acesso simplificado às propriedades do objeto, vamos escrever um bloco de código para trabalhar com pixels de imagem:

//+------------------------------------------------------------------+
//| Methods of working with image pixels                             |
//+------------------------------------------------------------------+
//--- Return the pixel copier object by ID
   CPixelCopier     *GetPixelCopier(const int id);
//--- Copy the part or the entire image to the array
   bool              ImageCopy(const int id,const uint x_coord,const uint y_coord,uint &width,uint &height);
//--- Copy the part or the entire image from the array to the canvas
   bool              ImagePaste(const int id,const uint x_coord,const uint y_coord);
   
//+------------------------------------------------------------------+

Fora do corpo da classe, vamos escrever a implementação dos métodos declarados.

Método que retorna o sinalizador que indica se o objeto-copiador com o identificador especificado existe na lista:

//+------------------------------------------------------------------+
//| Return the flag indicating the presence                          |
//| of the copier object with the specified ID in the list           |
//+------------------------------------------------------------------+
bool CForm::IsPresentPC(const int id)
  {
   for(int i=0;i<this.m_list_pc_obj.Total();i++)
     {
      CPixelCopier *pc=this.m_list_pc_obj.At(i);
      if(pc==NULL)
         continue;
      if(pc.ID()==id)
         return true;
     }
   return false;
  }
//+------------------------------------------------------------------+

Aqui, num loop simples ao longo da lista de objetos-copiadores, obtemos o próximo objeto e, se seu identificador for igual ao passado para o método, retornamos true. No final do loop, devolvemos false.

Método que cria um novo objeto-copiador de pixeis de imagem:

//+------------------------------------------------------------------+
//| Create a new pixel copier object                                 |
//+------------------------------------------------------------------+
CPixelCopier *CForm::CreateNewPixelCopier(const int id,const int x_coord,const int y_coord,const int width,const int height)
  {
//--- If the object with such an ID is already present, inform of that in the journal and return NULL
   if(this.IsPresentPC(id))
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST),(string)id);
      return NULL;
     }
//--- Create a new copier object with the specified parameters
   CPixelCopier *pc=new CPixelCopier(id,x_coord,y_coord,width,height,CGCnvElement::GetObject());
//--- If failed to create an object, inform of that and return NULL
   if(pc==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ));
      return NULL;
     }
//--- If failed to add the created object to the list, inform of that, remove the object and return NULL
   if(!this.m_list_pc_obj.Add(pc))
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST)," ID: ",id);
      delete pc;
      return NULL;
     }
//--- Return the pointer to a newly created object
   return pc;
  }
//+------------------------------------------------------------------+

Toda a lógica do método está descrita nos comentários ao código. De qualquer forma, na discussão do artigo podem ser feitas todas as perguntas que surgirem.

Método que retorna um ponteiro para um objeto-copiador de pixeis por identificador:

//+------------------------------------------------------------------+
//| Return the pixel copier object by ID                             |
//+------------------------------------------------------------------+
CPixelCopier *CForm::GetPixelCopier(const int id)
  {
   for(int i=0;i<this.m_list_pc_obj.Total();i++)
     {
      CPixelCopier *pc=m_list_pc_obj.At(i);
      if(pc==NULL)
         continue;
      if(pc.ID()==id)
         return pc;
     }
   return NULL;
  }
//+------------------------------------------------------------------+

Aqui, também, tudo é simples, num loop ao longo da lista de objetos-copiadores, obtemos um ponteiro para o próximo objeto e, se seu identificador corresponder ao necessário, retornamos o ponteiro. No final do loop, retornamos NULL - o objeto com o identificador especificado não foi encontrado na lista.

Método que copia para a matriz parte ou toda a imagem:

//+------------------------------------------------------------------+
//| Copy part or all of the image to the array                       |
//+------------------------------------------------------------------+
bool CForm::ImageCopy(const int id,const uint x_coord,const uint y_coord,uint &width,uint &height)
  {
   CPixelCopier *pc=this.GetPixelCopier(id);
   if(pc==NULL)
     {
      pc=this.CreateNewPixelCopier(id,x_coord,y_coord,width,height);
      if(pc==NULL)
         return false;
     }
   return pc.CopyImgDataToArray(x_coord,y_coord,width,height);
  }
//+------------------------------------------------------------------+

Aqui, obtemos um ponteiro para o objeto-copiador por identificador. Se o objeto não for encontrado, imprimimos isso e devolvemos false. Se o ponteiro para o objeto for recebido com sucesso, retornamos o resultado do método CopyImgDataToArray() da classe do objeto-copiador que consideramos acima.

Método que copia da matriz para a tela parte ou toda a imagem:

//+------------------------------------------------------------------+
//| Copy the part or the entire image from the array to the canvas   |
//+------------------------------------------------------------------+
bool CForm::ImagePaste(const int id,const uint x_coord,const uint y_coord)
  {
   CPixelCopier *pc=this.GetPixelCopier(id);
   if(pc==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST),(string)id);
      return false;
     }
   return pc.CopyImgDataToCanvas(x_coord,y_coord);
  }
//+------------------------------------------------------------------+

A lógica do método é idêntica à anterior, exceto que agora não salvamos a área numa matriz, mas a restauramos a partir da matriz.

Estamos prontos para testar o funcionamento do objeto-copiador de pixeis de imagem.


Teste

Precisamos testar e ter certeza de que o objeto-copiador funciona corretamente. No início do artigo, apresentei uma imagem GIF que mostrava claramente como cada imagem desenhada contra o fundo do objeto-forma era sobreposta às anteriores. Agora, precisamos do copiador de pixeis para, primeiro, salvar o fundo sobre o qual será colocado o texto e, antes de desenhar o novo texto (isto é, mover visualmente o texto desenhado para um novo lugar), restaurar o fundo no qual foi desenhado o texto (substituindo-o assim), salvar parte da imagem de fundo nas novas coordenadas e exibir o texto a seguir. Repetiremos o mesmo procedimento para cada um dos nove textos exibidos, textos esses que terão diferentes pontos de ancoragem e serão exibidos nos lados da forma que corresponderão aos pontos de ancoragem do texto. Assim, verificaremos também que cálculo dos deslocamentos das coordenadas da parte salva da imagem sob o texto estarão corretos.

Para o teste, vamos pegar o Expert Advisor do artigo anterior e salvá-lo na nova pasta \MQL5\Experts\TestDoEasy\Part78\ com o novo nome TestDoEasyPart78.mq5.

O Expert Advisor exibe três formas no gráfico. Na forma mais inferior é desenhado um fundo com um preenchimento de gradiente vertical. Aqui também desenharemos outra forma, a quarta, nela faremos um preenchimento de gradiente horizontal. Exibiremos os textos em teste nesta forma.

No campo das variáveis globais do Expert Advisor indicamos a necessidade de criar quatro objetos-forma:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart78.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 (4)   // 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[];
//+------------------------------------------------------------------+

Ao criar a forma, no manipulador OnInit() calcularemos as coordenadas da nova forma dependendo das coordenadas da anterior. Depois de criar cada forma consecutiva, não é necessário redesenhar todo o gráfico, por isso, excluímos das linhas especificadas a transferência - para os métodos - da atualização das formas (anteriormente, passávamos explicitamente o valor true) Agora vamos passar este valor no final - após a criação completa da última forma - num novo bloco de código para criar a quarta 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++)
     {
      int y=40;
      if(i>0)
        {
         CForm *form_prev=list_forms.At(i-1);
         if(form_prev==NULL)
            continue;
         y=form_prev.BottomEdge()+10;
        }
      //--- When creating an object, pass all the required parameters to it
      CForm *form=new CForm("Form_0"+(string)(i+1),300,y,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();
        }
      //--- If this is the second 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();
        }
      //--- If this is the third 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,-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();
        }
      //--- If this is the fourth (bottom - tested) form
      if(i==3)
        {
         //--- 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,-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 horizontal gradient
         form.Erase(array_clr,form.Opacity(),false);
         //--- 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        
         //--- Specify the text parameters (text coordinates in the center of the form) and the anchor point (located at the center as well)
         string text=TextByLanguage("H-Градиент","H-Gradient");
         int text_x=form.Width()/2;
         int text_y=form.Height()/2;
         ENUM_TEXT_ANCHOR anchor=TEXT_ANCHOR_CENTER;
         
         //--- Find out the width and height of the outlining text rectangle (to be used as the size of the saved area)
         int text_w=0,text_h=0;
         form.TextSize(text,text_w,text_h);
         //--- Calculate coordinate offsets for the saved area depending on the text anchor point
         int shift_x=0,shift_y=0;
         form.TextGetShiftXY(text,anchor,shift_x,shift_y);
         
         //--- If a background area with calculated coordinates and size under the future text is successfully saved
         if(form.ImageCopy(0,text_x+shift_x,text_y+shift_y,text_w,text_h))
           {
            //--- Draw the text and update the form together with redrawing a chart
            form.Text(text_x,text_y,text,C'211,233,149',255,anchor);
            form.Update(true);
           }
        }
      //--- Add objects to the list
      if(!list_forms.Add(form))
        {
         delete form;
         continue;
        }
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

Aqui, cada linha de código para criar a nova forma é comentada em detalhes. A chave é que depois de gerar a forma e antes de desenhar nela o texto, precisamos salvar aquela parte do fundo em que estará localizado o texto. Já depois, em outro manipulador, precisamos, primeiro, restaurar o fundo da forma apagando o texto com ele e, só então, exibir os textos em novos locais, preservando o fundo sob eles da mesma forma e restaurando-o com cada novo movimento do texto para novas coordenadas.

Faremos tudo isso no manipulador OnChartEvent() num novo bloco de código:

//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- If clicking an object
   if(id==CHARTEVENT_OBJECT_CLICK)
     {
      //--- If the clicked object belongs to the EA
      if(StringFind(sparam,MQLInfoString(MQL_PROGRAM_NAME))==0)
        {
         //--- Get the object ID from it
         int form_id=(int)StringToInteger(StringSubstr(sparam,StringLen(sparam)-1))-1;
         //--- Find this form object in the loop by all forms created in the EA
         for(int i=0;i<list_forms.Total();i++)
           {
            CForm *form=list_forms.At(i);
            if(form==NULL)
               continue;
            //--- If the clicked object has the ID of 3 and the form has the same ID
            if(form_id==3 && form.ID()==3)
              {
               //--- Set the text parameters 
               string text=TextByLanguage("H-Градиент","H-Gradient");
               //--- Get the size of the future text
               int text_w=0,text_h=0;
               form.TextSize(text,text_w,text_h);
               //--- Get the anchor point of the last drawn text (this is the form which contained the last drawn text)
               ENUM_TEXT_ANCHOR anchor=form.TextAnchor();
               //--- Get the coordinates of the last drawn text
               int text_x=form.TextLastX();
               int text_y=form.TextLastY();
               //--- Calculate the coordinate offset of the saved rectangle area depending on the text anchor point
               int shift_x=0,shift_y=0;
               form.TextGetShiftXY(text,anchor,shift_x,shift_y);
               //--- Set the text anchor initial point (0 = LEFT_TOP) out of nine possible ones
               static int n=0;
               //--- If the previously copied form background image is successfully restored when creating the form object in OnInit()
               if(form.ImagePaste(0,text_x+shift_x,text_y+shift_y))
                 {
                  //--- Depending on the n variable, set the new text anchor point
                  switch(n)
                    {
                     case 0 : anchor=TEXT_ANCHOR_LEFT_TOP;     text_x=1;               text_y=1;               break;
                     case 1 : anchor=TEXT_ANCHOR_CENTER_TOP;   text_x=form.Width()/2;  text_y=1;               break;
                     case 2 : anchor=TEXT_ANCHOR_RIGHT_TOP;    text_x=form.Width()-2;  text_y=1;               break;
                     case 3 : anchor=TEXT_ANCHOR_LEFT_CENTER;  text_x=1;               text_y=form.Height()/2; break;
                     case 4 : anchor=TEXT_ANCHOR_CENTER;       text_x=form.Width()/2;  text_y=form.Height()/2; break;
                     case 5 : anchor=TEXT_ANCHOR_RIGHT_CENTER; text_x=form.Width()-2;  text_y=form.Height()/2; break;
                     case 6 : anchor=TEXT_ANCHOR_LEFT_BOTTOM;  text_x=1;               text_y=form.Height()-2; break;
                     case 7 : anchor=TEXT_ANCHOR_CENTER_BOTTOM;text_x=form.Width()/2;  text_y=form.Height()-2; break;
                     case 8 : anchor=TEXT_ANCHOR_RIGHT_BOTTOM; text_x=form.Width()-2;  text_y=form.Height()-2; break;
                     default: anchor=TEXT_ANCHOR_CENTER;       text_x=form.Width()/2;  text_y=form.Height()/2; break;
                    }
                  //--- According to the new anchor point, get the new offsets of the saved area coordinates
                  form.TextGetShiftXY(text,anchor,shift_x,shift_y);
                  //--- If the background area is successfully saved at new coordinates
                  if(form.ImageCopy(0,text_x+shift_x,text_y+shift_y,text_w,text_h))
                    {
                     //--- Draw the text in new coordinates and update the form
                     form.Text(text_x,text_y,text,C'211,233,149',255,anchor);
                     form.Update();
                    }
                  //--- Increase the object click counter (and also the pointer to the text anchor point),
                  //--- and if the value exceeds 8, reset the value to zero (from 0 to 8 = nine anchor points)
                  n++;
                  if(n>8) n=0;
                 }
              }
           }
        }
     }
  }
//+------------------------------------------------------------------+

Tudo é descrito com detalhes suficientes nos comentários do código. Todas as perguntas podem ser feitas na discussão do artigo.

Vamos compilar o Expert Advisor e executá-lo no gráfico.

Clicamos na forma inferior com o mouse e nos certificamos de que tudo funciona conforme o esperado:



O que vem agora?

No próximo artigo, continuaremos a desenvolver na biblioteca esta ideia sobre animação e começaremos a trabalhar com animação de sprites.

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
Gráficos na biblioteca DoEasy (Parte 77): classe do objeto Sombra

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/9612

Arquivos anexados |
MQL5.zip (4044.2 KB)
Gráficos na biblioteca DoEasy (Parte 79): classe para o objeto quadro-de-animação e seus objetos herdeiros Gráficos na biblioteca DoEasy (Parte 79): classe para o objeto quadro-de-animação e seus objetos herdeiros
Neste artigo, desenvolveremos uma classe para um quadro de animação e seus herdeiros. A classe permitirá desenhar formas, bem como salvar e restaurar o plano de fundo.
Padrões com exemplos (Parte I): Topo múltiplo Padrões com exemplos (Parte I): Topo múltiplo
Com este artigo começamos um ciclo em que consideraremos padrões de reversão no âmbito da negociação algorítmica. Iniciamos examinando a primeira e mais interessante família de padrões desse tipo que se originam dos chamados topo duplo e fundo duplo.
Gráficos na biblioteca DoEasy (Parte 80): classe do objeto quadro de animação geométrica Gráficos na biblioteca DoEasy (Parte 80): classe do objeto quadro de animação geométrica
Neste artigo, otimizaremos o código das classes vistas nos artigos anteriores e criaremos uma classe para o objeto do quadro de animação geométrica que nos permite desenhar polígonos regulares com um determinado número de vértices.
Gráficos na biblioteca DoEasy (Parte 77): classe do objeto Sombra Gráficos na biblioteca DoEasy (Parte 77): classe do objeto Sombra
Neste artigo, criaremos uma classe separada para o objeto de sombra (um herdeiro do objeto de elemento gráfico) e também adicionaremos a capacidade de ocupar o fundo do objeto com um preenchimento gradiente.