English Русский
preview
Tabelas no paradigma MVC em MQL5: colunas configuráveis e ordenáveis

Tabelas no paradigma MVC em MQL5: colunas configuráveis e ordenáveis

MetaTrader 5Exemplos |
33 3
Artyom Trishkin
Artyom Trishkin

Sumário



Introdução

No artigo anterior, dedicado à criação de tabelas em MQL5 no paradigma MVC, integramos os dados tabulares (Model) à sua representação gráfica (View) em um único controle (TableView) e, com base nesse objeto, criamos uma tabela estática simples. As tabelas são uma ferramenta prática para classificar e apresentar diferentes tipos de dados de maneira conveniente para o usuário. Portanto, uma tabela deve oferecer ao usuário mais possibilidades de controlar a forma como os dados são exibidos.

Hoje adicionaremos às tabelas a possibilidade de ajustar a largura das colunas, especificar os tipos de dados exibidos e ordenar os dados da tabela por suas colunas. Para isso, basta aprimorar as classes de controles que já criamos anteriormente. No final, porém, adicionaremos uma nova classe para simplificar a criação de tabelas. Essa classe permitirá criar, em poucas linhas, tabelas a partir de dados previamente preparados.

No padrão MVC (Model - View - Controller), a interação entre os três componentes é estruturada de modo que, quando uma ação na interface (View), mediada pelo controlador (Controller), altera o modelo (Model), o modelo (Model) é atualizado e seu novo estado é então exibido novamente pela View. Aplicaremos a mesma lógica aos três componentes da tabela: um clique do mouse no cabeçalho de uma coluna da tabela (atuação do componente Controller) provocará uma alteração na disposição dos dados do modelo da tabela (reordenação dos dados no componente Model), e a View atualizará a tabela para refletir essa nova ordenação.



Aprimorando as classes da biblioteca

Todos os arquivos da biblioteca em desenvolvimento estão localizados em \MQL5\Indicators\Tables. O arquivo das classes do modelo de tabelas (Tables.mqh), juntamente com o arquivo do indicador de teste (iTestTable.mq5), está localizado na pasta \MQL5\Indicators\Tables.

Os arquivos da biblioteca gráfica (Base.mqh e Controls.mqh) estão localizados na subpasta \MQL5\Indicators\Tables\Controls. Todos os arquivos necessários podem ser baixados em um único arquivo compactado no artigo anterior.

Vamos aprimorar as classes do modelo de tabelas no arquivo \MQL5\Indicators\Tables\Tables.mqh.

Por padrão, as linhas do modelo da tabela são ordenadas pelo identificador (índice) da linha. A primeira linha tem índice 0. As células de cada linha também começam pelo índice zero. Para ordenar pelos índices das células, precisamos definir um valor que será somado ao índice da célula e que permitirá identificar que a ordenação deve ser feita pelo índice dessa célula. Além disso, precisamos indicar a direção da ordenação. Portanto, serão necessários dois números: o primeiro indicará que a ordenação pelo índice da célula deve ser feita em ordem crescente, enquanto o segundo indicará que ela deve ser feita em ordem decrescente.

Definiremos macros para esses números:

//+------------------------------------------------------------------+
//| Macros                                                           |
//+------------------------------------------------------------------+
#define  __TABLES__                 // File ID
#define  MARKER_START_DATA    -1    // Data start marker in a file
#define  MAX_STRING_LENGTH    128   // Maximum length of a string in a cell
#define  CELL_WIDTH_IN_CHARS  19    // Table cell width in characters
#define  ASC_IDX_CORRECTION   10000 // Column index offset for ascending sorting
#define  DESC_IDX_CORRECTION  20000 // Column index offset for descending sorting

Ao definir essas macros, estabelecemos que a tabela poderá ter no máximo 10.000 linhas e cada linha, no máximo, 10.000 células. Isso parece mais do que suficiente para trabalhar com tabelas, já que corresponde a 100 milhões de células.

No método de ordenação, passaremos no parâmetro mode um número de zero até 10.000. Esse intervalo será usado para a ordenação pelos índices das linhas da tabela. Os valores de 10.000 inclusive até 19.999 indicarão a ordenação crescente pelo índice da coluna da tabela. Já os valores a partir de 20.000 indicarão a ordenação decrescente pelo índice da coluna:

/*
    Sort(0)                      -  by row index
    
    Sort(ASC_IDX_CORRECTION)     -  ascending by column 0
    Sort(1+ASC_IDX_CORRECTION)   -  ascending by column 1
    Sort(2+ASC_IDX_CORRECTION)   -  ascending by column 2
    etc.
    Sort(DESC_IDX_CORRECTION)    -  descending by column 0
    Sort(1+DESC_IDX_CORRECTION)  -  descending by column 1
    Sort(2+DESC_IDX_CORRECTION)  -  descending by column 2
    etc.
*/  

Para que o método Sort() de ordenação da lista funcione corretamente, é necessário redefinir o método virtual Compare(), responsável pela comparação entre dois objetos e definido na classe base da Biblioteca Padrão. Por padrão, esse método retorna 0, o que indica que os objetos comparados são iguais.

Já temos um método de comparação implementado na classe CTableRow, que representa uma linha da tabela. Vamos aprimorá-lo para permitir a ordenação das linhas pelos valores da coluna indicada pelo índice, levando também em conta a direção da ordenação:

//+------------------------------------------------------------------+
//| Compare two objects                                              |
//+------------------------------------------------------------------+
int CTableRow::Compare(const CObject *node,const int mode=0) const
  {
   if(node==NULL)
      return -1;
   
//--- Sort by row index
   if(mode==0)
     {
      const CTableRow *obj=node;
      return(this.Index()>obj.Index() ? 1 : this.Index()<obj.Index() ? -1 : 0);
     }
   
//--- Sort by cell index in ascending/descending order
//--- Sorting direction flag and cell index for sorting
   bool asc=(mode>=ASC_IDX_CORRECTION && mode<DESC_IDX_CORRECTION);
   int  col= mode%(asc ? ASC_IDX_CORRECTION : DESC_IDX_CORRECTION);
      
//--- Remove the 'node' constancy
   CTableRow *nonconst_this=(CTableRow*)&this;
   CTableRow *nonconst_node=(CTableRow*)node;

//--- Get the current and compared cells by 'mode' index
   CTableCell *cell_current =nonconst_this.GetCell(col);
   CTableCell *cell_compared=nonconst_node.GetCell(col);
   if(cell_current==NULL || cell_compared==NULL)
      return -1;
   
//--- Compare depending on the cell type
   int cmp=0;
   switch(cell_current.Datatype())
     {
      case TYPE_DOUBLE  :  cmp=(cell_current.ValueD()>cell_compared.ValueD() ? 1 : cell_current.ValueD()<cell_compared.ValueD() ? -1 : 0); break;
      case TYPE_LONG    :
      case TYPE_DATETIME:
      case TYPE_COLOR   :  cmp=(cell_current.ValueL()>cell_compared.ValueL() ? 1 : cell_current.ValueL()<cell_compared.ValueL() ? -1 : 0); break;
      case TYPE_STRING  :  cmp=::StringCompare(cell_current.ValueS(),cell_compared.ValueS());                                              break;
      default           :  break;
     }
//--- Return the result of comparing cells taking into account the sorting direction
   return(asc ? cmp : -cmp);   
  }

O bloco de código destacado realiza a comparação entre duas células da tabela em ordem crescente (mode >= 10000 && <20000) ou decrescente (mode >= 20000). Como, para fazer a comparação, precisaremos extrair do objeto string os objetos de célula necessários, e como eles não são constantes (ao mesmo tempo em que a string para comparação é passada ao método como um ponteiro constante), é necessário, primeiro , remover a constante de *node, declarando objetos não constantes para a comparação. A partir deles, podemos então obter os objetos de célula que serão comparados.

Essas conversões são perigosas, pois removem o qualificador const dos ponteiros e podem permitir que os objetos sejam modificados acidentalmente. Neste caso, porém, sabemos com certeza que o método apenas compara valores, sem modificá-los. Portanto, podemos admitir aqui essa pequena exceção controlada para obter o resultado necessário.

Na classe CTableModel, que representa o modelo da tabela, adicionaremos três novos métodos para simplificar a manipulação das colunas e a ordenação por seus valores:

public:
//--- Create a new string and (1) add it to the end of the list, (2) insert to the specified list position
   CTableRow        *RowAddNew(void);
   CTableRow        *RowInsertNewTo(const uint index_to);
//--- (1) Remove or (2) relocate the row, (3) clear the row data
   bool              RowDelete(const uint index);
   bool              RowMoveTo(const uint row_index, const uint index_to);
   void              RowClearData(const uint index);
//--- (1) Return and (2) display the row description in the journal
   string            RowDescription(const uint index);
   void              RowPrint(const uint index,const bool detail);
   
//--- (1) Add, (2) remove, (3) relocate a column, (4) clear data, set the column data (5) type,
//--- (6) data accuracy, (7) time, (8) column color names display flags
   bool              ColumnAddNew(const int index=-1);
   bool              ColumnDelete(const uint index);
   bool              ColumnMoveTo(const uint col_index, const uint index_to);
   void              ColumnClearData(const uint index);
   void              ColumnSetDatatype(const uint index,const ENUM_DATATYPE type);
   void              ColumnSetDigits(const uint index,const int digits);
   void              ColumnSetTimeFlags(const uint index, const uint flags);
   void              ColumnSetColorNamesFlag(const uint index, const bool flag);
  
//--- Sort the table by the specified column and direction
   void              SortByColumn(const uint column, const bool descending);
   
//--- (1) Return and (2) display the table description in the journal
   virtual string    Description(void);
   void              Print(const bool detail);
   void              PrintTable(const int cell_width=CELL_WIDTH_IN_CHARS);

Fora do corpo da classe, escreveremos a implementação desses métodos.

Método que define os flags de exibição de tempo da coluna:

//+------------------------------------------------------------------+
//| Set the column time display flags                                |
//+------------------------------------------------------------------+
void CTableModel::ColumnSetTimeFlags(const uint index,const uint flags)
  {
//--- In a loop through all table rows
   for(uint i=0;i<this.RowsTotal();i++)
     {
      //--- get the cell with the column index from each row and set the time display flags
      CTableCell *cell=this.GetCell(i, index);
      if(cell!=NULL)
         cell.SetDatetimeFlags(flags);
     }
  }

Método que define o flag de exibição dos nomes das cores da coluna:

//+------------------------------------------------------------------+
//| Sets the flag for displaying column color names                  |
//+------------------------------------------------------------------+
void CTableModel::ColumnSetColorNamesFlag(const uint index,const bool flag)
  {
//--- In a loop through all table rows
   for(uint i=0;i<this.RowsTotal();i++)
     {
      //--- get the cell with the column index from each row and set the flag for displaying color names
      CTableCell *cell=this.GetCell(i, index);
      if(cell!=NULL)
         cell.SetColorNameFlag(flag);
     }
  }

Os dois métodos percorrem as linhas da tabela em um laço simples, obtêm de cada linha a célula correspondente e definem nela o flag especificado.

Método que ordena a tabela pela coluna especificada e no sentido indicado:

//+------------------------------------------------------------------+
//| Sort the table by the specified column and direction             |
//+------------------------------------------------------------------+
void CTableModel::SortByColumn(const uint column,const bool descending)
  {
   if(this.m_list_rows.Total()==0)
      return;
   int mode=(int)column+(descending ? DESC_IDX_CORRECTION : ASC_IDX_CORRECTION);
   this.m_list_rows.Sort(mode);
   this.CellsPositionUpdate();   
  }

O método recebe o índice da coluna da tabela cujos valores serão usados na ordenação e um flag que define a direção da ordenação. Se a lista de linhas estiver vazia, saímos do método. Em seguida, determinamos o modo de ordenação (mode). Se a ordenação for decrescente (descending == true), somamos 20000 ao índice da coluna; se a ordenação for crescente, somamos 10000 ao índice da coluna. Depois disso, chamamos o método de ordenação com o modo especificado e atualizamos todas as células da tabela em cada linha.

Agora adicionaremos novos métodos à classe CTable. São métodos homônimos aos que acabamos de adicionar à classe do modelo da tabela:

public:
//--- (1) Return and (2) display the cell description and (3) the object assigned to the cell
   string            CellDescription(const uint row, const uint col);
   void              CellPrint(const uint row, const uint col);
//--- Return (1) the object assigned to the cell and (2) the type of the object assigned to the cell
   CObject          *CellGetObject(const uint row, const uint col);
   ENUM_OBJECT_TYPE  CellGetObjType(const uint row, const uint col);
   
//--- Create a new string and (1) add it to the end of the list, (2) insert to the specified list position
   CTableRow        *RowAddNew(void);
   CTableRow        *RowInsertNewTo(const uint index_to);
//--- (1) Remove or (2) relocate the row, (3) clear the row data
   bool              RowDelete(const uint index);
   bool              RowMoveTo(const uint row_index, const uint index_to);
   void              RowClearData(const uint index);
//--- (1) Return and (2) display the row description in the journal
   string            RowDescription(const uint index);
   void              RowPrint(const uint index,const bool detail);
   
//--- (1) Add new, (2) remove, (3) relocate the column and (4) clear the column data
   bool              ColumnAddNew(const string caption,const int index=-1);
   bool              ColumnDelete(const uint index);
   bool              ColumnMoveTo(const uint index, const uint index_to);
   void              ColumnClearData(const uint index);
   
//--- Set (1) the value of the specified header and (2) data accuracy,
//--- (3) time and (4) color names for the specified column display flags
   void              ColumnCaptionSetValue(const uint index,const string value);
   void              ColumnSetDigits(const uint index,const int digits);
   void              ColumnSetTimeFlags(const uint index,const uint flags);
   void              ColumnSetColorNamesFlag(const uint col, const bool flag);
   
//--- (1) Set and (2) return the data type for the specified column
   void              ColumnSetDatatype(const uint index,const ENUM_DATATYPE type);
   ENUM_DATATYPE     ColumnDatatype(const uint index);
   
//--- (1) Return and (2) display the object description in the journal
   virtual string    Description(void);
   void              Print(const int column_width=CELL_WIDTH_IN_CHARS);
  
//--- Sort the table by the specified column and direction
   void              SortByColumn(const uint column, const bool descending)
                       {
                        if(this.m_table_model!=NULL)
                           this.m_table_model.SortByColumn(column,descending);
                       }
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                             const { return(OBJECT_TYPE_TABLE);           }

...

//+------------------------------------------------------------------+
//| Set the time display flags for the specified column              |
//+------------------------------------------------------------------+
void CTable::ColumnSetTimeFlags(const uint index,const uint flags)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.ColumnSetTimeFlags(index,flags);
  }
//+------------------------------------------------------------------+
//| Set the color name display flags for the specified column        |
//+------------------------------------------------------------------+
void CTable::ColumnSetColorNamesFlag(const uint index,const bool flag)
  {
   if(this.m_table_model!=NULL)
      this.m_table_model.ColumnSetColorNamesFlag(index,flag);
  }

Os métodos verificam se o objeto do modelo da tabela é válido e chamam os respectivos métodos homônimos apresentados acima.

Vamos aprimorar as classes no arquivo \MQL5\Indicators\Tables\Controls\Base.mqh.

Adicionaremos as declarações antecipadas das classes criadas anteriormente, mas que não foram incluídas na lista, bem como da nova classe que criaremos hoje:

//+------------------------------------------------------------------+
//| Include libraries                                                |
//+------------------------------------------------------------------+
#include <Canvas\Canvas.mqh>              // CCanvas class
#include <Arrays\List.mqh>                // CList class
#include "..\Tables.mqh"

//--- Forward declaration of control element classes
class    CBoundedObj;                     // Base class storing object dimensions
class    CCanvasBase;                     // Base class of graphical elements canvas
class    CCounter;                        // Delay counter class
class    CAutoRepeat;                     // Event auto-repeat class
class    CImagePainter;                   // Image drawing class
class    CVisualHint;                     // Hint class
class    CLabel;                          // Text label class
class    CButton;                         // Simple button class
class    CButtonTriggered;                // Two-position button class
class    CButtonArrowUp;                  // Up arrow button class
class    CButtonArrowDown;                // Down arrow button class
class    CButtonArrowLeft;                // Left arrow button class
class    CButtonArrowRight;               // Right arrow button class
class    CCheckBox;                       // CheckBox control class
class    CRadioButton;                    // RadioButton control class
class    CScrollBarThumbH;                // Horizontal scrollbar slider class
class    CScrollBarThumbV;                // Vertical scrollbar slider class
class    CScrollBarH;                     // Horizontal scrollbar class
class    CScrollBarV;                     // Vertical scrollbar class
class    CTableCellView;                  // Class for visual representation of a table cell
class    CTableRowView;                   // Class for visual representation of a table row
class    CColumnCaptionView;              // Class for visual representation of table column header
class    CTableHeaderView;                // Class for visual representation of a table header
class    CTableView;                      // Class for visual representation of a table
class    CTableControl;                   // Table management class
class    CPanel;                          // Panel control class
class    CGroupBox;                       // GroupBox control class
class    CContainer;                      // Container control class

Na enumeração dos tipos de elementos gráficos, adicionaremos um novo tipo:

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_TYPE                    // Enumeration of graphical element types
  {
   ELEMENT_TYPE_BASE = 0x10000,           // Basic object of graphical elements
   ELEMENT_TYPE_COLOR,                    // Color object
   ELEMENT_TYPE_COLORS_ELEMENT,           // Color object of the graphical object element
   ELEMENT_TYPE_RECTANGLE_AREA,           // Rectangular area of the element
   ELEMENT_TYPE_IMAGE_PAINTER,            // Object for drawing images
   ELEMENT_TYPE_COUNTER,                  // Counter object
   ELEMENT_TYPE_AUTOREPEAT_CONTROL,       // Event auto-repeat object
   ELEMENT_TYPE_BOUNDED_BASE,             // Basic object of graphical element sizes
   ELEMENT_TYPE_CANVAS_BASE,              // Basic canvas object for graphical elements
   ELEMENT_TYPE_ELEMENT_BASE,             // Basic object of graphical elements
   ELEMENT_TYPE_HINT,                     // Hint
   ELEMENT_TYPE_LABEL,                    // Text label
   ELEMENT_TYPE_BUTTON,                   // Simple button
   ELEMENT_TYPE_BUTTON_TRIGGERED,         // Two-position button
   ELEMENT_TYPE_BUTTON_ARROW_UP,          // Up arrow button
   ELEMENT_TYPE_BUTTON_ARROW_DOWN,        // Down arrow button
   ELEMENT_TYPE_BUTTON_ARROW_LEFT,        // Left arrow button
   ELEMENT_TYPE_BUTTON_ARROW_RIGHT,       // Right arrow button
   ELEMENT_TYPE_CHECKBOX,                 // CheckBox control
   ELEMENT_TYPE_RADIOBUTTON,              // RadioButton control
   ELEMENT_TYPE_SCROLLBAR_THUMB_H,        // Horizontal scroll bar slider
   ELEMENT_TYPE_SCROLLBAR_THUMB_V,        // Vertical scroll bar slider
   ELEMENT_TYPE_SCROLLBAR_H,              // ScrollBarHorisontal control
   ELEMENT_TYPE_SCROLLBAR_V,              // ScrollBarVertical control
   ELEMENT_TYPE_TABLE_CELL_VIEW,          // Table cell (View)
   ELEMENT_TYPE_TABLE_ROW_VIEW,           // Table row (View)
   ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW,// Table column header (View)
   ELEMENT_TYPE_TABLE_HEADER_VIEW,        // Table header (View)
   ELEMENT_TYPE_TABLE_VIEW,               // Table (View)
   ELEMENT_TYPE_TABLE_CONTROL_VIEW,       // Table control (View)
   ELEMENT_TYPE_PANEL,                    // Panel control
   ELEMENT_TYPE_GROUPBOX,                 // GroupBox control
   ELEMENT_TYPE_CONTAINER,                // Container control
  };

Na função que retorna o nome curto do elemento de acordo com seu tipo, adicionaremos o novo tipo de objeto:

//+------------------------------------------------------------------+
//|  Return the short name of the element by type                    |
//+------------------------------------------------------------------+
string ElementShortName(const ENUM_ELEMENT_TYPE type)
  {
   switch(type)
     {
      case ELEMENT_TYPE_ELEMENT_BASE               :  return "BASE";    // Basic object of graphical elements
      case ELEMENT_TYPE_HINT                       :  return "HNT";     // Hint
      case ELEMENT_TYPE_LABEL                      :  return "LBL";     // Text label
      case ELEMENT_TYPE_BUTTON                     :  return "SBTN";    // Simple button
      case ELEMENT_TYPE_BUTTON_TRIGGERED           :  return "TBTN";    // Toggle button
      case ELEMENT_TYPE_BUTTON_ARROW_UP            :  return "BTARU";   // Up arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_DOWN          :  return "BTARD";   // Down arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_LEFT          :  return "BTARL";   // Left arrow button
      case ELEMENT_TYPE_BUTTON_ARROW_RIGHT         :  return "BTARR";   // Right arrow button
      case ELEMENT_TYPE_CHECKBOX                   :  return "CHKB";    // CheckBox control
      case ELEMENT_TYPE_RADIOBUTTON                :  return "RBTN";    // RadioButton control
      case ELEMENT_TYPE_SCROLLBAR_THUMB_H          :  return "THMBH";   // Horizontal scroll bar slider
      case ELEMENT_TYPE_SCROLLBAR_THUMB_V          :  return "THMBV";   // Vertical scroll bar slider
      case ELEMENT_TYPE_SCROLLBAR_H                :  return "SCBH";    // ScrollBarHorisontal control
      case ELEMENT_TYPE_SCROLLBAR_V                :  return "SCBV";    // ScrollBarVertical control
      case ELEMENT_TYPE_TABLE_CELL_VIEW            :  return "TCELL";   // Table cell (View)
      case ELEMENT_TYPE_TABLE_ROW_VIEW             :  return "TROW";    // Table row (View)
      case ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW  :  return "TCAPT";   // Table column header (View)
      case ELEMENT_TYPE_TABLE_HEADER_VIEW          :  return "THDR";    // Table header (View)
      case ELEMENT_TYPE_TABLE_VIEW                 :  return "TABLE";   // Table (View)
      case ELEMENT_TYPE_TABLE_CONTROL_VIEW         :  return "TBLCTRL"; // Table control (View)
      case ELEMENT_TYPE_PANEL                      :  return "PNL";     // Panel Table control
      case ELEMENT_TYPE_GROUPBOX                   :  return "GRBX";    // GroupBox control
      case ELEMENT_TYPE_CONTAINER                  :  return "CNTR";    // Container control
      default                                      :  return "Unknown"; // Unknown
     }
  }

Na classe base dos elementos gráficos, adicionaremos métodos que retornam as coordenadas do cursor:

//+------------------------------------------------------------------+
//| Base class of graphical elements                                 |
//+------------------------------------------------------------------+
class CBaseObj : public CObject
  {
protected:
   int               m_id;                                     // ID
   ushort            m_name[];                                 // Name
   
public:
//--- Set (1) name and (2) ID
   void              SetName(const string name)                { ::StringToShortArray(name,this.m_name);          }
   virtual void      SetID(const int id)                       { this.m_id=id;                                    }
//--- Return (1) name and (2) ID
   string            Name(void)                          const { return ::ShortArrayToString(this.m_name);        }
   int               ID(void)                            const { return this.m_id;                                }

//--- Return the cursor coordinates
   int               CursorX(void)                       const { return CCommonManager::GetInstance().CursorX();  }
   int               CursorY(void)                       const { return CCommonManager::GetInstance().CursorY();  }

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_BASE); }
   
//--- (1) Return and (2) display the object description in the journal
   virtual string    Description(void);
   virtual void      Print(void);
   
//--- Constructor/destructor
                     CBaseObj (void) : m_id(-1) { this.SetName(""); }
                    ~CBaseObj (void) {}
  };

Isso permitirá que qualquer elemento gráfico tenha acesso às coordenadas do cursor a qualquer momento. A classe singleton CCommonManager monitora continuamente as coordenadas do cursor, e qualquer elemento gráfico pode acessar essa classe para obter as coordenadas do cursor.

Na classe da área retangular, adicionaremos um método que desassocia um objeto da área:

//+------------------------------------------------------------------+
//| Rectangular region class                                         |
//+------------------------------------------------------------------+
class CBound : public CBaseObj
  {
protected:
   CBaseObj         *m_assigned_obj;                           // Object assigned to the region
   CRect             m_bound;                                  // Rectangular area structure

public:
//--- Change the bounding rectangular (1) width, (2) height and (3) size
   void              ResizeW(const int size)                   { this.m_bound.Width(size);                                    }
   void              ResizeH(const int size)                   { this.m_bound.Height(size);                                   }
   void              Resize(const int w,const int h)           { this.m_bound.Width(w); this.m_bound.Height(h);               }
   
//--- Set (1) X, (2) Y and (3) both coordinates of the bounding rectangle
   void              SetX(const int x)                         { this.m_bound.left=x;                                         }
   void              SetY(const int y)                         { this.m_bound.top=y;                                          }
   void              SetXY(const int x,const int y)            { this.m_bound.LeftTop(x,y);                                   }
   
//--- (1) Set and (2) shift the bounding rectangle by the specified coordinates/offset size
   void              Move(const int x,const int y)             { this.m_bound.Move(x,y);                                      }
   void              Shift(const int dx,const int dy)          { this.m_bound.Shift(dx,dy);                                   }
   
//--- Returns the object coordinates, dimensions, and boundaries
   int               X(void)                             const { return this.m_bound.left;                                    }
   int               Y(void)                             const { return this.m_bound.top;                                     }
   int               Width(void)                         const { return this.m_bound.Width();                                 }
   int               Height(void)                        const { return this.m_bound.Height();                                }
   int               Right(void)                         const { return this.m_bound.right-(this.m_bound.Width()  >0 ? 1 : 0);}
   int               Bottom(void)                        const { return this.m_bound.bottom-(this.m_bound.Height()>0 ? 1 : 0);}

//--- Returns the flag indicating whether the cursor is inside the area
   bool              Contains(const int x,const int y)   const { return this.m_bound.Contains(x,y);                           }
   
//--- (1) Assign, (2) unassign and (3) return the pointer to the assigned element
   void              AssignObject(CBaseObj *obj)               { this.m_assigned_obj=obj;                                     }
   void              UnassignObject(void)                      { this.m_assigned_obj=NULL;                                    }           
   CBaseObj         *GetAssignedObj(void)                      { return this.m_assigned_obj;                                  }
   
//--- Return the object description
   virtual string    Description(void);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_RECTANGLE_AREA);                         }
   
//--- Constructors/destructor
                     CBound(void) { ::ZeroMemory(this.m_bound); }
                     CBound(const int x,const int y,const int w,const int h) { this.SetXY(x,y); this.Resize(w,h);             }
                    ~CBound(void) { ::ZeroMemory(this.m_bound); }
  };

Se algum elemento gráfico for removido por código enquanto estiver associado a uma determinada área de outro elemento gráfico, seu ponteiro continuará registrado nessa área. Qualquer tentativa de acessar o elemento por meio desse ponteiro causará o encerramento do programa por erro crítico. Portanto, ao excluir um objeto, é necessário desassociá-lo da área caso esteja vinculado a ela. Atribuir NULL ao ponteiro permitirá verificar que ele não referencia mais um elemento gráfico válido depois que o objeto for removido.

As classes dos elementos gráficos da biblioteca em desenvolvimento foram implementadas de modo que, quando um objeto está vinculado a algum contêiner e ultrapassa seus limites, ele é recortado nas bordas desse contêiner. Se o elemento estiver completamente fora do contêiner, ele simplesmente será ocultado. Porém, quando o contêiner recebe um comando para ser movido para o primeiro plano, ele também move automaticamente para o primeiro plano todos os elementos vinculados a ele, percorrendo-os em um laço. Como consequência, os elementos ocultos tornam-se visíveis, pois mover um objeto para o primeiro plano equivale a executar, em sequência, os comandos de ocultar e exibir. Para evitar que elementos ocultos sejam movidos para o primeiro plano, precisamos adicionar às propriedades do elemento gráfico um flag indicando que ele está oculto por se encontrar fora dos limites do contêiner. No método que move o objeto para o primeiro plano, esse flag deverá ser verificado.

Na seção protected da classe base de canvas dos elementos gráficos CCanvasBase, declararemos esse flag:

protected:
   CCanvas          *m_background;                             // Background canvas
   CCanvas          *m_foreground;                             // Foreground canvas
   CCanvasBase      *m_container;                              // Parent container object
   CColorElement     m_color_background;                       // Background color control object
   CColorElement     m_color_foreground;                       // Foreground color control object
   CColorElement     m_color_border;                           // Border color control object
   
   CColorElement     m_color_background_act;                   // Activated element background color control object
   CColorElement     m_color_foreground_act;                   // Activated element foreground color control object
   CColorElement     m_color_border_act;                       // Activated element frame color control object
   
   CAutoRepeat       m_autorepeat;                             // Event auto-repeat control object
   
   ENUM_ELEMENT_STATE m_state;                                 // Control state (e.g. buttons (on/off))
   long              m_chart_id;                               // Chart ID
   int               m_wnd;                                    // Chart subwindow index
   int               m_wnd_y;                                  // Cursor Y coordinate offset in the subwindow
   int               m_obj_x;                                  // Graphical object X coordinate
   int               m_obj_y;                                  // Graphical object Y coordinate
   uchar             m_alpha_bg;                               // Background transparency
   uchar             m_alpha_fg;                               // Foreground transparency
   uint              m_border_width_lt;                        // Left frame width
   uint              m_border_width_rt;                        // Right frame width
   uint              m_border_width_up;                        // Top frame width
   uint              m_border_width_dn;                        // Bottom frame width
   string            m_program_name;                           // Program name
   bool              m_hidden;                                 // Hidden object flag
   bool              m_blocked;                                // Blocked element flag
   bool              m_movable;                                // Moved element flag
   bool              m_resizable;                              // Resizing flag
   bool              m_focused;                                // Element flag in focus
   bool              m_main;                                   // Main object flag
   bool              m_autorepeat_flag;                        // Event sending auto-repeat flag
   bool              m_scroll_flag;                            // Flag for scrolling content using scrollbars
   bool              m_trim_flag;                              // Flag for clipping the element to the container borders
   bool              m_cropped;                                // Flag indicating that the object is hidden outside the container borders  
   int               m_cursor_delta_x;                         // Distance from the cursor to the left edge of the element
   int               m_cursor_delta_y;                         // Distance from the cursor to the top edge of the element
   int               m_z_order;                                // Graphical object Z-order

Na seção public, adicionaremos um método que retorna esse flag:

public:
//--- (1) Set and (2) return the state
   void              SetState(ENUM_ELEMENT_STATE state)        { this.m_state=state; this.ColorsToDefault();                                       }
   ENUM_ELEMENT_STATE State(void)                        const { return this.m_state;                                                              }

//--- (1) Set and (2) return z-order
   bool              ObjectSetZOrder(const int value);
   int               ObjectZOrder(void)                  const { return this.m_z_order;                                                            }
   
//--- Return (1) the object's belonging to the program, the flag (2) of a hidden element, (3) a blocked element,
//--- (4) moved, (5) resized, (6) main element, (7) in focus, (8, 9) graphical object name (background, text)
   bool              IsBelongsToThis(const string name)  const { return(::ObjectGetString(this.m_chart_id,name,OBJPROP_TEXT)==this.m_program_name);}
   bool              IsHidden(void)                      const { return this.m_hidden;                                                             }
   bool              IsBlocked(void)                     const { return this.m_blocked;                                                            }
   bool              IsMovable(void)                     const { return this.m_movable;                                                            }
   bool              IsResizable(void)                   const { return this.m_resizable;                                                          }
   bool              IsMain(void)                        const { return this.m_main;                                                               }
   bool              IsFocused(void)                     const { return this.m_focused;                                                            }
   bool              IsAutorepeat(void)                  const { return this.m_autorepeat_flag;                                                    }
   bool              IsScrollable(void)                  const { return this.m_scroll_flag;                                                        }
   bool              IsTrimmed(void)                     const { return this.m_trim_flag;                                                          }
   bool              IsCropped(void)                     const { return this.m_cropped;                                                            }
   string            NameBG(void)                        const { return this.m_background.ChartObjectName();                                       }
   string            NameFG(void)                        const { return this.m_foreground.ChartObjectName();                                       }

um método que define o flag e um método virtual que retorna um flag indicando que o elemento está completamente fora dos limites do contêiner:

//--- Return the object boundaries considering the frame
   int               LimitLeft(void)                     const { return this.ObjectX()+(int)this.m_border_width_lt;                                }
   int               LimitRight(void)                    const { return this.ObjectRight()-(int)this.m_border_width_rt;                            }
   int               LimitTop(void)                      const { return this.ObjectY()+(int)this.m_border_width_up;                                }
   int               LimitBottom(void)                   const { return this.ObjectBottom()-(int)this.m_border_width_dn;                           }
   
//--- Set (1) movability, (2) main object flag for the object and (3) resizability,
//--- (4) auto-repeat events, (5) scrolling within the container and (6) clipping by the container borders
   void              SetMovable(const bool flag)               { this.m_movable=flag;                                                              }
   void              SetAsMain(void)                           { this.m_main=true;                                                                 }
   virtual void      SetResizable(const bool flag)             { this.m_resizable=flag;                                                            }
   void              SetAutorepeat(const bool flag)            { this.m_autorepeat_flag=flag;                                                      }
   void              SetScrollable(const bool flag)            { this.m_scroll_flag=flag;                                                          }
   virtual void      SetTrimmered(const bool flag)             { this.m_trim_flag=flag;                                                            }
   void              SetCropped(const bool flag)               { this.m_cropped=flag;                                                              }
   
//--- Return the flag that the object is located outside its container
   virtual bool      IsOutOfContainer(void);
//--- Limit the graphical object by the container dimensions
   virtual bool      ObjectTrim(void);

Implementação do método que retorna o flag indicando que o objeto está fora dos limites do contêiner:

//+------------------------------------------------------------------+
//| CCanvasBase::Return a flag that the object is                    |
//| located outside of its container                                 |
//+------------------------------------------------------------------+
bool CCanvasBase::IsOutOfContainer(void)
  {
//--- Return the result of checking that the object is completely outside the container
   return(this.Right() <= this.ContainerLimitLeft() || this.X() >= this.ContainerLimitRight() ||
          this.Bottom()<= this.ContainerLimitTop()  || this.Y() >= this.ContainerLimitBottom());
  }

O método verifica as coordenadas dos limites do objeto em relação aos limites do contêiner e retorna um flag indicando se o elemento está completamente fora dele. Para alguns elementos gráficos, essa condição pode precisar ser determinada de outra forma. Por isso, o método foi declarado como virtual.

No método que recorta o objeto gráfico pelos limites do contêiner, passaremos a controlar o novo flag:

//+-----------------------------------------------------------------------+
//| CCanvasBase::Crop a graphical object to the outline of its container  |
//+-----------------------------------------------------------------------+
bool CCanvasBase::ObjectTrim()
  {
//--- Check the element cropping permission flag and
//--- if the element should not be clipped by the container borders, return 'false'
   if(!this.m_trim_flag)
      return false;
//--- Get the container boundaries
   int container_left   = this.ContainerLimitLeft();
   int container_right  = this.ContainerLimitRight();
   int container_top    = this.ContainerLimitTop();
   int container_bottom = this.ContainerLimitBottom();
   
//--- Get the current object boundaries
   int object_left   = this.X();
   int object_right  = this.Right();
   int object_top    = this.Y();
   int object_bottom = this.Bottom();

//--- Check if the object is completely outside the container and hide it if it is
   if(this.IsOutOfContainer())
     {
      //--- Set the flag that the object is outside the container 
      this.m_cropped=true;
      //--- Hide the object and restore its dimensions
      this.Hide(false);
      if(this.ObjectResize(this.Width(),this.Height()))
         this.BoundResize(this.Width(),this.Height());
      return true;
     }
//--- The object is fully or partially located within the visible area of the container
   else
     {
      //--- Remove the flag indicating that the object is located outside the container
      this.m_cropped=false;
      //--- If the element is completely inside the container
      if(object_right<=container_right && object_left>=container_left &&
         object_bottom<=container_bottom && object_top>=container_top)
        {
         //--- If the width or height of the graphical object does not match the width or height of the element,
         //--- modify the graphical object according to the element dimensions and return 'true'
         if(this.ObjectWidth()!=this.Width() || this.ObjectHeight()!=this.Height())
           {
            if(this.ObjectResize(this.Width(),this.Height()))
               return true;
           }
        }
      //--- If the element is partially within the container visible area
      else
        {
         //--- If the element is vertically within the container visible area
         if(object_bottom<=container_bottom && object_top>=container_top)
           {
            //--- If the height of the graphic object does not match the height of the element,
            //--- modify the graphical object by the element height
            if(this.ObjectHeight()!=this.Height())
               this.ObjectResizeH(this.Height());
           }
         else
           {
            //--- If the element is horizontally within the container visible area
            if(object_right<=container_right && object_left>=container_left)
              {
               //--- If the width of the graphic object does not match the width of the element,
               //--- modify the graphical object by the element width
               if(this.ObjectWidth()!=this.Width())
                  this.ObjectResizeW(this.Width());
              }
           }
        }
     }
     
//--- Check whether the object extends horizontally and vertically beyond the container boundaries
   bool modified_horizontal=false;     // Horizontal change flag
   bool modified_vertical  =false;     // Vertical change flag
   
//--- Horizontal cropping
   int new_left = object_left;
   int new_width = this.Width();
//--- If the object extends beyond the container left border
   if(object_left<=container_left)
     {
      int crop_left=container_left-object_left;
      new_left=container_left;
      new_width-=crop_left;
      modified_horizontal=true;
     }
//--- If the object extends beyond the container right border
   if(object_right>=container_right)
     {
      int crop_right=object_right-container_right;
      new_width-=crop_right;
      modified_horizontal=true;
     }
//--- If there were changes horizontally
   if(modified_horizontal)
     {
      this.ObjectSetX(new_left);
      this.ObjectResizeW(new_width);
     }

//--- Vertical cropping
   int new_top=object_top;
   int new_height=this.Height();
//--- If the object extends beyond the top edge of the container
   if(object_top<=container_top)
     {
      int crop_top=container_top-object_top;
      new_top=container_top;
      new_height-=crop_top;
      modified_vertical=true;
     }
//--- If the object extends beyond the bottom border of the container 
   if(object_bottom>=container_bottom)
     {
      int crop_bottom=object_bottom-container_bottom;
      new_height-=crop_bottom;
      modified_vertical=true;
     }
//--- If there were vertical changes
   if(modified_vertical)
     {
      this.ObjectSetY(new_top);
      this.ObjectResizeH(new_height);
     }

//--- After calculations, the object may be hidden, but is now in the container area - display it
   this.Show(false);
      
//--- If the object has been changed, redraw it
   if(modified_horizontal || modified_vertical)
     {
      this.Update(false);
      this.Draw(false);
      return true;
     }
   return false;
  }

Com essa alteração, os objetos que estiverem completamente fora dos limites do contêiner não serão movidos para o primeiro plano e, portanto, não se tornarão visíveis caso seja enviado um comando para mover o próprio objeto ou todo o contêiner com seu conteúdo para o primeiro plano.

No método que move o objeto para o primeiro plano, verificaremos o flag definido no método analisado acima:

//+------------------------------------------------------------------+
//| CCanvasBase::Bring an object to the foreground                   |
//+------------------------------------------------------------------+
void CCanvasBase::BringToTop(const bool chart_redraw)
  {
   if(this.m_cropped)
      return;
   this.Hide(false);
   this.Show(chart_redraw);
  }

Se o flag estiver definido para o objeto, não será necessário mover o elemento para o primeiro plano, portanto saímos do método.

Cada elemento gráfico possui um manipulador comum para o giro da roda do mouse. Esse manipulador comum chama um método virtual responsável por tratar o giro da roda do mouse. Nesse caso, o valor de sparam do manipulador comum é repassado em sparam. Isso está incorreto, pois nenhum dos controles consegue determinar se a roda do mouse está sendo girada enquanto o cursor está sobre ele. No manipulador comum, conhecemos o nome do elemento ativo, isto é, daquele sobre o qual está o cursor. Portanto, ao chamar o manipulador da rolagem da roda do mouse, devemos passar em sparam o nome do elemento ativo. No próprio manipulador, basta comparar o nome do elemento com o valor de sparam. Se forem iguais, esse é exatamente o objeto sobre o qual a roda do mouse está sendo rolada. Faremos essa alteração no manipulador comum de eventos:

//--- Mouse wheel scroll event
   if(id==CHARTEVENT_MOUSE_WHEEL)
     {
      //--- If this is an active element, call its scroll wheel event handler
      if(this.IsCurrentActiveElement())
         this.OnWheelEvent(id,lparam,dparam,this.ActiveElementName());  // pass the name of the active element to sparam
     }


A comparação entre o nome do objeto e o valor de sparam será implementada no manipulador localizado em outro arquivo, juntamente com as demais alterações.

Abriremos o arquivo \MQL5\Indicators\Tables\ControlsControls.mqh. A partir de agora, faremos nele as próximas alterações.

Na seção de macros, adicionaremos novas definições:

//+------------------------------------------------------------------+
//| Macro substitutions                                              |
//+------------------------------------------------------------------+
#define  DEF_LABEL_W                50          // Text label default width
#define  DEF_LABEL_H                16          // Text label default height
#define  DEF_BUTTON_W               60          // Default button width
#define  DEF_BUTTON_H               16          // Default button height
#define  DEF_TABLE_ROW_H            16          // Default table row height
#define  DEF_TABLE_HEADER_H         20          // Default table header height
#define  DEF_TABLE_COLUMN_MIN_W     12          // Table column minimal width
#define  DEF_PANEL_W                80          // Default panel width
#define  DEF_PANEL_H                80          // Default panel height
#define  DEF_PANEL_MIN_W            60          // Minimum panel width
#define  DEF_PANEL_MIN_H            60          // Minimum panel height
#define  DEF_SCROLLBAR_TH           13          // Default scrollbar width
#define  DEF_THUMB_MIN_SIZE         8           // Minimum width of the scrollbar slider
#define  DEF_AUTOREPEAT_DELAY       500         // Delay before launching auto-repeat
#define  DEF_AUTOREPEAT_INTERVAL    100         // Auto-repeat frequency

#define  DEF_HINT_NAME_TOOLTIP      "HintTooltip"     // Tooltip name
#define  DEF_HINT_NAME_HORZ         "HintHORZ"        // "Double horizontal arrow" hint name
#define  DEF_HINT_NAME_VERT         "HintVERT"        // "Double vertical arrow" hint name
#define  DEF_HINT_NAME_NWSE         "HintNWSE"        // "Double arrow top-left" (NorthWest-SouthEast) hint name
#define  DEF_HINT_NAME_NESW         "HintNESW"        // "Double arrow bottom-left" (NorthEast-SouthWest) hint name
#define  DEF_HINT_NAME_SHIFT_HORZ   "HintShiftHORZ"   // "Horizontal offset arrow" hint name
#define  DEF_HINT_NAME_SHIFT_VERT   "HintShiftVERT"   // "Vertical offset arrow" hint name

A largura de uma coluna da tabela não pode ser inferior a 12 pixels, para evitar que as colunas fiquem estreitas demais ao serem redimensionadas. É mais conveniente definir os nomes das dicas por meio de diretivas do compilador e usá-las como nomes, pois, se for necessário alterar o nome de uma dica, bastará modificar a diretiva, sem precisar localizar e alterar todas as ocorrências desse nome em diferentes partes do código. Agora temos dois nomes para duas novas dicas. Elas serão exibidas quando o cursor passar sobre a borda do objeto que pode ser "puxada" para redimensioná-lo.

Adicionaremos uma nova enumeração para os modos de ordenação das colunas e novas constantes de enumeração:

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_ELEMENT_SORT_BY                       // Compared properties
  {
   ELEMENT_SORT_BY_ID   =  BASE_SORT_BY_ID,     // Comparison by element ID
   ELEMENT_SORT_BY_NAME =  BASE_SORT_BY_NAME,   // Comparison by element name
   ELEMENT_SORT_BY_X    =  BASE_SORT_BY_X,      // Comparison by element X coordinate
   ELEMENT_SORT_BY_Y    =  BASE_SORT_BY_Y,      // Comparison by element Y coordinate
   ELEMENT_SORT_BY_WIDTH=  BASE_SORT_BY_WIDTH,  // Comparison by element width
   ELEMENT_SORT_BY_HEIGHT= BASE_SORT_BY_HEIGHT, // Comparison by element height
   ELEMENT_SORT_BY_ZORDER= BASE_SORT_BY_ZORDER, // Comparison by element Z-order
   ELEMENT_SORT_BY_TEXT,                        // Comparison by element text
   ELEMENT_SORT_BY_COLOR_BG,                    // Comparison by element background color
   ELEMENT_SORT_BY_ALPHA_BG,                    // Comparison by element background transparency
   ELEMENT_SORT_BY_COLOR_FG,                    // Comparison by element foreground color
   ELEMENT_SORT_BY_ALPHA_FG,                    // Comparison by element foreground transparency color
   ELEMENT_SORT_BY_STATE,                       // Comparison by element state
   ELEMENT_SORT_BY_GROUP,                       // Comparison by element group
  };

enum ENUM_TABLE_SORT_MODE                       // Table column sorting modes
  {
   TABLE_SORT_MODE_NONE,                        // No sorting
   TABLE_SORT_MODE_ASC,                         // Sort in ascending order
   TABLE_SORT_MODE_DESC,                        // Sort in descending order
  };

enum ENUM_HINT_TYPE                             // Hint types
  {
   HINT_TYPE_TOOLTIP,                           // Tooltip
   HINT_TYPE_ARROW_HORZ,                        // Double horizontal arrow
   HINT_TYPE_ARROW_VERT,                        // Double vertical arrow
   HINT_TYPE_ARROW_NWSE,                        // Double arrow top-left --- bottom-right (NorthWest-SouthEast)
   HINT_TYPE_ARROW_NESW,                        // Double arrow bottom-left --- top-right (NorthEast-SouthWest)
   HINT_TYPE_ARROW_SHIFT_HORZ,                  // Horizontal offset arrow
   HINT_TYPE_ARROW_SHIFT_VERT,                  // Vertical offset arrow
  };

Na classe CImagePainter, responsável pelo desenho de imagens, declararemos dois novos métodos que desenham setas de redimensionamento horizontal e vertical:

//--- Clear the area
   bool              Clear(const int x,const int y,const int w,const int h,const bool update=true);
//--- Draw a filled (1) up, (2) down, (3) left and (4) right arrow
   bool              ArrowUp(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowDown(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowLeft(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowRight(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Draw (1) horizontal 17х7 and (2) vertical 7х17 double arrow
   bool              ArrowHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); 
   bool              ArrowVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true); 
   
//--- Draw a diagonal (1) top-left --- bottom-right and (2) bottom-left --- up-right 17x17 double arrow
   bool              ArrowNWSE(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowNESW(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Draw an 18x18 offset arrow (1) horizontally and (2) vertically
   bool              ArrowShiftHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              ArrowShiftVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Draw (1) checked and (2) unchecked CheckBox
   bool              CheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              UncheckedBox(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   
//--- Draw (1) checked and (2) unchecked RadioButton
   bool              CheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);
   bool              UncheckedRadioButton(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true);

Fora do corpo da classe, implementaremos os métodos declarados.

Método que desenha uma seta de deslocamento horizontal de 18x18:

//+------------------------------------------------------------------+
//| CImagePainter::Draw an 18x18 arrow with horizontal offset        |
//+------------------------------------------------------------------+
bool CImagePainter::ArrowShiftHorz(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound(__FUNCTION__))
      return false;

//--- Shape coordinates
   int arrx[25]={0, 3, 4, 4, 7, 7, 10, 10, 13, 13, 14, 17, 17, 14, 13, 13, 10, 10,  7,  7,  4,  4,  3, 0, 0};
   int arry[25]={8, 5, 5, 7, 7, 0,  0,  7,  7,  5,  5,  8,  9, 12, 12, 10, 10, 17, 17, 10, 10, 12, 12, 9, 8};
   
//--- Draw the white background
   this.m_canvas.Polyline(arrx,arry,::ColorToARGB(clrWhite,alpha));

//--- Draw the line of arrows
   this.m_canvas.FillRectangle(1,8, 16,9,::ColorToARGB(clr,alpha));
//--- Draw a dividing line
   this.m_canvas.FillRectangle(8,1, 9,16,::ColorToARGB(clr,alpha));
//--- Draw the left triangle
   this.m_canvas.Line(2,7, 2,10,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(3,6, 3,11,::ColorToARGB(clr,alpha));
//--- Draw the right triangle
   this.m_canvas.Line(14,6, 14,11,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(15,7, 15,10,::ColorToARGB(clr,alpha));

   if(update)
      this.m_canvas.Update(false);
   return true;
  }

Método que desenha uma seta de deslocamento vertical de 18x18:

//+------------------------------------------------------------------+
//| CImagePainter::Draw an 18x18 arrow with vertical offset          |
//+------------------------------------------------------------------+
bool CImagePainter::ArrowShiftVert(const int x,const int y,const int w,const int h,const color clr,const uchar alpha,const bool update=true)
  {
//--- If the image area is not valid, return 'false'
   if(!this.CheckBound(__FUNCTION__))
      return false;

//--- Shape coordinates
   int arrx[25]={0, 7, 7, 5, 5, 8, 9, 12, 12, 10, 10, 17, 17, 10, 10, 12, 12,  9,  8,  5,  5,  7,  7,  0, 0};
   int arry[25]={7, 7, 4, 4, 3, 0, 0,  3,  4,  4,  7,  7, 10, 10, 13, 13, 14, 17, 17, 14, 13, 13, 10, 10, 7};
   
//--- Draw the white background
   this.m_canvas.Polyline(arrx,arry,::ColorToARGB(clrWhite,alpha));

//--- Draw a dividing line
   this.m_canvas.FillRectangle(1,8, 16,9,::ColorToARGB(clr,alpha));
//--- Draw the line of arrows
   this.m_canvas.FillRectangle(8,1, 9,16,::ColorToARGB(clr,alpha));
//--- Draw the top triangle
   this.m_canvas.Line(7,2, 10,2,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(6,3, 11,3,::ColorToARGB(clr,alpha));
//--- Draw the bottom triangle
   this.m_canvas.Line(6,14, 11,14,::ColorToARGB(clr,alpha));
   this.m_canvas.Line(7,15, 10,15,::ColorToARGB(clr,alpha));

   if(update)
      this.m_canvas.Update(false);

   return true;
  }

Os dois métodos desenham dicas com setas que indicam o redimensionamento horizontal ( ) e vertical ( ) do elemento pelo arraste de sua borda.

Na classe base dos elementos gráficos CElementBase, tornaremos virtuais os métodos AddHintsArrowed e ShowCursorHint:

//--- Add an existing hint object to the list
   CVisualHint      *AddHint(CVisualHint *obj, const int dx, const int dy);
//--- (1) Add to the list and (2) remove tooltip objects with arrows from the list
   virtual bool      AddHintsArrowed(void);
   bool              DeleteHintsArrowed(void);
//--- Displays the resize cursor
   virtual bool      ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y);
   
//--- Handler for dragging element edges and corners
   virtual void      ResizeActionDragHandler(const int x, const int y);

No destrutor da classe, limparemos a lista de dicas:

//--- Constructors/destructor
                     CElementBase(void) { this.m_painter.CanvasAssign(this.GetForeground()); this.m_visible_in_container=true; }
                     CElementBase(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CElementBase(void) { this.m_list_hints.Clear(); }

Ao adicionar objetos às listas, primeiro procuramos na lista um objeto exatamente igual. Para que a busca seja realizada corretamente, a lista recebe temporariamente a ordenação pelo propriedade usada na comparação. No entanto, inicialmente a lista pode estar ordenada por outra propriedade. Para não alterar sua ordenação original, vamos salvá-la, definir a ordenação necessária para a busca e, ao final, restaurar a ordenação anterior.

No método que adiciona a dica especificada à lista, faremos a seguinte alteração:

//+------------------------------------------------------------------+
//| CElementBase::Add the specified hint object to the list          |
//+------------------------------------------------------------------+
bool CElementBase::AddHintToList(CVisualHint *obj)
  {
//--- If an empty pointer is passed, report this and return 'false'
   if(obj==NULL)
     {
      ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__);
      return false;
     }
//--- Save the list sorting method
   int sort_mode=this.m_list_hints.SortMode();
//--- Set the sorting flag for the list by ID
   this.m_list_hints.Sort(ELEMENT_SORT_BY_ID);
//--- If there is no such element in the list,
   if(this.m_list_hints.Search(obj)==NULL)
     {
      //--- return the list to its original sorting and get back the result of adding it to the list
      this.m_list_hints.Sort(sort_mode);
      return(this.m_list_hints.Add(obj)>-1);
     }
//--- Return the list to its original sorting
   this.m_list_hints.Sort(sort_mode);
//--- An element with this ID is already in the list - return 'false'
   return false;
  }

Nos métodos que trabalham com os nomes dos objetos de dica, passaremos a usar os nomes definidos anteriormente pelas diretivas:

//+------------------------------------------------------------------+
//| CElementBase::Add hint objects with arrows to the list           |
//+------------------------------------------------------------------+
bool CElementBase::AddHintsArrowed(void)
  {
//--- Arrays of names and hint types
   string array[4]={DEF_HINT_NAME_HORZ,DEF_HINT_NAME_VERT,DEF_HINT_NAME_NWSE,DEF_HINT_NAME_NESW};
   
   ENUM_HINT_TYPE type[4]={HINT_TYPE_ARROW_HORZ,HINT_TYPE_ARROW_VERT,HINT_TYPE_ARROW_NWSE,HINT_TYPE_ARROW_NESW};
   
//--- In the loop, create four hints with arrows
   bool res=true;
   for(int i=0;i<(int)array.Size();i++)
      res &=(this.CreateAndAddNewHint(type[i],array[i],0,0)!=NULL);
      
//--- If there were errors during creation, return 'false'
   if(!res)
      return false;
      
//--- In the loop through the array of names of hint objects
   for(int i=0;i<(int)array.Size();i++)
     {
      //--- get the next object by name,
      CVisualHint *obj=this.GetHint(array[i]);
      if(obj==NULL)
         continue;
      //--- hide the object and draw the appearance (arrows according to the object type)
      obj.Hide(false);
      obj.Draw(false);
     }
//--- All is successful
   return true;
  }

...

//+------------------------------------------------------------------+
//| CElementBase::Displays the resize cursor                         |
//+------------------------------------------------------------------+
bool CElementBase::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y)
  {
   CVisualHint *hint=NULL;          // Pointer to the hint
   int hint_shift_x=0;              // Hint offset by X
   int hint_shift_y=0;              // Hint offset by Y
   
//--- Depending on the location of the cursor on the element borders
//--- specify the tooltip offsets relative to the cursor coordinates,
//--- display the required hint on the chart and get the pointer to this object
   switch(edge)
     {
      //--- Cursor on the right or left border - horizontal double arrow
      case CURSOR_REGION_RIGHT         :
      case CURSOR_REGION_LEFT          :
         hint_shift_x=1;
         hint_shift_y=18;
         this.ShowHintArrowed(HINT_TYPE_ARROW_HORZ,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_HORZ);
        break;
    
      //--- Cursor at the top or bottom border - vertical double arrow
      case CURSOR_REGION_TOP           :
      case CURSOR_REGION_BOTTOM        :
         hint_shift_x=12;
         hint_shift_y=4;
         this.ShowHintArrowed(HINT_TYPE_ARROW_VERT,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_VERT);
        break;
    
      //--- Cursor in the upper left or lower right corner - a diagonal double arrow from top left to bottom right
      case CURSOR_REGION_LEFT_TOP      :
      case CURSOR_REGION_RIGHT_BOTTOM  :
         hint_shift_x=10;
         hint_shift_y=2;
         this.ShowHintArrowed(HINT_TYPE_ARROW_NWSE,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_NWSE);
        break;
    
      //--- Cursor in the lower left or upper right corner - a diagonal double arrow from bottom left to top right
      case CURSOR_REGION_LEFT_BOTTOM   :
      case CURSOR_REGION_RIGHT_TOP     :
         hint_shift_x=5;
         hint_shift_y=12;
         this.ShowHintArrowed(HINT_TYPE_ARROW_NESW,x+hint_shift_x,y+hint_shift_y);
         hint=this.GetHint(DEF_HINT_NAME_NESW);
        break;
      
      //--- By default, do nothing
      default: break;
     }

//--- Return the result of adjusting the position of the tooltip relative to the cursor
   return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false);
  }

e assim por diante.

Na classe do objeto de dica, declararemos dois novos métodos que desenham as duas novas dicas:

//+------------------------------------------------------------------+
//| Hint class                                                       |
//+------------------------------------------------------------------+
class CVisualHint : public CButton
  {
protected:
   ENUM_HINT_TYPE    m_hint_type;                              // Hint type

//--- Draw (1) a tooltip, (2) a horizontal, (3) a vertical arrow,
//--- arrows (4) top-left --- bottom-right, (5) bottom-left --- top-right,
//--- (6) horizontal and (7) vertical offset arrows
   void              DrawTooltip(void);
   void              DrawArrHorz(void);
   void              DrawArrVert(void);
   void              DrawArrNWSE(void);
   void              DrawArrNESW(void);
   void              DrawArrShiftHorz(void);
   void              DrawArrShiftVert(void);
   
//--- Initialize colors for the hint type (1) Tooltip, (2) arrows
   void              InitColorsTooltip(void);
   void              InitColorsArrowed(void);
   
public:
//--- (1) Set and (2) return the hint type
   void              SetHintType(const ENUM_HINT_TYPE type);
   ENUM_HINT_TYPE    HintType(void)                      const { return this.m_hint_type;             }

//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);

//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle)               { return CButton::Save(file_handle);   }
   virtual bool      Load(const int file_handle)               { return CButton::Load(file_handle);   }
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_HINT);           }
   
//--- Initialize (1) the class object and (2) default object colors
   void              Init(const string text);
   virtual void      InitColors(void);
   
//--- Constructors/destructor
                     CVisualHint(void);
                     CVisualHint(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CVisualHint (void) {}
  };

Fora do corpo da classe, implementaremos esses métodos:

//+------------------------------------------------------------------+
//| CVisualHint::Draw horizontal offset arrows                       |
//+------------------------------------------------------------------+
void CVisualHint::DrawArrShiftHorz(void)
  {
//--- Clear the drawing area
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Draw horizontal offset arrows
   this.m_painter.ArrowShiftHorz(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true);
  }
//+------------------------------------------------------------------+
//| CVisualHint::Draw vertical offset arrows                         |
//+------------------------------------------------------------------+
void CVisualHint::DrawArrShiftVert(void)
  {
//--- Clear the drawing area
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Draw horizontal offset arrows
   this.m_painter.ArrowShiftVert(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),this.ForeColor(),this.AlphaFG(),true);
  }

Adicionaremos o tratamento dos novos métodos de desenho das dicas aos métodos responsáveis por desenhá-las e definir seu tipo:

//+------------------------------------------------------------------+
//| CVisualHint::Set the hint type                                   |
//+------------------------------------------------------------------+
void CVisualHint::SetHintType(const ENUM_HINT_TYPE type)
  {
//--- If the passed type matches the set one, leave
   if(this.m_hint_type==type)
      return;
//--- Set a new hint type
   this.m_hint_type=type;
//--- Depending on the hint type, set the object dimensions
   switch(this.m_hint_type)
     {
      case HINT_TYPE_ARROW_HORZ        :  this.Resize(17,7);   break;
      case HINT_TYPE_ARROW_VERT        :  this.Resize(7,17);   break;
      case HINT_TYPE_ARROW_NESW        :
      case HINT_TYPE_ARROW_NWSE        :  this.Resize(13,13);  break;
      case HINT_TYPE_ARROW_SHIFT_HORZ  :
      case HINT_TYPE_ARROW_SHIFT_VERT  :  this.Resize(18,18);  break;
      default                          :  break;
     }
//--- Set the offset and dimensions of the image area,
//--- initialize colors based on the hint type
   this.SetImageBound(0,0,this.Width(),this.Height());
   this.InitColors();
  }
//+------------------------------------------------------------------+
//| CVisualHint::Draw the appearance                                 |
//+------------------------------------------------------------------+
void CVisualHint::Draw(const bool chart_redraw)
  {
//--- Depending on the type of hint, call the corresponding drawing method
   switch(this.m_hint_type)
     {
      case HINT_TYPE_ARROW_HORZ        :  this.DrawArrHorz();        break;
      case HINT_TYPE_ARROW_VERT        :  this.DrawArrVert();        break;
      case HINT_TYPE_ARROW_NESW        :  this.DrawArrNESW();        break;
      case HINT_TYPE_ARROW_NWSE        :  this.DrawArrNWSE();        break;
      case HINT_TYPE_ARROW_SHIFT_HORZ  :  this.DrawArrShiftHorz();   break;
      case HINT_TYPE_ARROW_SHIFT_VERT  :  this.DrawArrShiftVert();   break;
      default                          :  this.DrawTooltip();        break;
     }

//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

Vamos aprimorar a classe CPanel.

Adicionaremos métodos auxiliares para manipular as listas de elementos vinculados:

//+------------------------------------------------------------------+
//| Panel class                                                      |
//+------------------------------------------------------------------+
class CPanel : public CLabel
  {
private:
   CElementBase      m_temp_elm;                // Temporary object for element searching
   CBound            m_temp_bound;              // Temporary object for area searching
protected:
   CListElm          m_list_elm;                // List of attached elements
   CListElm          m_list_bounds;             // List of areas
//--- Add a new element to the list
   bool              AddNewElement(CElementBase *element);

public:
//--- Return the pointer to the list of (1) attached elements and (2) areas
   CListElm         *GetListAttachedElements(void)             { return &this.m_list_elm;                         }
   CListElm         *GetListBounds(void)                       { return &this.m_list_bounds;                      }
   
//--- Return the attached element by (1) index in the list, (2) ID and (3) specified object name
   CElementBase     *GetAttachedElementAt(const uint index)    { return this.m_list_elm.GetNodeAtIndex(index);    }
   CElementBase     *GetAttachedElementByID(const int id);
   CElementBase     *GetAttachedElementByName(const string name);
   
//--- Returns the number of added elements
   int               AttachedElementsTotal(void)         const { return this.m_list_elm.Total();                  }

//--- Return the area by (1) index in the list, (2) ID and (3) specified area name
   CBound           *GetBoundAt(const uint index)              { return this.m_list_bounds.GetNodeAtIndex(index); }
   CBound           *GetBoundByID(const int id);
   CBound           *GetBoundByName(const string name);
   
//--- Create and add (1) a new and (2) a previously created element to the list
   virtual CElementBase *InsertNewElement(const ENUM_ELEMENT_TYPE type,const string text,const string user_name,const int dx,const int dy,const int w,const int h);
   virtual CElementBase *InsertElement(CElementBase *element,const int dx,const int dy);
//--- Remove the specified element
   bool              DeleteElement(const int index)            { return this.m_list_elm.Delete(index);            }

//--- (1) Create and add a new area to the list and (2) remove the specified region
   CBound           *InsertNewBound(const string name,const int dx,const int dy,const int w,const int h);
   bool              DeleteBound(const int index)              { return this.m_list_bounds.Delete(index);         }
   
//--- (1) Assign an object to the specified area and (2) unassign an object from the specified area
   bool              AssignObjectToBound(const int bound, CBaseObj *object);
   bool              UnassignObjectFromBound(const int bound);

//--- Resize the object
   virtual bool      ResizeW(const int w);
   virtual bool      ResizeH(const int h);
   virtual bool      Resize(const int w,const int h);
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0) const;
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                          const { return(ELEMENT_TYPE_PANEL);                      }
  
//--- Initialize (1) the class object and (2) default object colors
   void              Init(void);
   virtual void      InitColors(void);
   
//--- Set new XY object coordinates
   virtual bool      Move(const int x,const int y);
//--- Shift the object by XY axes by the specified offset
   virtual bool      Shift(const int dx,const int dy);
//--- Set both the element coordinates and dimensions
   virtual bool      MoveXYWidthResize(const int x,const int y,const int w,const int h);
   
//--- (1) Hide and (2) display the object on all chart periods,
//--- (3) bring the object to the front, (4) block, (5) unblock the element,
   virtual void      Hide(const bool chart_redraw);
   virtual void      Show(const bool chart_redraw);
   virtual void      BringToTop(const bool chart_redraw);
   virtual void      Block(const bool chart_redraw);
   virtual void      Unblock(const bool chart_redraw);
   
//--- Display the object description in the journal
   virtual void      Print(void);
   
//--- Print a list of (1) attached objects and (2) areas
   void              PrintAttached(const uint tab=3);
   void              PrintBounds(void);

//--- Event handler
   virtual void      OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam);
   
//--- Timer event handler
   virtual void      TimerEventHandler(void);
   
//--- Constructors/destructor
                     CPanel(void);
                     CPanel(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CPanel (void) { this.m_list_elm.Clear(); this.m_list_bounds.Clear(); }
  };

No método ResizeW(), que altera a largura do elemento, existe um erro potencial:

//--- Get the pointer to the base element and, if it exists, its type is container,
//--- check the ratio of the current element dimensions to the container dimensions
//--- to display scrollbars in the container if necessary
   CContainer *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
      base.CheckElementSizes(&this);

Se o contêiner obtido pelo método GetContainer() não for um objeto da classe CContainer, o programa será encerrado com um erro crítico porque não é possível converter entre esses tipos de objeto.

Vamos corrigir isso:

//+------------------------------------------------------------------+
//| CPanel::Change the object width                                  |
//+------------------------------------------------------------------+
bool CPanel::ResizeW(const int w)
  {
   if(!this.ObjectResizeW(w))
      return false;
   this.BoundResizeW(w);
   this.SetImageSize(w,this.Height());
   if(!this.ObjectTrim())
     {
      this.Update(false);
      this.Draw(false);
     }
//--- Get the pointer to the base element and, if it exists, its type is container,
//--- check the ratio of the current element dimensions to the container dimensions
//--- to display scrollbars in the container if necessary
   CContainer  *container=NULL;
   CCanvasBase *base=this.GetContainer();
   if(base!=NULL && base.Type()==ELEMENT_TYPE_CONTAINER)
     {
      container=base;
      container.CheckElementSizes(&this);
     }
      
//--- In the loop through the attached elements, trim each element along the container boundaries
   int total=this.m_list_elm.Total();
   for(int i=0;i<total;i++)
     {
      CElementBase *elm=this.GetAttachedElementAt(i);
      if(elm!=NULL)
         elm.ObjectTrim();
     }
//--- All is successful
   return true;
  }

Agora podemos atribuir com segurança ao ponteiro o objeto do tipo correto.

No método que adiciona um novo elemento à lista, salvaremos a ordenação original da lista e a restauraremos depois que o objeto for adicionado:

//+------------------------------------------------------------------+
//| CPanel::Add a new element to the list                            |
//+------------------------------------------------------------------+
bool CPanel::AddNewElement(CElementBase *element)
  {
//--- If an empty pointer is passed, report this and return 'false'
   if(element==NULL)
     {
      ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__);
      return false;
     }
//--- Save the list sorting method
   int sort_mode=this.m_list_elm.SortMode();
//--- Set the sorting flag for the list by ID
   this.m_list_elm.Sort(ELEMENT_SORT_BY_ID);
//--- If there is no such element in the list,
   if(this.m_list_elm.Search(element)==NULL)
     {
      //--- return the list to its original sorting and get back the result of adding it to the list
      this.m_list_elm.Sort(sort_mode);
      return(this.m_list_elm.Add(element)>-1);
     }
//--- Return the list to its original sorting
   this.m_list_elm.Sort(sort_mode);
//--- An element with this ID is already in the list - return 'false'
   return false;
  }

Vamos aprimorar o método que cria uma nova área e a adiciona à lista:

//+------------------------------------------------------------------+
//| Create and add a new area to the list                            |
//+------------------------------------------------------------------+
CBound *CPanel::InsertNewBound(const string name,const int dx,const int dy,const int w,const int h)
  {
//--- Check whether the list contains a region with the specified name and, if it does, report this and return NULL
   this.m_temp_bound.SetName(name);
//--- Save the list sorting method
   int sort_mode=this.m_list_bounds.SortMode();
//--- Set the sorting by name flag to the list
   this.m_list_bounds.Sort(ELEMENT_SORT_BY_NAME);
   if(this.m_list_bounds.Search(&this.m_temp_bound)!=NULL)
     {
      //--- Return the list to its original sorting, report that such an object already exists and return NULL
      this.m_list_bounds.Sort(sort_mode);
      ::PrintFormat("%s: Error. An area named \"%s\" is already in the list",__FUNCTION__,name);
      return NULL;
     }
//--- Return the list to its original sorting
   this.m_list_bounds.Sort(sort_mode);
//--- Create a new area object; if unsuccessful, report it and return NULL
   CBound *bound=new CBound(dx,dy,w,h);
   if(bound==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create CBound object",__FUNCTION__);
      return NULL;
     }
//--- Set the area name and ID, and return the pointer to the object
   bound.SetName(name);
   bound.SetID(this.m_list_bounds.Total());
//--- If failed to add the new object to the list, report this, remove the object and return NULL
   if(this.m_list_bounds.Add(bound)==-1)
     {
      ::PrintFormat("%s: Error. Failed to add CBound object to list",__FUNCTION__);
      delete bound;
      return NULL;
     }
   return bound;
  }

A classe possui dois métodos que foram declarados, mas ainda não foram implementados. Vamos corrigir isso.

Método que retorna uma área pelo identificador:

//+------------------------------------------------------------------+
//| CPanel::Return area by ID                                        |
//+------------------------------------------------------------------+
CBound *CPanel::GetBoundByID(const int id)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      CBound *bound=this.GetBoundAt(i);
      if(bound!=NULL && bound.ID()==id)
         return bound;
     }
   return NULL;
  }

Em um laço simples pelos objetos das áreas do elemento, procuramos a área com o identificador especificado e retornamos um ponteiro para o objeto encontrado.

Método que retorna uma área pelo nome atribuído:

//+------------------------------------------------------------------+
//| CPanel::Return the area by the assigned area name                |
//+------------------------------------------------------------------+
CBound *CPanel::GetBoundByName(const string name)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      CBound *bound=this.GetBoundAt(i);
      if(bound!=NULL && bound.Name()==name)
         return bound;
     }
   return NULL;
  }

Em um laço simples pelos objetos das áreas do elemento, procuramos a área com o nome especificado e retornamos um ponteiro para o objeto encontrado.

Agora implementaremos os dois métodos declarados.

Método que associa um objeto à área especificada:

//+------------------------------------------------------------------+
//| CPanel::Assign an object to the specified area                   |
//+------------------------------------------------------------------+
bool CPanel::AssignObjectToBound(const int bound,CBaseObj *object)
  {
   CBound *bound_obj=this.GetBoundAt(bound);
   if(bound_obj==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get Bound at index %d",__FUNCTION__,bound);
      return false;
     }
   bound_obj.AssignObject(object);
   return true;
  }

Obtemos a área pelo identificador e chamamos o método do objeto da área que associa o objeto a ela.

Método que desassocia o objeto da área especificada:

//+------------------------------------------------------------------+
//| CPanel::Unassign an object from the specified area               |
//+------------------------------------------------------------------+
bool CPanel::UnassignObjectFromBound(const int bound)
  {
   CBound *bound_obj=this.GetBoundAt(bound);
   if(bound_obj==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get Bound at index %d",__FUNCTION__,bound);
      return false;
     }
   bound_obj.UnassignObject();
   return true;
  }

Obtemos a área pelo identificador e chamamos o método do objeto da área que desassocia o objeto anteriormente vinculado.

Nas classes das barras de rolagem horizontal e vertical, existe uma falha que pode fazer com que vários controles deslizantes sejam movimentados ao mesmo tempo quando a roda do mouse é acionada, caso haja mais de um contêiner no gráfico. Para corrigir esse comportamento, precisamos ler em sparam o nome do elemento ativo e compará-lo com o nome do controle deslizante. Se o nome do controle deslizante contiver como substring o nome do elemento ativo, então é esse controle deslizante que está sendo movimentado. Faremos as alterações nos manipuladores do giro da roda do mouse das duas classes de controles deslizantes das barras de rolagem:

//+------------------------------------------------------------------+
//| CScrollBarThumbH::Wheel scroll handler                           |
//+------------------------------------------------------------------+
void CScrollBarThumbH::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Get the pointer to the base object (the "horizontal scroll bar" control)
   CCanvasBase *base_obj=this.GetContainer();
   
//--- Get the name of the main object in the hierarchy by the value in sparam
   string array_names[];
   string name_main=(GetElementNames(sparam,"_",array_names)>0 ? array_names[0] : "");
   
//--- If the main object in the hierarchy is not ours, leave
   if(::StringFind(this.NameFG(),name_main)!=0)
      return;
      
//--- If the slider's movability flag is not set, or the pointer to the base object is not received, leave
   if(!this.IsMovable() || base_obj==NULL)
      return;
   
//--- Get the width of the base object and calculate the boundaries of the space for the slider
   int base_w=base_obj.Width();
   int base_left=base_obj.X()+base_obj.Height();
   int base_right=base_obj.Right()-base_obj.Height()+1;
   
//--- Set the offset direction depending on the mouse wheel rotation direction
   int dx=(dparam<0 ? 2 : dparam>0 ? -2 : 0);
   if(dx==0)
      dx=(int)lparam;

//--- If the slider goes beyond the left edge of its area when moving, set it to the left edge
   if(dx<0 && this.X()+dx<=base_left)
      this.MoveX(base_left);
//--- otherwise, if the slider moves beyond the right edge of its area, position it along the right edge
   else if(dx>0 && this.Right()+dx>=base_right)
      this.MoveX(base_right-this.Width());
//--- Otherwise, if the slider is within its area, move it by the offset value
   else
     {
      this.ShiftX(dx);
     }

//--- Calculate the slider position
   int thumb_pos=this.X()-base_left;
   
//--- Get cursor coordinates
   int x=CCommonManager::GetInstance().CursorX();
   int y=CCommonManager::GetInstance().CursorY();
   
//--- If the cursor lands on the slider, change the color to "In focus",
   if(this.Contains(x,y))
      this.OnFocusEvent(id,lparam,dparam,sparam);
//--- otherwise, return the color to "Default"
   else
      this.OnReleaseEvent(id,lparam,dparam,sparam);
      
//--- Send a custom event to the chart with the slider position in lparam and the object name in sparam
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG());
//--- Redraw the chart
   if(this.m_chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

//+------------------------------------------------------------------+
//| CScrollBarThumbV::Wheel scroll handler                           |
//+------------------------------------------------------------------+
void CScrollBarThumbV::OnWheelEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Get the pointer to the base object (the "vertical scroll bar" control)
   CCanvasBase *base_obj=this.GetContainer();
   
//--- Get the name of the main object in the hierarchy by the value in sparam
   string array_names[];
   string name_main=(GetElementNames(sparam,"_",array_names)>0 ? array_names[0] : "");
   
//--- If the main object in the hierarchy is not ours, leave
   if(::StringFind(this.NameFG(),name_main)!=0)
      return;
      
//--- If the slider's movability flag is not set, or the pointer to the base object is not received, leave
   if(!this.IsMovable() || base_obj==NULL)
      return;
   
//--- Get the height of the base object and calculate the boundaries of the space for the slider
   int base_h=base_obj.Height();
   int base_top=base_obj.Y()+base_obj.Width();
   int base_bottom=base_obj.Bottom()-base_obj.Width()+1;
   
//--- Set the offset direction depending on the mouse wheel rotation direction
   int dy=(dparam<0 ? 2 : dparam>0 ? -2 : 0);
   if(dy==0)
      dy=(int)lparam;

//--- If the slider goes beyond the top edge of its area when moving, set it to the top edge
   if(dy<0 && this.Y()+dy<=base_top)
      this.MoveY(base_top);
//--- otherwise, if the slider moves beyond the bottom edge of its area, position it along the bottom edge
   else if(dy>0 && this.Bottom()+dy>=base_bottom)
      this.MoveY(base_bottom-this.Height());
//--- Otherwise, if the slider is within its area, move it by the offset value
   else
     {
      this.ShiftY(dy);
     }

//--- Calculate the slider position
   int thumb_pos=this.Y()-base_top;
   
//--- Get cursor coordinates
   int x=CCommonManager::GetInstance().CursorX();
   int y=CCommonManager::GetInstance().CursorY();
   
//--- If the cursor lands on the slider, change the color to "In focus",
   if(this.Contains(x,y))
      this.OnFocusEvent(id,lparam,dparam,sparam);
//--- otherwise, return the color to "Default"
   else
      this.OnReleaseEvent(id,lparam,dparam,sparam);
      
//--- Send a custom event to the chart with the slider position in lparam and the object name in sparam
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_MOUSE_WHEEL, thumb_pos, dparam, this.NameFG());
//--- Redraw the chart
   if(this.m_chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

//+-------------------------------------------------------------------+
//|CContainer::Shift the content horizontally by the specified value  |
//+-------------------------------------------------------------------+
bool CContainer::ContentShiftHorz(const int value)
  {
//--- Get the pointer to the container contents
   CElementBase *elm=this.GetAttachedElement();
   if(elm==NULL)
      return false;
   
//--- Get the table header for the CTableView element
   CElementBase     *elm_container=elm.GetContainer();
   CTableHeaderView *table_header=NULL;
   if(elm_container!=NULL && ::StringFind(elm.Name(),"Table")==0)
     {
      CElementBase *obj=elm_container.GetContainer();
      if(obj!=NULL && obj.Type()==ELEMENT_TYPE_TABLE_VIEW)
        {
         CTableView *table_view=obj;
         table_header=table_view.GetHeader();
        }
     }
//--- Calculate the offset value based on the slider position
   int content_offset=this.CalculateContentOffsetHorz(value);

//--- Shift the header
   bool res=true;
   if(table_header!=NULL)
     {
      res &=table_header.MoveX(this.X()-content_offset);
     }
     
//--- Return the result of shifting the content by the calculated value
   res &=elm.MoveX(this.X()-content_offset);
   return res;
  }

Na classe CContainer, ao deslocar horizontalmente o conteúdo do contêiner, é importante considerar que, no caso de uma tabela, a rolagem horizontal também deve deslocar o cabeçalho da tabela. Vamos implementar isso:

//--- Get the names of all elements in the hierarchy (if an error occurs, return -1)
   string names[]={};
   int total = GetElementNames(name,"_",names);
   if(total==WRONG_VALUE)
      return WRONG_VALUE;
      
//--- If the name of the base element in the hierarchy does not match the name of the container, then this is not our event - leave
   string base_name=names[0];
   if(base_name!=this.NameFG())
      return WRONG_VALUE;

No método que retorna o tipo do elemento que gerou o evento, na classe do contêiner, a busca pelo elemento base estava sendo realizada de forma incorreta:

//+------------------------------------------------------------------+
//| Return the type of the element that sent the event               |
//+------------------------------------------------------------------+
ENUM_ELEMENT_TYPE CContainer::GetEventElementType(const string name)
  {
//--- Get the names of all elements in the hierarchy (if an error occurs, return -1)
   string names[]={};
   int total = GetElementNames(name,"_",names);
   if(total==WRONG_VALUE)
      return WRONG_VALUE;
   
//--- Find the container name in the array that is closest to the name of the element with the event
   int    cntr_index=-1;      // Index of the container name in the array of names in the element hierarchy
   string cntr_name="";       // The name of the container in the array of names in the element hierarchy
   
//--- Search in the loop for the very first occurrence of the CNTR substring from the end
   for(int i=total-1;i>=0;i--)
     {
      if(::StringFind(names[i],"CNTR")==0)
        {
         cntr_name=names[i];
         cntr_index=i;
         break;
        }
     }
//--- If the container name is not found in the array (index is -1), return -1
   if(cntr_index==WRONG_VALUE)
      return WRONG_VALUE;
   
//--- If the element name does not contain a substring with the name of the base element, then this is not our event - leave
   string base_name=names[cntr_index];
   if(::StringFind(this.NameFG(),base_name)==WRONG_VALUE)
      return WRONG_VALUE;

//--- Events that do not arrive from scrollbars are skipped
   string check_name=::StringSubstr(names[cntr_index+1],0,4);
   if(check_name!="SCBH" && check_name!="SCBV")
      return WRONG_VALUE;
      
//--- Get the name of the element the event came from and initialize the element type
   string elm_name=names[names.Size()-1];
   ENUM_ELEMENT_TYPE type=WRONG_VALUE;
   
//--- Check and write the element type
//--- Up arrow button
   if(::StringFind(elm_name,"BTARU")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_UP;
//--- Down arrow button
   else if(::StringFind(elm_name,"BTARD")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_DOWN;
//--- Left arrow button
   else if(::StringFind(elm_name,"BTARL")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_LEFT;
//--- Right arrow button
   else if(::StringFind(elm_name,"BTARR")==0)
      type=ELEMENT_TYPE_BUTTON_ARROW_RIGHT;
//--- Horizontal scroll bar slider
   else if(::StringFind(elm_name,"THMBH")==0)
      type=ELEMENT_TYPE_SCROLLBAR_THUMB_H;
//--- Vertical scroll bar slider
   else if(::StringFind(elm_name,"THMBV")==0)
      type=ELEMENT_TYPE_SCROLLBAR_THUMB_V;
//--- ScrollBarHorisontal control
   else if(::StringFind(elm_name,"SCBH")==0)
      type=ELEMENT_TYPE_SCROLLBAR_H;
//--- ScrollBarVertical control
   else if(::StringFind(elm_name,"SCBV")==0)
      type=ELEMENT_TYPE_SCROLLBAR_V;
      
//--- Return the element type
   return type;
  }

O objeto base nem sempre ocupa a primeira posição na hierarquia de elementos gráficos do contêiner. Pode haver casos de vários níveis de aninhamento de um elemento dentro de outro e, nesse caso, para os elementos mais internos da hierarquia, o objeto base não será o primeiro na lista de nomes de todos os elementos aninhados no contêiner. Vamos corrigir a busca pelo objeto base:

//+------------------------------------------------------------------+
//| Table cell visual representation class                           |
//+------------------------------------------------------------------+
class CTableCellView : public CBoundedObj
  {
protected:
   CTableCell       *m_table_cell_model;                       // Pointer to the cell model
   CImagePainter    *m_painter;                                // Pointer to the drawing object
   CTableRowView    *m_element_base;                           // Pointer to the base element (table row)
   CCanvas          *m_background;                             // Pointer to the background canvas
   CCanvas          *m_foreground;                             // Pointer to the foreground canvas
   int               m_index;                                  // Index in the cell list
   ENUM_ANCHOR_POINT m_text_anchor;                            // Text anchor point (alignment in cell)
   int               m_text_x;                                 // Text X coordinate (offset relative to the left border of the object area)
   int               m_text_y;                                 // Y text coordinate (offset relative to the top border of the object area)
   ushort            m_text[];                                 // Text
   color             m_fore_color;                             // Foreground color
   
//--- Return the offsets of the initial drawing coordinates on the canvas relative to the canvas and the coordinates of the base element
   int               CanvasOffsetX(void)     const { return(this.m_element_base.ObjectX()-this.m_element_base.X());  }
   int               CanvasOffsetY(void)     const { return(this.m_element_base.ObjectY()-this.m_element_base.Y());  }
   
//--- Return the adjusted coordinate of a point on the canvas, taking into account the offset of the canvas relative to the base element
   int               AdjX(const int x)                            const { return(x-this.CanvasOffsetX());            }
   int               AdjY(const int y)                            const { return(y-this.CanvasOffsetY());            }

//--- Return the X and Y coordinates of the text based on the anchor point
   bool              GetTextCoordsByAnchor(int &x, int &y, int &dir_x, int dir_y);

//--- Return the pointer to the table row panel container
   CContainer       *GetRowsPanelContainer(void);
   
public:
//--- Return the pointer to the specified (1) background and (2) foreground canvas
   CCanvas          *GetBackground(void)                                { return this.m_background;                  }
   CCanvas          *GetForeground(void)                                { return this.m_foreground;                  }

//--- Get the boundaries of the parent container object
   int               ContainerLimitLeft(void)   const { return(this.m_element_base==NULL ? this.X()      :  this.m_element_base.LimitLeft());   }
   int               ContainerLimitRight(void)  const { return(this.m_element_base==NULL ? this.Right()  :  this.m_element_base.LimitRight());  }
   int               ContainerLimitTop(void)    const { return(this.m_element_base==NULL ? this.Y()      :  this.m_element_base.LimitTop());    }
   int               ContainerLimitBottom(void) const { return(this.m_element_base==NULL ? this.Bottom() :  this.m_element_base.LimitBottom()); }

//--- Return the flag that the object is located outside its container
   virtual bool      IsOutOfContainer(void);

//--- (1) Set and (2) return the cell text
   void              SetText(const string text)                         { ::StringToShortArray(text,this.m_text);    }
   string            Text(void)                                   const { return ::ShortArrayToString(this.m_text);  }

//--- (1) Set and (2) return the cell text color
   void              SetForeColor(const color clr)                      { this.m_fore_color=clr;                     }
   color             ForeColor(void)                              const { return this.m_fore_color;                  }

//--- Set the ID
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                 }
//--- (1) Set and (2) return the cell index
   void              SetIndex(const int index)                          { this.SetID(index);                         }
   int               Index(void)                                  const { return this.m_index;                       }

//--- (1) Set and (2) return the text offset along the X axis
   void              SetTextShiftX(const int shift)                     { this.m_text_x=shift;                       }
   int               TextShiftX(void)                             const { return this.m_text_x;                      }
   
//--- (1) Set and (2) return the text offset along the Y axis
   void              SetTextShiftY(const int shift)                     { this.m_text_y=shift;                       }
   int               TextShiftY(void)                             const { return this.m_text_y;                      }
   
//--- (1) Set and (2) return the text anchor point
   void              SetTextAnchor(const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw);
   int               TextAnchor(void)                             const { return this.m_text_anchor;                 }
   
//--- Sets the anchor point and text offsets
   void              SetTextPosition(const ENUM_ANCHOR_POINT anchor,const int shift_x,const int shift_y,const bool cell_redraw,const bool chart_redraw);

//--- Assign the base element (table row)
   void              RowAssign(CTableRowView *base_element);
   
//--- (1) Assign and (2) return the cell model
   bool              TableCellModelAssign(CTableCell *cell_model,int dx,int dy,int w,int h);
   CTableCell       *GetTableCellModel(void)                            { return this.m_table_cell_model;            }

//--- Print the assigned cell model in the journal
   void              TableCellModelPrint(void);
   
//--- (1) Fill the object with the background color, (2) update the object to reflect the changes and (3) draw the appearance
   virtual void      Clear(const bool chart_redraw);
   virtual void      Update(const bool chart_redraw);
   virtual void      Draw(const bool chart_redraw);
   
//--- Display the text
   virtual void      DrawText(const int dx, const int dy, const string text, const bool chart_redraw);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0)const { return CBaseObj::Compare(node,mode);       }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_CELL_VIEW);      }
   
//--- Initialize a class object
   void              Init(const string text);
   
//--- Return the object description
   virtual string    Description(void);
   
//--- Constructors/destructor
                     CTableCellView(void);
                     CTableCellView(const int id, const string user_name, const string text, const int x, const int y, const int w, const int h);
                    ~CTableCellView (void){}
  };

Agora vamos aprimorar a classe de representação visual da célula da tabela CTableCellView. As alterações incluem impedir a renderização de células fora dos limites do contêiner: não é necessário desenhar uma célula que esteja fora dos limites de seu contêiner, evitando gastar recursos renderizando conteúdo fora da área visível do contêiner. Também adicionaremos a possibilidade de alterar a cor do texto exibido na célula e corrigiremos seu posicionamento para diferentes tipos de ponto de ancoragem.

Declararemos novas variáveis e métodos:

//+---------------------------------------------------------------------+
//| CTableCellView::Assigns the row, background and foreground canvases |
//+---------------------------------------------------------------------+
void CTableCellView::RowAssign(CTableRowView *base_element)
  {
   if(base_element==NULL)
     {
      ::PrintFormat("%s: Error. Empty element passed",__FUNCTION__);
      return;
     }
   this.m_element_base=base_element;
   this.m_background=this.m_element_base.GetBackground();
   this.m_foreground=this.m_element_base.GetForeground();
   this.m_painter=this.m_element_base.Painter();
   this.m_fore_color=this.m_element_base.ForeColor();
  }

No método que associa a célula à linha e define os canvas de fundo e de primeiro plano, atribuiremos ao texto a mesma cor do primeiro plano:

//+------------------------------------------------------------------+
//| CTableCellView::Return the X and Y coordinates of the text       |
//| depending on the anchor point                                    |
//+------------------------------------------------------------------+
bool CTableCellView::GetTextCoordsByAnchor(int &x,int &y, int &dir_x,int dir_y)
  {
//--- Get the text size in the cell
   int text_w=0, text_h=0;
   this.m_foreground.TextSize(this.Text(),text_w,text_h);
   if(text_w==0 || text_h==0)
      return false;
//--- Depending on the text anchor point in the cell,
//--- calculate its initial coordinates (upper left corner)
   switch(this.m_text_anchor)
     {
      //--- Anchor point left centered
      case ANCHOR_LEFT :
        x=0;
        y=(this.Height()-text_h)/2;
        dir_x=1;
        dir_y=1;
        break;
      //--- Anchor point in the lower left corner
      case ANCHOR_LEFT_LOWER :
        x=0;
        y=this.Height()-text_h;
        dir_x= 1;
        dir_y=-1;
        break;
      //--- Anchor point at the bottom center
      case ANCHOR_LOWER :
        x=(this.Width()-text_w)/2;
        y=this.Height()-text_h;
        dir_x= 1;
        dir_y=-1;
        break;
      //--- Anchor point in the lower right corner
      case ANCHOR_RIGHT_LOWER :
        x=this.Width()-text_w;
        y=this.Height()-text_h;
        dir_x=-1;
        dir_y=-1;
        break;
      //--- Anchor point is right centered
      case ANCHOR_RIGHT :
        x=this.Width()-text_w;
        y=(this.Height()-text_h)/2;
        dir_x=-1;
        dir_y= 1;
        break;
      //--- Anchor point in the upper right corner
      case ANCHOR_RIGHT_UPPER :
        x=this.Width()-text_w;
        y=0;
        dir_x=-1;
        dir_y= 1;
        break;
      //--- Anchor point at top center
      case ANCHOR_UPPER :
        x=(this.Width()-text_w)/2;
        y=0;
        dir_x=1;
        dir_y=1;
        break;
      //--- Anchor point is strictly in the center of the object
      case ANCHOR_CENTER :
        x=(this.Width()-text_w)/2;
        y=(this.Height()-text_h)/2;
        dir_x=1;
        dir_y=1;
        break;
      //--- Anchor point in the upper left corner
      //---ANCHOR_LEFT_UPPER
      default:
        x=0;
        y=0;
        dir_x=1;
        dir_y=1;
        break;
     }
   return true;
  }

No método que retorna as coordenadas X e Y do texto de acordo com o ponto de ancoragem, adicionaremos variáveis nas quais serão armazenados os sinais do deslocamento (+1 / -1) nos eixos X e Y:

//+------------------------------------------------------------------+
//| CTableCellView::Draw the appearance                              |
//+------------------------------------------------------------------+
void CTableCellView::Draw(const bool chart_redraw)
  {
//--- If the cell is outside the table row container, leave
   if(this.IsOutOfContainer())
      return;
      
//--- Get the text coordinates and the offset direction depending on the anchor point
   int text_x=0, text_y=0;
   int dir_horz=0, dir_vert=0;
   if(!this.GetTextCoordsByAnchor(text_x,text_y,dir_horz,dir_vert))
      return;
//--- Correct the text coordinates
   int x=this.AdjX(this.X()+text_x);
   int y=this.AdjY(this.Y()+text_y);
   
//--- Set the coordinates of the dividing line
   int x1=this.AdjX(this.X());
   int x2=this.AdjX(this.X());
   int y1=this.AdjY(this.Y());
   int y2=this.AdjY(this.Bottom());
   
//--- Display the text on the foreground canvas taking into account the offset direction without updating the chart
   this.DrawText(x+this.m_text_x*dir_horz,y+this.m_text_y*dir_vert,this.Text(),false);
   
//--- If this is not the rightmost cell, draw a vertical dividing line near the cell on the right
   if(this.m_element_base!=NULL && this.Index()<this.m_element_base.CellsTotal()-1)
     {
      int line_x=this.AdjX(this.Right());
      this.m_background.Line(line_x,y1,line_x,y2,::ColorToARGB(this.m_element_base.BorderColor(),this.m_element_base.AlphaBG()));
     }
//--- Update the background canvas with the specified chart redraw flag
   this.m_background.Update(chart_redraw);
  }

No método que desenha o elemento, passaremos a usar os valores obtidos como multiplicadores dos deslocamentos do texto nos eixos X e Y. Além disso, a célula não será desenhada se estiver fora dos limites de seu contêiner:

//+------------------------------------------------------------------+
//| CTableCellView::Return the pointer                               |
//| to the table row panel container                                 |
//+------------------------------------------------------------------+
CContainer *CTableCellView::GetRowsPanelContainer(void)
  {
//--- Check the row
   if(this.m_element_base==NULL)
      return NULL;
//--- Get the panel for placing rows
   CPanel *rows_area=this.m_element_base.GetContainer();
   if(rows_area==NULL)
      return NULL;
//--- Return the panel container with rows
   return rows_area.GetContainer();
  }

Agora o texto da célula será posicionado corretamente, independentemente do ponto de ancoragem.

Método que retorna um ponteiro para o contêiner do painel de linhas da tabela:

//+------------------------------------------------------------------+
//| CTableCellView::Return the flag that the object is               |
//| located outside of its container                                 |
//+------------------------------------------------------------------+
bool CTableCellView::IsOutOfContainer(void)
  {
//--- Check the row
   if(this.m_element_base==NULL)
      return false;

//--- Get the panel container with rows
   CContainer *container=this.GetRowsPanelContainer();
   if(container==NULL)
      return false;
  
//--- Get the cell boundaries by all sides
   int cell_l=this.m_element_base.X()+this.X();
   int cell_r=this.m_element_base.X()+this.Right();
   int cell_t=this.m_element_base.Y()+this.Y();
   int cell_b=this.m_element_base.Y()+this.Bottom();
   
//--- Return the result of checking that the object is completely outside the container
   return(cell_r <= container.X() || cell_l >= container.Right() || cell_b <= container.Y() || cell_t >= container.Bottom());
  }

A célula fica dentro de uma linha. Essa linha, juntamente com as demais, fica em um painel. O painel, por sua vez, está vinculado a um contêiner e pode ser deslocado dentro dele por meio das barras de rolagem. O método retorna um ponteiro para esse contêiner com barras de rolagem.

Método que retorna um flag indicando que o objeto está fora dos limites de seu contêiner:

//+------------------------------------------------------------------+
//| Table row visual representation class                            |
//+------------------------------------------------------------------+
class CTableRowView : public CPanel
  {
protected:
   CTableCellView    m_temp_cell;                                    // Temporary cell object for searching
   CTableRow        *m_table_row_model;                              // Pointer to the row model
   CListElm          m_list_cells;                                   // Cell list
   int               m_index;                                        // Index in the list of rows
//--- Create and add a new cell representation object to the list
   CTableCellView   *InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h);
//--- Delete the specified row region and the cell with the corresponding index
   bool              BoundCellDelete(const int index);
   
public:
//--- Return (1) the list, (2) the number of cells and (3) the cell
   CListElm         *GetListCells(void)                                 { return &this.m_list_cells;                       }
   int               CellsTotal(void)                             const { return this.m_list_cells.Total();                }
   CTableCellView   *GetCellView(const uint index)                      { return this.m_list_cells.GetNodeAtIndex(index);  }
   
//--- Set the ID
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                       }
//--- (1) Set and (2) return the row index
   void              SetIndex(const int index)                          { this.SetID(index);                               }
   int               Index(void)                                  const { return this.m_index;                             }

//--- (1) Set and (2) return the row model
   bool              TableRowModelAssign(CTableRow *row_model);
   CTableRow        *GetTableRowModel(void)                             { return this.m_table_row_model;                   }
//--- Update the row with the updated model
   bool              TableRowModelUpdate(CTableRow *row_model);

//--- Recalculate cell areas
   bool              RecalculateBounds(CListElm *list_bounds);

//--- Print the assigned row model in the journal
   void              TableRowModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0)const { return CLabel::Compare(node,mode);               }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_ROW_VIEW);             }
  
//--- Initialize (1) the class object and (2) default object colors
   void              Init(void);
   virtual void      InitColors(void);

//--- Constructors/destructor
                     CTableRowView(void);
                     CTableRowView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableRowView (void){ this.m_list_cells.Clear(); }
  };

A célula fica dentro de sua área, que está localizada na linha. O método calcula as coordenadas da célula em relação à linha e retorna um flag indicando se essas coordenadas ficam fora dos limites do contêiner.

Agora vamos aprimorar a classe de representação visual da linha da tabela CTableRowView.

Declararemos novos métodos e, no destrutor, limparemos a lista de células:

//+------------------------------------------------------------------+
//| CTableRowView::Create and add a new                              |
//| cell representation object to the list                           |
//+------------------------------------------------------------------+
CTableCellView *CTableRowView::InsertNewCellView(const int index,const string text,const int dx,const int dy,const int w,const int h)
  {
//--- Check whether the list contains an object with the specified ID and, if it does, report this and return NULL
   this.m_temp_cell.SetIndex(index);
//--- Save the list sorting method
   int sort_mode=this.m_list_cells.SortMode();
//--- Set the sorting flag for the list by ID
   this.m_list_cells.Sort(ELEMENT_SORT_BY_ID);
   if(this.m_list_cells.Search(&this.m_temp_cell)!=NULL)
     {
      //--- Return the list to its original sorting, report that such an object already exists and return NULL
      this.m_list_cells.Sort(sort_mode);
      ::PrintFormat("%s: Error. The TableCellView object with index %d is already in the list",__FUNCTION__,index);
      return NULL;
     }
//--- Return the list to its original sorting
   this.m_list_cells.Sort(sort_mode);
//--- Create a cell object name
   string name="TableCellView"+(string)this.Index()+"x"+(string)index;

//--- Create a new TableCellView object; in case of a failure, report it and return NULL
   CTableCellView *cell_view=new CTableCellView(index,name,text,dx,dy,w,h);
   if(cell_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to create CTableCellView object",__FUNCTION__);
      return NULL;
     }
//--- If failed to add the new object to the list, report this, remove the object and return NULL
   if(this.m_list_cells.Add(cell_view)==-1)
     {
      ::PrintFormat("%s: Error. Failed to add CTableCellView object to list",__FUNCTION__);
      delete cell_view;
      return NULL;
     }
//--- Assign the base element (row) and return the pointer to the object
   cell_view.RowAssign(&this);
   return cell_view;
  }

No método que cria um novo objeto de representação da célula e o adiciona à lista, salvaremos o modo de ordenação atual da lista e, depois de adicionar o objeto, restauraremos a ordenação original:

//+------------------------------------------------------------------+
//| CTableRowView::Set the row model                                 |
//+------------------------------------------------------------------+
bool CTableRowView::TableRowModelAssign(CTableRow *row_model)
  {
//--- If an empty object is passed, report this and return 'false'
   if(row_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- If the passed row model does not contain a single cell, report this and return 'false'
   int total=(int)row_model.CellsTotal();
   if(total==0)
     {
      ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__);
      return false;
     }
//--- Save the pointer to the passed row model
   this.m_table_row_model=row_model;
//--- calculate the cell width based on the row panel width
   CCanvasBase *base=this.GetContainer();
   int w=(base!=NULL ? base.Width() : this.Width());
   int cell_w=(int)::fmax(::round((double)w/(double)total),DEF_TABLE_COLUMN_MIN_W);

//--- In the loop by the number of cells in the row model
   for(int i=0;i<total;i++)
     {
      //--- get the model of the next cell,
      CTableCell *cell_model=this.m_table_row_model.GetCell(i);
      if(cell_model==NULL)
         return false;
      //--- calculate the coordinate and create a name for the cell area
      int x=cell_w*i;
      string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i;
      //--- Create a new cell area
      CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height());
      if(cell_bound==NULL)
         return false;
      //--- Create a new cell visual representation object
      CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height());
      if(cell_view==NULL)
         return false;
      //--- Assign the corresponding visual representation object of the cell to the current cell area
      cell_bound.AssignObject(cell_view);
     }
//--- All is successful
   return true;
  }

No método que define o modelo da linha, calcularemos a largura das colunas com base na largura do painel de linhas, e não na largura da própria linha, pois ela é um pouco mais estreita que o painel. A largura do cabeçalho da tabela também é igual à largura do painel de linhas. Portanto, para que a largura das células coincida com a das colunas, usaremos a largura do painel:

//+------------------------------------------------------------------+
//| CTableRowView::Update the row with the updated model             |
//+------------------------------------------------------------------+
bool CTableRowView::TableRowModelUpdate(CTableRow *row_model)
  {
//--- If an empty object is passed, report this and return 'false'
   if(row_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- If the passed row model does not contain a single cell, report this and return 'false'
   int total_model=(int)row_model.CellsTotal(); // Number of cells in the row model
   if(total_model==0)
     {
      ::PrintFormat("%s: Error. Row model does not contain any cells",__FUNCTION__);
      return false;
     }
//--- Save the pointer to the passed row model
   this.m_table_row_model=row_model;

//--- Calculate the cell width based on the row panel width
   CCanvasBase *base=this.GetContainer();
   int w=(base!=NULL ? base.Width() : this.Width());
   int cell_w=(int)::fmax(::round((double)w/(double)total_model),DEF_TABLE_COLUMN_MIN_W);
   
   CBound *cell_bound=NULL;
   int total_bounds=this.m_list_bounds.Total(); // Number of areas
   int diff=total_model-total_bounds;           // Difference between the number of areas in a row and the number of cells in a row model 
   
//--- If the model has more cells than areas in the list, create the missing areas and cells at the end of the lists
   if(diff>0)
     {
      //--- In a loop by the number of missing areas
      for(int i=total_bounds;i<total_bounds+diff;i++)
        {
         //--- create and add the number of cell areas of the row to the diff list.
         //--- Get the model of the next cell,
         CTableCell *cell_model=this.m_table_row_model.GetCell(i);
         if(cell_model==NULL)
            return false;
         //--- calculate the coordinate and create a name for the cell area
         int x=cell_w*i;
         string name="CellBound"+(string)this.m_table_row_model.Index()+"x"+(string)i;
         //--- Create a new cell area
         CBound *cell_bound=this.InsertNewBound(name,x,0,cell_w,this.Height());
         if(cell_bound==NULL)
            return false;
            
         //--- Create a new cell visual representation object
         CTableCellView *cell_view=this.InsertNewCellView(i,cell_model.Value(),x,0,cell_w,this.Height());
         if(cell_view==NULL)
            return false;
        }
     }
 
//--- If there are more areas in the list than cells in the model, remove the extra areas at the end of the list
   if(diff<0)
     {
      int  start=total_bounds-1;
      int  end=start-diff;
      bool res=true;
      for(int i=start;i>end;i--)
        {
         if(!this.BoundCellDelete(i))
            return false;
        }
     }
   
//--- In the loop by the number of cells in the row model
   for(int i=0;i<total_model;i++)
     {
      //--- get the model of the next cell,
      CTableCell *cell_model=this.m_table_row_model.GetCell(i);
      if(cell_model==NULL)
         return false;
      
      //--- calculate the cell coordinate
      int x=cell_w*i;
      //--- Get the next cell area
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         return false;
      
      //--- Get the cell visual representation object from the list 
      CTableCellView *cell_view=this.m_list_cells.GetNodeAtIndex(i);
      if(cell_view==NULL)
         return false;
      
      //--- Assign the corresponding visual representation object of the cell and its text to the current cell area
      cell_bound.AssignObject(cell_view);
      cell_view.SetText(cell_model.Value());
     }
//--- All is successful
   return true;
  }

A aparência das células de uma linha da tabela pode precisar ser atualizada, por exemplo, quando uma coluna é adicionada ou removida. Essas alterações feitas no modelo da tabela também precisam ser refletidas em sua representação visual.

Para isso, criaremos um método específico.

Método que atualiza a linha de acordo com o modelo:

//+------------------------------------------------------------------+
//| CTableRowView::Delete the specified row region                   |
//| and the cell with the corresponding index                        |
//+------------------------------------------------------------------+
bool CTableRowView::BoundCellDelete(const int index)
  {
   if(!this.m_list_cells.Delete(index))
      return false;
   return this.m_list_bounds.Delete(index);
  }

A lógica do método está descrita nos comentários do código. Se colunas tiverem sido adicionadas ou removidas no modelo da tabela, a representação visual da linha também adicionará ou removerá a quantidade correspondente de células. Em seguida, percorremos em um laço todas as células da linha no modelo da tabela, associamos a cada área de célula o respectivo objeto de representação visual da célula.

Método que remove a área especificada da linha e a célula com o índice correspondente:

//+------------------------------------------------------------------+
//| CTableRowView::Draw the appearance                               |
//+------------------------------------------------------------------+
void CTableRowView::Draw(const bool chart_redraw)
  {
//--- If the row is outside the container, leave
   if(this.IsOutOfContainer())
      return;

//--- Fill the object with the background color, draw the row line and update the background canvas
   this.Fill(this.BackColor(),false);
   this.m_background.Line(this.AdjX(0),this.AdjY(this.Height()-1),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(this.BorderColor(),this.AlphaBG()));
  
//--- Draw the row cells
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the area of the next cell
      CBound *cell_bound=this.GetBoundAt(i);
      if(cell_bound==NULL)
         continue;
      
      //--- Get the attached cell object from the cell area
      CTableCellView *cell_view=cell_bound.GetAssignedObj();
      //--- Draw a visual representation of the cell
      if(cell_view!=NULL)
         cell_view.Draw(false);
     }
//--- Update the background and foreground canvases with the specified chart redraw flag
   this.Update(chart_redraw);
  }

Se o objeto da célula for removido com sucesso da lista, removemos também da lista de áreas a área correspondente.

No método que desenha a aparência da linha, verificamos se ela está fora dos limites do contêiner:

//+------------------------------------------------------------------+
//| CTableRowView::Recalculate cell areas                            |
//+------------------------------------------------------------------+
bool CTableRowView::RecalculateBounds(CListElm *list_bounds)
  {
//--- Check the list
   if(list_bounds==NULL)
      return false;

//--- In the loop by the number of areas in the list
   for(int i=0;i<list_bounds.Total();i++)
     {
      //--- get the next header area and the corresponding cell area
      CBound *capt_bound=list_bounds.GetNodeAtIndex(i);
      CBound *cell_bound=this.GetBoundAt(i);
      if(capt_bound==NULL || cell_bound==NULL)
         return false;

      //--- Set the coordinate and size of the header area in the cell area 
      cell_bound.SetX(capt_bound.X());
      cell_bound.ResizeW(capt_bound.Width());
      
      //--- Get the attached cell object from the cell area
      CTableCellView *cell_view=cell_bound.GetAssignedObj();
      if(cell_view==NULL)
         return false;

      //--- Set the coordinate and size of the cell area in the cell's visual representation object
      cell_view.BoundSetX(cell_bound.X());
      cell_view.BoundResizeW(cell_bound.Width());
     }
//--- All is successful
   return true;
  }

Uma linha que não esteja na área visível do contêiner não deve ser desenhada.

Ao alterar a largura de uma coluna, é necessário modificar a largura de todas as células correspondentes a essa coluna e deslocar as células adjacentes para as novas coordenadas. Para isso, criaremos um novo método.

Método que recalcula as áreas das células:

//+------------------------------------------------------------------+
//| Class for visual representation of table column header           |
//+------------------------------------------------------------------+
class CColumnCaptionView : public CButton
  {
protected:
   CColumnCaption   *m_column_caption_model;                         // Pointer to the column header model
   CBound           *m_bound_node;                                   // Pointer to the header area
   int               m_index;                                        // Index in the column list
   ENUM_TABLE_SORT_MODE m_sort_mode;                                 // Table column sorting mode
   
//--- Add hint objects with arrows to the list
   virtual bool      AddHintsArrowed(void);
//--- Displays the resize cursor
   virtual bool      ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y);
   
public:
//--- Set the ID
   virtual void      SetID(const int id)                                { this.m_index=this.m_id=id;                 }
//--- (1) Set and (2) return the cell index
   void              SetIndex(const int index)                          { this.SetID(index);                         }
   int               Index(void)                                  const { return this.m_index;                       }
   
//--- (1) Assign and (2) return the area of the header the object is assigned to
   void              AssignBoundNode(CBound *bound)                     { this.m_bound_node=bound;                   }
   CBound           *GetBoundNode(void)                                 { return this.m_bound_node;                  }

//--- (1) Assign and (2) return the column header model
   bool              ColumnCaptionModelAssign(CColumnCaption *caption_model);
   CColumnCaption   *ColumnCaptionModel(void)                           { return this.m_column_caption_model;        }

//--- Print the assigned column header model in the journal
   void              ColumnCaptionModelPrint(void);

//--- (1) Set and (2) return the sorting mode
   void              SetSortMode(const ENUM_TABLE_SORT_MODE mode)       { this.m_sort_mode=mode;                     }
   ENUM_TABLE_SORT_MODE SortMode(void)                            const { return this.m_sort_mode;                   }
   
//--- Set the reverse sorting direction
   void              SetSortModeReverse(void);
   
//--- Draw (1) the appearance and (2) sorting direction arrow
   virtual void      Draw(const bool chart_redraw);
protected:
   void              DrawSortModeArrow(void);
public:  
//--- Handler for resizing an element by the right side
   virtual bool      ResizeZoneRightHandler(const int x, const int y);
   
//--- Handlers for resizing the element by sides and corners
   virtual bool      ResizeZoneLeftHandler(const int x, const int y)       { return false;                           }
   virtual bool      ResizeZoneTopHandler(const int x, const int y)        { return false;                           }
   virtual bool      ResizeZoneBottomHandler(const int x, const int y)     { return false;                           }
   virtual bool      ResizeZoneLeftTopHandler(const int x, const int y)    { return false;                           }
   virtual bool      ResizeZoneRightTopHandler(const int x, const int y)   { return false;                           }
   virtual bool      ResizeZoneLeftBottomHandler(const int x, const int y) { return false;                           }
   virtual bool      ResizeZoneRightBottomHandler(const int x, const int y){ return false;                           }
   
//--- Change the object width
   virtual bool      ResizeW(const int w);
   
//--- Mouse button click event handler (Press)
   virtual void      OnPressEvent(const int id, const long lparam, const double dparam, const string sparam);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0)const { return CButton::Compare(node,mode);        }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_COLUMN_CAPTION_VIEW);}
  
//--- Initialize (1) the class object and (2) default object colors
   void              Init(const string text);
   virtual void      InitColors(void);
   
//--- Return the object description
   virtual string    Description(void);
   
//--- Constructors/destructor
                     CColumnCaptionView(void);
                     CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h); 
                    ~CColumnCaptionView (void){}
  };

O método recebe uma lista das áreas dos cabeçalhos das colunas. Com base nas propriedades das áreas dos cabeçalhos da lista recebida, recalculamos o tamanho e as coordenadas das áreas das células da tabela. Os novos tamanhos e coordenadas também são aplicados aos respectivos objetos de célula.

No paradigma MVC, o cabeçalho da tabela não é apenas uma parte da tabela, mas um controle independente por meio do qual é possível interagir com as colunas e controlar sua aparência. Nesse contexto, o cabeçalho da coluna também é um controle. Nesta implementação, ele permite alterar o tamanho da coluna, definir as mesmas propriedades para todas as células da coluna e assim por diante.

Assim, é nas classes do cabeçalho da coluna e do cabeçalho da tabela que se concentram as principais alterações responsáveis por "dar vida" à tabela.

Vamos aprimorar a classe de representação visual do cabeçalho da coluna da tabela CColumnCaptionView.

Como controle, o cabeçalho da coluna controlará sua largura e o estado da ordenação, incluindo o sentido crescente, o sentido decrescente ou a ausência de ordenação. Ao clicar no cabeçalho, será exibida uma seta indicando o sentido da ordenação. Quando não houver ordenação, por exemplo, depois de clicar em outro cabeçalho, a seta não será exibida. Ao posicionar o cursor sobre a borda direita do cabeçalho, aparecerá junto ao cursor uma seta de dica indicando a direção do redimensionamento. Se o botão do mouse for mantido pressionado e a borda for arrastada, a largura da coluna será alterada, enquanto as colunas à direita serão reposicionadas para acompanhar a nova posição da borda direita da coluna redimensionada.

Declararemos novas variáveis e métodos:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Default constructor. Builds an object        |
//| in the main window of the current chart at coordinates 0,0       |
//| with default dimensions                                          |
//+------------------------------------------------------------------+
CColumnCaptionView::CColumnCaptionView(void) : CButton("ColumnCaption","Caption",::ChartID(),0,0,0,DEF_PANEL_W,DEF_TABLE_ROW_H), m_index(0), m_sort_mode(TABLE_SORT_MODE_NONE)
  {
//--- Initialization
   this.Init("Caption");
   this.SetID(0);
   this.SetName("ColumnCaption");
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Parametric constructor.                      |
//| Plot an object in the specified window of the specified plot with|
//| the specified text, coordinates and dimensions                   |
//+------------------------------------------------------------------+
CColumnCaptionView::CColumnCaptionView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h) :
   CButton(object_name,text,chart_id,wnd,x,y,w,h), m_index(0), m_sort_mode(TABLE_SORT_MODE_NONE)
  {
//--- Initialization
   this.Init(text);
   this.SetID(0);
  }

Vamos analisar as alterações nos métodos já existentes e a implementação dos novos métodos da classe.

No construtor, definiremos por padrão que não há ordenação:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Initialization                               |
//+------------------------------------------------------------------+
void CColumnCaptionView::Init(const string text)
  {
//--- Default text offsets
   this.m_text_x=4;
   this.m_text_y=2;
//--- Set the colors of different states
   this.InitColors();
//--- It is possible to resize
   this.SetResizable(true);
   this.SetMovable(false);
   this.SetImageBound(this.ObjectWidth()-14,4,8,11);
  }

No método de inicialização, habilitaremos o redimensionamento, desabilitaremos a movimentação do objeto e definiremos a área da imagem para a seta que indica o sentido da ordenação:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Draw the appearance                          |
//+------------------------------------------------------------------+
void CColumnCaptionView::Draw(const bool chart_redraw)
  {
//--- If the object is outside its container, leave
   if(this.IsOutOfContainer())
      return;

//--- Fill the object with the background color, draw the light vertical line on the left and the dark one on the right
   this.Fill(this.BackColor(),false);
   color clr_dark =this.BorderColor();                                                       // "Dark color"
   color clr_light=this.GetBackColorControl().NewColor(this.BorderColor(), 100, 100, 100);   // "Light color"
   this.m_background.Line(this.AdjX(0),this.AdjY(0),this.AdjX(0),this.AdjY(this.Height()-1),::ColorToARGB(clr_light,this.AlphaBG()));                          // Line on the left
   this.m_background.Line(this.AdjX(this.Width()-1),this.AdjY(0),this.AdjX(this.Width()-1),this.AdjY(this.Height()-1),::ColorToARGB(clr_dark,this.AlphaBG())); // Line on the right
//--- Update the background canvas
   this.m_background.Update(false);
   
//--- Display the header text
   CLabel::Draw(false);
      
//--- Draw sorting direction arrows
   this.DrawSortModeArrow();

//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

No método que desenha a aparência, verificaremos se o objeto está dentro dos limites do contêiner e chamaremos o método que desenha a seta que indica o sentido da ordenação:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Draw the sorting direction arrow             |
//+------------------------------------------------------------------+
void CColumnCaptionView::DrawSortModeArrow(void)
  {
//--- Set the arrow color for the normal and disabled object states
   color clr=(!this.IsBlocked() ? this.GetForeColorControl().NewColor(this.ForeColor(),90,90,90) : this.ForeColor());
   switch(this.m_sort_mode)
     {
      //--- Sort ascending
      case TABLE_SORT_MODE_ASC   :  
         //--- Clear the drawing area and draw the down arrow
         this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
         this.m_painter.ArrowDown(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true);
         break;
      //--- Sort descending
      case TABLE_SORT_MODE_DESC  :  
         //--- Clear the drawing area and draw the up arrow
         this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
         this.m_painter.ArrowUp(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),clr,this.AlphaFG(),true);
         break;
      //--- No sorting
      default : 
         //--- Clear the drawing area
         this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
         break;
     }
  }

Método que desenha a seta do sentido da ordenação:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Reverse the sorting direction                |
//+------------------------------------------------------------------+
void CColumnCaptionView::SetSortModeReverse(void)
  {
   switch(this.m_sort_mode)
     {
      case TABLE_SORT_MODE_ASC   :  this.m_sort_mode=TABLE_SORT_MODE_DESC; break;
      case TABLE_SORT_MODE_DESC  :  this.m_sort_mode=TABLE_SORT_MODE_ASC;  break;
      default                    :  break;
     }
  }

Dependendo do sentido da ordenação, desenhamos a seta correspondente:

  • em ordem crescente, uma seta para baixo,
  • em ordem decrescente, uma seta para cima,
  • sem ordenação, simplesmente removemos a seta.

Método que inverte o sentido da ordenação:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Add hint objects                             |
//| with arrows to the list                                          |
//+------------------------------------------------------------------+
bool CColumnCaptionView::AddHintsArrowed(void)
  {
//--- Create a hint for the horizontal offset arrow
   CVisualHint *hint=this.CreateAndAddNewHint(HINT_TYPE_ARROW_SHIFT_HORZ,DEF_HINT_NAME_SHIFT_HORZ,18,18);
   if(hint==NULL)
      return false;

//--- Set the size of the hint image area
   hint.SetImageBound(0,0,hint.Width(),hint.Height());
   
//--- hide the hint and draw the appearance
   hint.Hide(false);
   hint.Draw(false);
   
//--- All is successful
   return true;
  }

Se a ordenação atual estiver definida como crescente, passamos para decrescente, e vice-versa. A ausência de ordenação é definida em outro método, pois este se destina apenas a alternar o sentido da ordenação ao clicar no cabeçalho da coluna.

Método que adiciona à lista os objetos de dica com setas:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Display the resize cursor                    |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ShowCursorHint(const ENUM_CURSOR_REGION edge,int x,int y)
  {
   CVisualHint *hint=NULL;          // Pointer to the hint
   int hint_shift_x=0;              // Hint offset by X
   int hint_shift_y=0;              // Hint offset by Y
   
//--- Depending on the location of the cursor on the element borders
//--- specify the tooltip offsets relative to the cursor coordinates,
//--- display the required hint on the chart and get the pointer to this object
   if(edge!=CURSOR_REGION_RIGHT)
      return false;
   
   hint_shift_x=-8;
   hint_shift_y=-12;
   this.ShowHintArrowed(HINT_TYPE_ARROW_SHIFT_HORZ,x+hint_shift_x,y+hint_shift_y);
   hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ);

//--- Return the result of adjusting the position of the tooltip relative to the cursor
   return(hint!=NULL ? hint.Move(x+hint_shift_x,y+hint_shift_y) : false);
  }

Uma nova dica é criada e adicionada à lista. Depois disso, ela pode ser obtida da lista pelo nome.

Método que exibe o cursor de redimensionamento:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Right edge resize handler                    |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ResizeZoneRightHandler(const int x,const int y)
  {
//--- Calculate and set the new element width
   int width=::fmax(x-this.X()+1,DEF_TABLE_COLUMN_MIN_W);
   if(!this.ResizeW(width))
      return false;
//--- Get the pointer to the hint
   CVisualHint *hint=this.GetHint(DEF_HINT_NAME_SHIFT_HORZ);
   if(hint==NULL)
      return false;
//--- Shift the hint by the specified values relative to the cursor
   int shift_x=-8;
   int shift_y=-12;
   
   CTableHeaderView *header=this.m_container;
   if(header==NULL)
      return false;
   
   bool res=header.RecalculateBounds(this.GetBoundNode(),this.Width());
   res &=hint.Move(x+shift_x,y+shift_y);
   if(res)
      ::ChartRedraw(this.m_chart_id);
   return res;
  }

O método só funciona quando o cursor está sobre a borda direita do elemento. Ele exibe a dica junto ao cursor, aplicando o deslocamento configurado.

Manipulador de redimensionamento pela borda direita:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Change the object width                      |
//+------------------------------------------------------------------+
bool CColumnCaptionView::ResizeW(const int w)
  {
   if(!CCanvasBase::ResizeW(w))
      return false;
//--- Clear the drawing area in the previous location
   this.m_painter.Clear(this.AdjX(this.m_painter.X()),this.AdjY(this.m_painter.Y()),this.m_painter.Width(),this.m_painter.Height(),false);
//--- Set a new drawing area
   this.SetImageBound(this.Width()-14,4,8,11);
   return true;
  }

Ao arrastar a borda direita do elemento, sua largura deve ser alterada de acordo com o movimento do cursor. Primeiro, a largura do elemento é modificada. Em seguida, a dica é exibida e chamamos o método RecalculateBounds() do cabeçalho da tabela para recalcular as áreas dos cabeçalhos das colunas. Esse método define o novo tamanho da área do cabeçalho alterado e reposiciona as áreas dos cabeçalhos de coluna adjacentes nas novas coordenadas.

Método virtual que altera a largura do objeto:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Mouse button click event handler             |
//+------------------------------------------------------------------+
void CColumnCaptionView::OnPressEvent(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- If the mouse button is released in the drag area of the right edge of the element, leave
   if(this.ResizeRegion()==CURSOR_REGION_RIGHT)
      return;
//--- Change the sort direction arrow to the opposite and call the mouse click handler
   this.SetSortModeReverse();
   CCanvasBase::OnPressEvent(id,lparam,dparam,sparam);
  }

Ao redimensionar o cabeçalho da coluna, é necessário deslocar a área da imagem, ou seja, da seta que indica o sentido da ordenação, para que ela seja desenhada na posição correta junto à borda direita do elemento.

Manipulador de eventos de pressionamento dos botões do mouse:

//+------------------------------------------------------------------+
//| CColumnCaptionView::Save to file                                 |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Save(const int file_handle)
  {
//--- Save the parent object data
   if(!CButton::Save(file_handle))
      return false;
  
//--- Save the header index
   if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE)
      return false;
//--- Save the sorting direction
   if(::FileWriteInteger(file_handle,this.m_sort_mode,INT_VALUE)!=INT_VALUE)
      return false;
      
//--- All is successful
   return true;
  }
//+------------------------------------------------------------------+
//| CColumnCaptionView::Load from file                               |
//+------------------------------------------------------------------+
bool CColumnCaptionView::Load(const int file_handle)
  {
//--- Load parent object data
   if(!CButton::Load(file_handle))
      return false;
      
//--- Load the header index
   this.m_id=this.m_index=::FileReadInteger(file_handle,INT_VALUE);
//--- Load the sorting direction
   this.m_id=this.m_sort_mode=(ENUM_TABLE_SORT_MODE)::FileReadInteger(file_handle,INT_VALUE);
   
//--- All is successful
   return true;
  }

Se o evento de clique tiver sido gerado pela liberação do botão do mouse na área de arraste, isso significa que o redimensionamento do cabeçalho acabou de ser concluído, portanto saímos do método. Em seguida, invertemos o sentido da ordenação e atualizamos a seta correspondente e chamamos o manipulador dos eventos dos botões do mouse do objeto base.

Nos métodos de manipulação de arquivos, salvaremos e carregaremos o valor que representa o sentido da ordenação:

//+------------------------------------------------------------------+
//| Class for visual representation of table header                  |
//+------------------------------------------------------------------+
class CTableHeaderView : public CPanel
  {
protected:
   CColumnCaptionView m_temp_caption;                                // Temporary column header object for searching
   CTableHeader     *m_table_header_model;                           // Pointer to the table header model

//--- Create and add a new column header representation object to the list
   CColumnCaptionView *InsertNewColumnCaptionView(const string text, const int x, const int y, const int w, const int h);
   
public:
//--- (1) Set and (2) return the table header model
   bool              TableHeaderModelAssign(CTableHeader *header_model);
   CTableHeader     *GetTableHeaderModel(void)                          { return this.m_table_header_model;    }

//--- Recalculate the header areas
   bool              RecalculateBounds(CBound *bound,int new_width);

//--- Print the assigned table header model in the journal
   void              TableHeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   
//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Set the sorting flag for the column header
   void              SetSortedColumnCaption(const uint index);

//--- Get the column header (1) by index and (2) with sorting flag
   CColumnCaptionView *GetColumnCaption(const uint index);
   CColumnCaptionView *GetSortedColumnCaption(void);
//--- Return the column header index with the sorting flag
   int               IndexSortedColumnCaption(void);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode);      }
   virtual bool      Save(const int file_handle)                        { return CPanel::Save(file_handle);       }
   virtual bool      Load(const int file_handle)                        { return CPanel::Load(file_handle);       }
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_HEADER_VIEW); }
   
//--- Handler for the element user event when clicking on the object area
   virtual void      MousePressHandler(const int id, const long lparam, const double dparam, const string sparam);
  
//--- Initialize (1) the class object and (2) default object colors
   void              Init(void);
   virtual void      InitColors(void);

//--- Constructors/destructor
                     CTableHeaderView(void);
                     CTableHeaderView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableHeaderView (void){}
  };

Agora vamos aprimorar a classe de representação visual do cabeçalho da tabela CTableHeaderView.

O objeto dessa classe contém e gerencia uma lista de cabeçalhos de colunas. Com isso, oferece ao usuário um controle para alterar a aparência das colunas da tabela e ordenar os dados por qualquer uma delas.

Declararemos novos métodos da classe:

//+------------------------------------------------------------------+
//| CTableHeaderView::Set the header model                           |
//+------------------------------------------------------------------+
bool CTableHeaderView::TableHeaderModelAssign(CTableHeader *header_model)
  {
//--- If an empty object is passed, report this and return 'false'
   if(header_model==NULL)
     {
      ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__);
      return false;
     }
//--- If the passed header model does not contain any column headers, report this and return 'false'
   int total=(int)header_model.ColumnsTotal();
   if(total==0)
     {
      ::PrintFormat("%s: Error. Header model does not contain any columns",__FUNCTION__);
      return false;
     }
//--- Store the pointer to the passed table header model and calculate the width of each column header
   this.m_table_header_model=header_model;
   int caption_w=(int)::fmax(::round((double)this.Width()/(double)total),DEF_TABLE_COLUMN_MIN_W);

//--- In the loop by the number of column headers in the table header model
   for(int i=0;i<total;i++)
     {
      //--- get the model of the next column header,
      CColumnCaption *caption_model=this.m_table_header_model.GetColumnCaption(i);
      if(caption_model==NULL)
         return false;
      //--- calculate the coordinate and create a name for the column header area
      int x=caption_w*i;
      string name="CaptionBound"+(string)i;
      //--- Create a new column header area
      CBound *caption_bound=this.InsertNewBound(name,x,0,caption_w,this.Height());
      if(caption_bound==NULL)
         return false;
      caption_bound.SetID(i);
      //--- Create a new column header visual representation object
      CColumnCaptionView *caption_view=this.InsertNewColumnCaptionView(caption_model.Value(),x,0,caption_w,this.Height());
      if(caption_view==NULL)
         return false;
         
      //--- Assign the corresponding visual representation object of the column header to the current column header area
      caption_bound.AssignObject(caption_view);
      caption_view.AssignBoundNode(caption_bound);
      
      //--- For the very first header, set the sorting flag in ascending order
      if(i==0)
         caption_view.SetSortMode(TABLE_SORT_MODE_ASC);
     }
//--- All is successful
   return true;
  }

Vamos analisar as alterações nos métodos existentes e a implementação dos métodos declarados.

No método que define o modelo do cabeçalho, calcularemos a largura dos cabeçalhos levando em conta a largura mínima permitida, definiremos corretamente o identificador de cada cabeçalho, atribuiremos a ele a área correspondente e, para o primeiro cabeçalho, definiremos o flag de ordenação crescente:

//+------------------------------------------------------------------+
//| CTableHeaderView::Recalculate the header areas                   |
//+------------------------------------------------------------------+
bool CTableHeaderView::RecalculateBounds(CBound *bound,int new_width)
  {
//--- If an empty area object is passed or its width has not changed, return 'false'
   if(bound==NULL || bound.Width()==new_width)
      return false;
      
//--- Get the area index in the list
   int index=this.m_list_bounds.IndexOf(bound);
   if(index==WRONG_VALUE)
      return false;

//--- Calculate the offset and, if it is absent, return 'false'
   int delta=new_width-bound.Width();
   if(delta==0)
      return false;

//--- Change the width of the current area and the object assigned to it
   bound.ResizeW(new_width);
   CElementBase *assigned_obj=bound.GetAssignedObj();
   if(assigned_obj!=NULL)
      assigned_obj.ResizeW(new_width);

//--- Get the next area after the current one
   CBound *next_bound=this.m_list_bounds.GetNextNode();
//--- Recalculate the X coordinates for all subsequent areas
   while(!::IsStopped() && next_bound!=NULL)
     {
      //--- Shift the region by delta
      int new_x = next_bound.X()+delta;
      int prev_width=next_bound.Width();
      next_bound.SetX(new_x);
      next_bound.Resize(prev_width,next_bound.Height());
      
      //--- If there is an assigned object in the area, update its position
      CElementBase *assigned_obj=next_bound.GetAssignedObj();
      if(assigned_obj!=NULL)
        {
         assigned_obj.Move(assigned_obj.X()+delta,assigned_obj.Y());
         
         //--- This block of code is part of the effort to find and fix artifacts when dragging headers
         CCanvasBase *base_obj=assigned_obj.GetContainer();
         if(base_obj!=NULL)
           {
            if(assigned_obj.X()>base_obj.ContainerLimitRight())
               assigned_obj.Hide(false);
            else
               assigned_obj.Show(false);
           }
        }
      //--- Move on to the next area
      next_bound=this.m_list_bounds.GetNextNode();
     }
     
//--- Calculate the new width of the table header based on the width of the column headers
   int header_width=0;
   for(int i=0;i<this.m_list_bounds.Total();i++)
     {
      CBound *bound=this.GetBoundAt(i);
      if(bound!=NULL)
         header_width+=bound.Width();
     }

//--- If the calculated width of the table header differs from the current one, change the width
   if(header_width!=this.Width())
     {
      if(!this.ResizeW(header_width))
         return false;
     }

//--- Get the pointer to the table object (View)
   CTableView *table_view=this.GetContainer();
   if(table_view==NULL)
      return false;

//--- Get a pointer to the panel with table rows from the table object
   CPanel *table_area=table_view.GetTableArea();
   if(table_area==NULL)
      return false;
   
//--- Resize  the table row panel to fit the overall size of the column headers
   if(!table_area.ResizeW(header_width))
      return false;
   
//--- Get a list of table rows and loop through all the rows
   CListElm *list=table_area.GetListAttachedElements();
   int total=list.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the next table row
      CTableRowView *row=table_area.GetAttachedElementAt(i);
      if(row!=NULL)
        {
         //--- Change the row size to fit the panel size and recalculate the cell areas
         row.ResizeW(table_area.Width());
         row.RecalculateBounds(&this.m_list_bounds);
        }
     }
//--- Redraw all table rows
   table_area.Draw(false);
   return true;
  }

Método que recalcula as áreas dos cabeçalhos:

//+------------------------------------------------------------------+
//| CTableHeaderView::Set the sorting flag for the column header     |
//+------------------------------------------------------------------+
void CTableHeaderView::SetSortedColumnCaption(const uint index)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the area of the next column header and
      //--- get the attached column header object from it
      CColumnCaptionView *caption_view=this.GetColumnCaption(i);
      if(caption_view==NULL)
         continue;
      
      //--- If the loop index is equal to the required index, set the ascending sort flag
      if(i==index)
        {
         caption_view.SetSortMode(TABLE_SORT_MODE_ASC);
         caption_view.Draw(false);
        }
      //--- Otherwise, reset the sorting flag
      else
        {
         caption_view.SetSortMode(TABLE_SORT_MODE_NONE);
         caption_view.Draw(false);
        }
     }
   this.Draw(true);
  }

A lógica do método está descrita nos comentários do código. O método recebe um ponteiro para a área do cabeçalho que foi alterada e sua nova largura. A área e o cabeçalho associado a ela são redimensionados e, a partir da área seguinte na lista, reposicionamos cada área subsequente, juntamente com o respectivo cabeçalho. Depois que todas as áreas são deslocadas, é calculada a nova largura do cabeçalho da tabela com base nas larguras de todas as áreas dos cabeçalhos de suas colunas. Posteriormente, esse novo tamanho influencia a largura da barra de rolagem horizontal da tabela. Com base no novo tamanho do cabeçalho da tabela, definimos a largura do painel de linhas. Todas as linhas são redimensionadas de acordo com o novo tamanho do painel e, para cada uma delas, todas as áreas das células são recalculadas com base na lista de áreas do cabeçalho da tabela. Ao final, a tabela é redesenhada, considerando apenas a parte visível dentro do contêiner.

Método que define o flag de ordenação para o cabeçalho da coluna:

//+------------------------------------------------------------------+
//| CTableHeaderView::Get the column header by index                 |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableHeaderView::GetColumnCaption(const uint index)
  {
//--- Get the column header area by index
   CBound *capt_bound=this.GetBoundAt(index);
   if(capt_bound==NULL)
      return NULL;
//--- Return a pointer to the attached column header object from the column header area
   return capt_bound.GetAssignedObj();
  }

Percorrendo em um laço a lista de áreas dos cabeçalhos das colunas, obtemos o objeto do cabeçalho. Se ele for o cabeçalho correspondente ao índice procurado, definimos para ele o flag de ordenação crescente. Caso contrário, removemos o flag de ordenação. Dessa forma, o flag de ordenação é removido de todos os cabeçalhos, enquanto o cabeçalho indicado pelo índice é configurado para ordenação crescente. Em outras palavras, ao clicar no cabeçalho de uma coluna, será definida a ordenação crescente para essa coluna. Se o mesmo cabeçalho for clicado novamente, será definido o flag de ordenação decrescente, mas isso já será feito no manipulador do evento de clique do elemento.

Método que retorna o cabeçalho da coluna pelo índice:

//+------------------------------------------------------------------+
//| CTableHeaderView::Get the column header with the sorting flag    |
//+------------------------------------------------------------------+
CColumnCaptionView *CTableHeaderView::GetSortedColumnCaption(void)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the area of the next column header and
      //--- get the attached column header object from it
      CColumnCaptionView *caption_view=this.GetColumnCaption(i);
      
      //--- If the object is received and it has the sorting flag set, return the pointer to it
      if(caption_view!=NULL && caption_view.SortMode()!=TABLE_SORT_MODE_NONE)
         return caption_view;
     }
   return NULL;
  }

Método que retorna o cabeçalho da coluna com o flag de ordenação ativo:

//+------------------------------------------------------------------+
//| CTableHeaderView::Return the index of the sorted column          |
//+------------------------------------------------------------------+
int CTableHeaderView::IndexSortedColumnCaption(void)
  {
   int total=this.m_list_bounds.Total();
   for(int i=0;i<total;i++)
     {
      //--- Get the area of the next column header and
      //--- get the attached column header object from it
      CColumnCaptionView *caption_view=this.GetColumnCaption(i);
     
      //--- If the object is received and it has the sorting flag set, return the area index
      if(caption_view!=NULL && caption_view.SortMode()!=TABLE_SORT_MODE_NONE)
         return i;
     }
   return WRONG_VALUE;
  }

Método que retorna o índice da coluna pela qual a tabela está ordenada:

//+------------------------------------------------------------------+
//| CTableHeaderView::Element custom event handler                   |
//| when clicking on an object area                                  |
//+------------------------------------------------------------------+
void CTableHeaderView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam)
  {
//--- Get the name of the table header object from sparam
   int len=::StringLen(this.NameFG());
   string header_str=::StringSubstr(sparam,0,len);
//--- If the retrieved name does not match the name of this object, it is not our event, we leave
   if(header_str!=this.NameFG())
      return;
   
//--- Find the column header index in sparam
   string capt_str=::StringSubstr(sparam,len+1);
   string index_str=::StringSubstr(capt_str,5,capt_str.Length()-7);
//--- Failed to find the index in the row - leave
   if(index_str=="")
      return;
//--- Set the column header index
   int index=(int)::StringToInteger(index_str);
   
//--- Get the column header by index
   CColumnCaptionView *caption=this.GetColumnCaption(index);
   if(caption==NULL)
      return;
   
//--- If the header does not have a sorting flag, set the ascending sorting flag
   if(caption.SortMode()==TABLE_SORT_MODE_NONE)
     {
      this.SetSortedColumnCaption(index);
     }
//--- Send a custom event to the chart with the header index in lparam, the sorting mode in dparam, and the object name in sparam
//--- Since the standard OBJECT_CLICK event passes cursor coordinates in lparam and dparam, we will pass negative values here
   ::EventChartCustom(this.m_chart_id, (ushort)CHARTEVENT_OBJECT_CLICK, -(1000+index), -(1000+caption.SortMode()), this.NameFG());
   ::ChartRedraw(this.m_chart_id);
  }

Os três métodos percorrem os cabeçalhos em um laço simples, procuram o objeto pelo flag ou pelo índice, conforme o caso, e retornam o dado correspondente.

Manipulador do evento personalizado do elemento ao clicar na área do objeto:

//+------------------------------------------------------------------+
//| Table visual representation class                                |
//+------------------------------------------------------------------+
class CTableView : public CPanel
  {
protected:
//--- Obtained table data
   CTable           *m_table_obj;                  // Pointer to table object (includes table and header models)
   CTableModel      *m_table_model;                // Pointer to the table model (obtained from CTable)
   CTableHeader     *m_header_model;               // Pointer to the table header model (obtained from CTable)
   
//--- View component data
   CTableHeaderView *m_header_view;                // Pointer to the table header (View)
   CPanel           *m_table_area;                 // Panel for placing table rows
   CContainer       *m_table_area_container;       // Container for placing a panel with table rows
   
//--- (1) Set and (2) return the table model
   bool              TableModelAssign(CTableModel *table_model);
   CTableModel      *GetTableModel(void)                                { return this.m_table_model;           }
   
//--- (1) Set and (2) return the table header model
   bool              HeaderModelAssign(CTableHeader *header_model);
   CTableHeader     *GetHeaderModel(void)                               { return this.m_header_model;          }

//--- Create a (1) header, (2) a table object and update the modified table from the model
   bool              CreateHeader(void);
   bool              CreateTable(void);
   bool              UpdateTable(void);
   
public:
//--- (1) Set and (2) return the table object
   bool              TableObjectAssign(CTable *table_obj);
   CTable           *GetTableObj(void)                                  { return this.m_table_obj;             }

//--- Return (1) the header, (2) the table layout area and (3) the table area container
   CTableHeaderView *GetHeader(void)                                    { return this.m_header_view;           }
   CPanel           *GetTableArea(void)                                 { return this.m_table_area;            }
   CContainer       *GetTableAreaContainer(void)                        { return this.m_table_area_container;  }

//--- Print the assigned (1) table, (2) table header and (3) table object model in the journal
   void              TableModelPrint(const bool detail);
   void              HeaderModelPrint(const bool detail, const bool as_table=false, const int cell_width=CELL_WIDTH_IN_CHARS);
   void              TablePrint(const int column_width=CELL_WIDTH_IN_CHARS);
   
//--- Get the column header (1) by index and (2) with sorting flag
   CColumnCaptionView *GetColumnCaption(const uint index)
                       { return(this.GetHeader()!=NULL ? this.GetHeader().GetColumnCaption(index) : NULL);     }
   CColumnCaptionView *GetSortedColumnCaption(void)
                       { return(this.GetHeader()!=NULL ? this.GetHeader().GetSortedColumnCaption(): NULL);     }

//--- Returns the visual representation object of the specified (1) row and (2) cell
   CTableRowView    *GetRowView(const uint index)
                       { return(this.GetTableArea()!=NULL ? this.GetTableArea().GetAttachedElementAt(index) : NULL); }
   CTableCellView   *GetCellView(const uint row,const uint col)
                       { return(this.GetRowView(row)!=NULL ? this.GetRowView(row).GetCellView(col) : NULL);    }
                       
//--- Return the number of table rows
   int               RowsTotal(void)
                       { return(this.GetTableArea()!=NULL ? this.GetTableArea().AttachedElementsTotal() : 0);  }

//--- Draw the appearance
   virtual void      Draw(const bool chart_redraw);
   
//--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type
   virtual int       Compare(const CObject *node,const int mode=0)const { return CPanel::Compare(node,mode);   }
   virtual bool      Save(const int file_handle);
   virtual bool      Load(const int file_handle);
   virtual int       Type(void)                                   const { return(ELEMENT_TYPE_TABLE_VIEW);     }
   
//--- Handler for the element user event when clicking on the object area
   virtual void      MousePressHandler(const int id, const long lparam, const double dparam, const string sparam);
   
//--- Sort the table by column value and direction
   bool              Sort(const uint column,const ENUM_TABLE_SORT_MODE sort_mode);
  
//--- Initialize (1) the class object and (2) default object colors
   void              Init(void);

//--- Constructors/destructor
                     CTableView(void);
                     CTableView(const string object_name, const string text, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableView (void){}
  };

O método envia um evento personalizado solicitando a ordenação pela coluna. Como lparam e dparam, no evento de clique do objeto, são usados para transmitir as coordenadas do cursor, utilizaremos valores negativos nesses parâmetros para identificar que se trata de um evento de ativação da ordenação:

  • Em lparam: (-1000) + índice da coluna,
  • Em dparam: (-1000) + tipo de ordenação.

Agora vamos aprimorar a classe de representação visual da tabela CTableView.

A classe representa um controle completo de tabela. As alterações adicionarão ao controle o redimensionamento das colunas e a ordenação pela coluna selecionada.

Declararemos novos métodos da classe:

//+------------------------------------------------------------------+
//| CTableView::Updates the modified table                           |
//+------------------------------------------------------------------+
bool CTableView::UpdateTable(void)
  {
   if(this.m_table_area==NULL)
      return false;
   
   int total_model=(int)this.m_table_model.RowsTotal();        // Number of rows in the model
   int total_view =this.m_table_area.AttachedElementsTotal();  // Number of rows in the visual representation
   int diff=total_model-total_view;                            // Difference in the number of rows of two components
   int y=1;                                                    // Vertical offset
   int table_height=0;                                         // Calculated panel height
   CTableRowView *row=NULL;                                    // Pointer to the visual representation of the row
   
//--- If there are more rows in the model than in the visual representation, create the missing rows in the visual representation at the end of the list
   if(diff>0)
     {
      //--- Get the last row of the visual representation of the table (added rows will be placed based on its coordinates)
      row=this.m_table_area.GetAttachedElementAt(total_view-1);
      //--- In the loop by the number of missing rows
      for(int i=total_view;i<total_view+diff;i++)
        {
         //--- create and attach the number of objects of visual representation of the table row to the diff panel
         row=this.m_table_area.InsertNewElement(ELEMENT_TYPE_TABLE_ROW_VIEW,"","TableRow"+(string)i,0,y+(row!=NULL ? row.Height()*i : 0),this.m_table_area.Width()-1,DEF_TABLE_ROW_H);
         if(row==NULL)
            return false;
        }
     }
 
//--- If there are more rows in the visual representation than in the model, remove the extra rows in the visual representation at the end of the list
   if(diff<0)
     {
      CListElm *list=this.m_table_area.GetListAttachedElements();
      if(list==NULL)
         return false;
      
      int  start=total_view-1;
      int  end=start-diff;
      bool res=true;
      for(int i=start;i>end;i--)
         res &=list.Delete(i);
      if(!res)
         return false;
     }
   
//--- In the loop through the list of rows of the table model
   for(int i=0;i<total_model;i++)
     {
      //--- get the next object of visual representation of the table row from the list of the rows panel
      row=this.m_table_area.GetAttachedElementAt(i);
      if(row==NULL)
         return false;
      //--- Check the object type
      if(row.Type()!=ELEMENT_TYPE_TABLE_ROW_VIEW)
         continue;
         
      //--- Set the row ID
      row.SetID(i);
      //--- Set the row background color depending on its index (even/odd)
      if(row.ID()%2==0)
         row.InitBackColorDefault(clrWhite);
      else
         row.InitBackColorDefault(C'242,242,242');
      row.BackColorToDefault();
      row.InitBackColorFocused(row.GetBackColorControl().NewColor(row.BackColor(),-4,-4,-4));
      
      //--- Get the row model from the table object
      CTableRow *row_model=this.m_table_model.GetRow(i);
      if(row_model==NULL)
         return false;

      //--- Update the cells of the table row object using the row model
      row.TableRowModelUpdate(row_model);
      //--- Calculate the new panel height value
      table_height+=row.Height();
     }
//--- Return the result of changing the panel size to the value calculated in the loop
   return this.m_table_area.ResizeH(table_height+y);
  }

Vamos analisar os métodos declarados.

Método que atualiza a tabela após uma alteração:

//+------------------------------------------------------------------+
//| CTableView::Element custom event handler                         |
//| when clicking on an object area                                  |
//+------------------------------------------------------------------+
void CTableView::MousePressHandler(const int id,const long lparam,const double dparam,const string sparam)
  {
   if(id==CHARTEVENT_OBJECT_CLICK && lparam>=0 && dparam>=0)
      return;
      
//--- Get the name of the table header object from sparam
   int len=::StringLen(this.NameFG());
   string header_str=::StringSubstr(sparam,0,len);
//--- If the retrieved name does not match the name of this object, it is not our event, we leave
   if(header_str!=this.NameFG())
      return;
   
//--- Set the column header index
//--- Since the standard OBJECT_CLICK event passes cursor coordinates in lparam and dparam,
//---  a negative value of the header index (the event occurred for) is passed for the handler
   int index=(int)::fabs(lparam+1000);
   
//--- Get the column header by index
   CColumnCaptionView *caption=this.GetColumnCaption(index);
   if(caption==NULL)
      return;
   
//--- Sort the list of rows by the sort value in the column header and update the table
   this.Sort(index,caption.SortMode());
   if(this.UpdateTable())
      this.Draw(true);
  }

A lógica do método está descrita nos comentários do código. Primeiro, comparamos a quantidade de linhas do modelo da tabela com a quantidade de linhas de sua representação visual. Se houver diferença, na representação visual são adicionadas as linhas da tabela ausentes ou removidas as excedentes. Em seguida, percorremos as linhas do modelo da tabela, as células da representação visual são atualizadas. Depois que todas as linhas da representação visual forem atualizadas, o método retorna o resultado do redimensionamento da tabela de acordo com a nova quantidade de linhas.

Manipulador do evento personalizado do elemento ao clicar na área do objeto:

//+------------------------------------------------------------------+
//| Table management class                                           |
//+------------------------------------------------------------------+
class CTableControl : public CPanel
  {
protected:
   CListObj          m_list_table_model;
//--- Add an object (1) of the model (CTable) and (2) table visual representation (CTableView) to the list
   bool              TableModelAdd(CTable *table_model,const int table_id,const string source);
   CTableView       *TableViewAdd(CTable *table_model,const string source);
//--- Update the specified column of the specified table
   bool              ColumnUpdate(const string source, CTable *table_model, const uint table, const uint col, const bool cells_redraw);
   
public:
//--- Returns (1) the model, (2) the table visual representation object and (3) the object type
   CTable           *GetTable(const uint index)                   { return this.m_list_table_model.GetNodeAtIndex(index);  }
   CTableView       *GetTableView(const uint index)               { return this.GetAttachedElementAt(index);               }
   
//--- Create a table based on the passed data
template<typename T>
   CTableView       *TableCreate(T &row_data[][],const string &column_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(const uint num_rows, const uint num_columns,const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE);
   CTableView       *TableCreate(CList &row_data,const string &column_names[],const int table_id=WRONG_VALUE);
   
//--- Return (1) the string value of the specified cell (Model), the specified (2) string and (3) the table cell (View)
   string            CellValueAt(const uint table, const uint row, const uint col);
   CTableRowView    *GetRowView(const uint table, const uint index);
   CTableCellView   *GetCellView(const uint table, const uint row, const uint col);
   
//--- Set the (1) value, (2) accuracy, (3) time display flags and (4) color name display flag to the specified cell (Model + View)
template<typename T>
   void              CellSetValue(const uint table, const uint row, const uint col, const T value, const bool chart_redraw);
   void              CellSetDigits(const uint table, const uint row, const uint col, const int digits, const bool chart_redraw);
   void              CellSetTimeFlags(const uint table, const uint row, const uint col, const uint flags, const bool chart_redraw);
   void              CellSetColorNamesFlag(const uint table, const uint row, const uint col, const bool flag, const bool chart_redraw);

//--- Set the foreground color to the specified cell (View)
   void              CellSetForeColor(const uint table, const uint row, const uint col, const color clr, const bool chart_redraw);
   
//--- (1) Set and (2) return the text anchor point in the specified cell (View)
   void              CellSetTextAnchor(const uint table, const uint row, const uint col, const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw);
   ENUM_ANCHOR_POINT CellTextAnchor(const uint table, const uint row, const uint col);
   
//--- Set the (1) accuracy, (2) time display flags, (3) color name display flag, (4) text anchor point and (5) data type in the specified column (View)
   void              ColumnSetDigits(const uint table, const uint col, const int digits, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetTimeFlags(const uint table, const uint col, const uint flags, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetColorNamesFlag(const uint table, const uint col, const bool flag, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetTextAnchor(const uint table, const uint col, const ENUM_ANCHOR_POINT anchor, const bool cells_redraw, const bool chart_redraw);
   void              ColumnSetDatatype(const uint table, const uint col, const ENUM_DATATYPE type, const bool cells_redraw, const bool chart_redraw);

//--- Object type
   virtual int       Type(void)                             const { return(ELEMENT_TYPE_TABLE_CONTROL_VIEW);               }

//--- Constructors/destructor
                     CTableControl(void) { this.m_list_table_model.Clear(); }
                     CTableControl(const string object_name, const long chart_id, const int wnd, const int x, const int y, const int w, const int h);
                    ~CTableControl(void) {}
  };

O método não processa o evento se lparam e dparam forem maiores ou iguais a zero. Em seguida, obtemos de lparam o índice da coluna e ordenamos a tabela de acordo com o modo de ordenação definido no cabeçalho dessa coluna. Depois disso, a tabela é atualizada.

Concluímos o aprimoramento das classes. Agora criaremos uma nova classe que permitirá criar tabelas de forma bastante prática a partir de dados previamente preparados. O gerenciamento das tabelas também ficará concentrado nessa mesma classe. Portanto, considerando sua finalidade, ela será uma classe de gerenciamento de tabelas. Um único objeto dessa classe poderá conter várias tabelas diferentes, acessíveis pelo identificador da tabela ou por seu nome definido pelo usuário.


Classe para simplificar a criação de tabelas

Continuaremos o desenvolvimento no arquivo \MQL5\Indicators\Tables\ControlsControls.mqh.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CTableControl::CTableControl(const string object_name,const long chart_id,const int wnd,const int x,const int y,const int w,const int h) :
   CPanel(object_name,"",chart_id,wnd,x,y,w,h)
  {
   this.m_list_table_model.Clear();
   this.SetName("Table Control");
  }

A classe representa um painel no qual são colocadas uma ou várias tabelas e é composta por duas listas:

  1. lista de modelos de tabelas,
  2. lista de representações visuais das tabelas criadas com base nos respectivos modelos.

Os métodos da classe permitem acessar a tabela desejada e gerenciar sua aparência, suas linhas, colunas e células.

Vamos analisar os métodos declarados da classe.

No construtor da classe, limpamos a lista de modelos de tabelas e definimos um nome padrão:

//+------------------------------------------------------------------+
//| Add the table model (CTable) object to the list                  |
//+------------------------------------------------------------------+
bool CTableControl::TableModelAdd(CTable *table_model,const int table_id,const string source)
  {
//--- Check the table model object
   if(table_model==NULL)
     {
      ::PrintFormat("%s::%s: Error. Failed to create Table Model object",source,__FUNCTION__);
      return false;
     }
//--- Set the ID to the table model - either by the size of the list or the specified one
   table_model.SetID(table_id<0 ? this.m_list_table_model.Total() : table_id);
//--- If a table model with the specified ID is in the list, report this, delete the object, and return 'false'
   this.m_list_table_model.Sort(0);
   if(this.m_list_table_model.Search(table_model)!=NULL)
     {
      ::PrintFormat("%s::%s: Error: Table Model object with ID %d already exists in the list",source,__FUNCTION__,table_id);
      delete table_model;
      return false;
     }
//--- If the table model is not added to the list, report this, delete the object, and return 'false'
   if(this.m_list_table_model.Add(table_model)<0)
     {
      ::PrintFormat("%s::%s: Error. Failed to add Table Model object to list",source,__FUNCTION__);
      delete table_model;
      return false;
     }
//--- All is successful
   return true;
  }

Se forem criados vários objetos dessa classe, cada um deverá receber um nome exclusivo, para evitar que a lista de objetos vinculados ao painel contenha dois ou mais elementos com o mesmo nome.

Método que adiciona um objeto do modelo da tabela à lista:

//+------------------------------------------------------------------+
//| Create a new object and add it to the list                       |
//| of the table visual representation (CTableView)                  |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableViewAdd(CTable *table_model,const string source)
  {
//--- Check the table model object
   if(table_model==NULL)
     {
      ::PrintFormat("%s::%s: Error. An invalid Table Model object was passed",source,__FUNCTION__);
      return NULL;
     }
//--- Create a new element - a visual representation of the table attached to the panel
   CTableView *table_view=this.InsertNewElement(ELEMENT_TYPE_TABLE_VIEW,"","TableView"+(string)table_model.ID(),1,1,this.Width()-2,this.Height()-2);
   if(table_view==NULL)
     {
      ::PrintFormat("%s::%s: Error. Failed to create Table View object",source,__FUNCTION__);
      return NULL;
     }
//--- Assign the table object (Model) and its ID to the Table graphical element (Model)
   table_view.TableObjectAssign(table_model);
   table_view.SetID(table_model.ID());
   return table_view;
  }

Método que cria um novo objeto de representação visual da tabela e o adiciona à lista:

//+-------------------------------------------------------------------+
//| Create a table while specifying a table array and a header array. | 
//| Defines the index and names of columns according to column_names  |
//| The number of rows is determined by the size of the row_data array|
//| also used to fill the table                                       |
//+-------------------------------------------------------------------+
template<typename T>
CTableView *CTableControl::TableCreate(T &row_data[][],const string &column_names[],const int table_id=WRONG_VALUE)
  {
//--- Create a table object using the specified parameters
   CTable *table_model=new CTable(row_data,column_names);
//--- If there are errors when creating or adding a table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return the table
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

Os dois métodos apresentados acima são utilizados nos métodos de criação de tabelas.

Método que cria uma tabela a partir de um array de dados e de um array de cabeçalhos:

//+------------------------------------------------------------------+
//| Create a table with a specified number of columns and rows.      |
//| The columns will have Excel names "A", "B", "C", etc.            |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const uint num_rows,const uint num_columns,const int table_id=WRONG_VALUE)
  {
   CTable *table_model=new CTable(num_rows,num_columns);
//--- If there are errors when creating or adding a table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return the table
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

Método que cria uma tabela com o número de colunas e linhas especificado:

//+-------------------------------------------------------------------------------------+
//| Create a table with column initialization according to column_names                 |
//| The number of rows is determined by the row_data parameter, with the 'matrix' type  |
//+-------------------------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(const matrix &row_data,const string &column_names[],const int table_id=WRONG_VALUE)
  {
   CTable *table_model=new CTable(row_data,column_names);
//--- If there are errors when creating or adding a table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return the table
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

Método que cria uma tabela a partir de uma matriz:

//+------------------------------------------------------------------+
//| Create a table with the specified table array based on the       |
//| row_data list containing objects with structure field data.      | 
//| Define the index and names of columns according to               |
//| column names in column_names                                     |
//+------------------------------------------------------------------+
CTableView *CTableControl::TableCreate(CList &row_data,const string &column_names[],const int table_id=WRONG_VALUE)
  {
   CTableByParam *table_model=new CTableByParam(row_data,column_names);
//--- If there are errors when creating or adding a table to the list, return NULL
   if(!this.TableModelAdd(table_model,table_id,__FUNCTION__))
      return NULL;
   
//--- Create and return the table
   return this.TableViewAdd(table_model,__FUNCTION__);
  }

Método que cria uma tabela com base em uma lista de parâmetros definidos pelo usuário e em um array de cabeçalhos de colunas:

//+------------------------------------------------------------------+
//| Set the value to the specified cell (Model + View)               |
//+------------------------------------------------------------------+
template<typename T>
void CTableControl::CellSetValue(const uint table,const uint row,const uint col,const T value,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Get the cell model from the table model
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL)
      return;
      
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
      
//--- Compare the value set in the cell with the passed one
   bool equal=false;
   ENUM_DATATYPE datatype=cell_model.Datatype();
   switch(datatype)
     {
      case TYPE_LONG    :  
      case TYPE_DATETIME:  
      case TYPE_COLOR   :  equal=(cell_model.ValueL()==value);                                           break;
      case TYPE_DOUBLE  :  equal=(::NormalizeDouble(cell_model.ValueD()-value,cell_model.Digits())==0);  break;
      //---TYPE_STRING
      default           :  equal=(::StringCompare(cell_model.ValueS(),(string)value)==0);                break;
     }
//--- If the values are equal, leave
   if(equal)
      return;
      
//--- Set a new value in the cell model;
//--- enter the value from the cell model into the cell visual representation object
//--- Redraw the cell with the chart update flag
   table_model.CellSetValue(row,col,value);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

Todos esses métodos seguem a mesma lógica: primeiro, criamos o modelo da tabela com base nos parâmetros recebidos e o adicionamos à lista. Em seguida, criamos sua representação visual. Esses são os principais métodos para criar tabelas a partir de uma ampla variedade de dados. A tabela resultante é colocada no painel, que funciona como área de posicionamento das tabelas.

Método que define um valor na célula especificada:

//+------------------------------------------------------------------+
//| Set the accuracy to the specified cell (Model + View)            |
//+------------------------------------------------------------------+
void CTableControl::CellSetDigits(const uint table,const uint row,const uint col,const int digits,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Get the cell model from the table model
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL || cell_model.Digits()==digits)
      return;
      
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- Set a new accuracy value in the cell model;
//--- enter the value from the cell model into the cell visual representation object
//--- Redraw the cell with the chart update flag
   table_model.CellSetDigits(row,col,digits);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

A lógica do método está descrita nos comentários do código. Se a célula já contiver o mesmo valor passado ao método para gravação, saímos do método. O novo valor é gravado primeiro na célula do modelo da tabela e depois na célula da representação visual, que então é redesenhada.

Método que define a precisão de exibição dos números fracionários na célula especificada:

//+------------------------------------------------------------------+
//| Set the time display flags                                       |
//| to the specified cell (Model + View)                             |
//+------------------------------------------------------------------+
void CTableControl::CellSetTimeFlags(const uint table,const uint row,const uint col,const uint flags,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Get the cell model from the table model
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL || cell_model.DatetimeFlags()==flags)
      return;
      
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- Set a new value for the time display flags in the cell model;
//--- enter the value from the cell model into the cell visual representation object
//--- Redraw the cell with the chart update flag
   table_model.CellSetTimeFlags(row,col,flags);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

O método é semelhante ao anterior. Se a precisão especificada para os números fracionários já corresponder à precisão atual, saímos do método. Em seguida, a precisão é definida no modelo da célula, o texto gerado pelo modelo da célula, já com a nova precisão, é aplicado à sua representação visual, e a célula é redesenhada.

Método que define os flags de exibição de tempo na célula especificada:

//+------------------------------------------------------------------+
//| Set the flag for displaying color names                          |
//| to the specified cell (Model + View)                             |
//+------------------------------------------------------------------+
void CTableControl::CellSetColorNamesFlag(const uint table,const uint row,const uint col,const bool flag,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
      return;
   
//--- Get the cell model from the table model
   CTableCell *cell_model=table_model.GetCell(row,col);
   if(cell_model==NULL || cell_model.ColorNameFlag()==flag)
      return;
      
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- Set a new value for the color name display flag in the cell model;
//--- enter the value from the cell model into the cell visual representation object
//--- Redraw the cell with the chart update flag
   table_model.CellSetColorNamesFlag(row,col,flag);
   cell_view.SetText(cell_model.Value());
   cell_view.Draw(chart_redraw);
  }

A lógica é idêntica à usada para definir a precisão da célula.

Método que define o flag de exibição dos nomes das cores na célula especificada:

//+------------------------------------------------------------------+
//| Set the foreground color to the specified cell (View)            |
//+------------------------------------------------------------------+
void CTableControl::CellSetForeColor(const uint table,const uint row,const uint col,const color clr,const bool chart_redraw)
  {
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- Set the cell background color in the cell visual representation object
//--- Redraw the cell with the chart update flag
   cell_view.SetForeColor(clr);
   cell_view.Draw(chart_redraw);
  }

Este método funciona exatamente como os analisados acima.

Método que define a cor do primeiro plano na célula especificada:

//+------------------------------------------------------------------+
//| Set the text anchor point to the specified cell (View)           |
//+------------------------------------------------------------------+
void CTableControl::CellSetTextAnchor(const uint table,const uint row,const uint col,const ENUM_ANCHOR_POINT anchor,const bool cell_redraw,const bool chart_redraw)
  {
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return;
   
//--- Set the text anchor point in the cell visual representation object
//--- Redraw the cell with the chart update flag
   cell_view.SetTextAnchor(anchor,cell_redraw,chart_redraw);
  }

Obtemos o objeto de representação visual da célula, definimos para ele uma nova cor do primeiro plano e redesenhamos a célula com a nova cor do texto.

Método que define o ponto de ancoragem do texto na célula especificada:

//+------------------------------------------------------------------+
//| Return the text anchor point in the specified cell (View)        |
//+------------------------------------------------------------------+
ENUM_ANCHOR_POINT CTableControl::CellTextAnchor(const uint table,const uint row,const uint col)
  {
//--- Get the cell visual representation object
   CTableCellView *cell_view=this.GetCellView(table,row,col);
   if(cell_view==NULL)
      return ANCHOR_LEFT_UPPER;
   
//--- Return the text anchor point
   return((ENUM_ANCHOR_POINT)cell_view.TextAnchor());
  }

Obtemos o objeto de representação visual da célula, definimos para ele um novo ponto de ancoragem e redesenhamos a célula com o texto na nova posição.

Método que retorna o ponto de ancoragem do texto na célula especificada:

//+------------------------------------------------------------------+
//| Update the specified column of the specified table               |
//+------------------------------------------------------------------+
bool CTableControl::ColumnUpdate(const string source,CTable *table_model,const uint table,const uint col,const bool cells_redraw)
  {
//--- Check the table model
   if(::CheckPointer(table_model)==POINTER_INVALID)
     {
      ::PrintFormat("%s::%s: Error. Invalid table model pointer passed",source,__FUNCTION__);
      return false;
     }
//--- Get the table visual representation
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s::%s: Error. Failed to get CTableView object",source,__FUNCTION__);
      return false;
     }
   
//--- In the loop by the rows of the table visual representation
   int total=table_view.RowsTotal();
   for(int i=0;i<total;i++)
     {
      //--- get the cell visual representation object in the specified column from the next table row
      CTableCellView *cell_view=this.GetCellView(table,i,col);
      if(cell_view==NULL)
        {
         ::PrintFormat("%s::%s: Error. Failed to get CTableCellView object (row %d, col %u)",source,__FUNCTION__,i,col);
         return false;
        }
      //--- Get the model of the corresponding cell from the row model
      CTableCell *cell_model=table_model.GetCell(i,col);
      if(cell_model==NULL)
        {
         ::PrintFormat("%s::%s: Error. Failed to get CTableCell object (row %d, col %u)",source,__FUNCTION__,i,col);
         return false;
        }
      
      //--- Set the value from the cell model to the cell visual representation object
      cell_view.SetText(cell_model.Value());
      //--- If specified, redraw the cell visual representation
      if(cells_redraw)
         cell_view.Draw(false);
     }
   return true;
  }

Obtemos o objeto de representação visual da célula e retornamos o ponto de ancoragem do texto definido para ele.

Método que atualiza a coluna especificada da tabela indicada:

//+------------------------------------------------------------------+
//| Set the accuracy in the specified column (Model + View)          |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetDigits(const uint table,const uint col,const int digits,const bool cells_redraw,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Set Digits for the specified column in the table model 
   table_model.ColumnSetDigits(col,digits);

//--- Update the display of the column data and, if specified, redraw the chart
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

A lógica do método está descrita nos comentários do código. Depois que o modelo da tabela é atualizado, nós o passamos para este método. Aqui, percorremos todas as linhas da representação visual da tabela, obtemos sucessivamente cada linha da representação visual e, da linha correspondente do modelo, a célula especificada. Em seguida, atualizamos a representação visual da célula com os dados de seu modelo, que então é redesenhada. Dessa forma, toda a coluna da tabela é redesenhada.

Método que define a precisão na coluna especificada:

//+------------------------------------------------------------------+
//| Set the time display flags                                       |
//| in the specified column (Model + View)                           |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetTimeFlags(const uint table,const uint col,const uint flags,const bool cells_redraw,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Set the time display flags for the specified column in the table model 
   table_model.ColumnSetTimeFlags(col,flags);

//--- Update the display of the column data and, if specified, redraw the chart
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

No modelo da tabela, definimos para a coluna a precisão especificada e chamamos o método de atualização da coluna da tabela analisado acima.

Método que define os flags de exibição de tempo na coluna especificada:

//+------------------------------------------------------------------+
//| Set the flag for displaying color names                          |
//| in the specified column (Model + View)                           |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetColorNamesFlag(const uint table,const uint col,const bool flag,const bool cells_redraw,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Set the time display flags for the specified column in the table model 
   table_model.ColumnSetColorNamesFlag(col,flag);

//--- Update the display of the column data and, if specified, redraw the chart
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

No modelo da tabela, definimos para a coluna os flags de exibição de tempo especificados e chamamos o método de atualização da coluna da tabela.

Método que define o flag de exibição dos nomes das cores na coluna especificada:

//+------------------------------------------------------------------+
//| Set the data type of the specified column ( (Model + View))      |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetDatatype(const uint table,const uint col,const ENUM_DATATYPE type,const bool cells_redraw,const bool chart_redraw)
  {
//--- Get the table model
   CTable *table_model=this.GetTable(table);
   if(table_model==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTable object",__FUNCTION__);
      return;
     }
//--- Set the data type for the specified column in the table model 
   table_model.ColumnSetDatatype(col,type);

//--- Update the display of the column data and, if specified, redraw the chart
   if(this.ColumnUpdate(__FUNCTION__,table_model,table,col,cells_redraw) && chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

No modelo da tabela, definimos para a coluna o flag de exibição dos nomes das cores especificado e chamamos o método de atualização da coluna da tabela.

Método que define o tipo de dados na coluna especificada:

//+------------------------------------------------------------------+
//| Set the text anchor point in the specified column (View)         |
//+------------------------------------------------------------------+
void CTableControl::ColumnSetTextAnchor(const uint table,const uint col,const ENUM_ANCHOR_POINT anchor,const bool cells_redraw,const bool chart_redraw)
  {
//--- Get the table visual representation
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return;
     }
//--- In a loop through all table rows
   int total=table_view.RowsTotal();
   for(int i=0;i<total;i++)
     {
      //--- get the next object of the cell visual representation
      //--- and set a new anchor point into the object
      CTableCellView *cell_view=this.GetCellView(table,i,col);
      if(cell_view!=NULL && cell_view.TextAnchor()!=anchor)
         cell_view.SetTextAnchor(anchor,cells_redraw,false);
     }
//--- If specified, update the chart
   if(chart_redraw)
      ::ChartRedraw(this.m_chart_id);
  }

No modelo da tabela, definimos para a coluna o tipo de dados especificado e chamamos o método de atualização da coluna da tabela.

Método que define o ponto de ancoragem do texto na coluna especificada:

//+------------------------------------------------------------------+
//| Return the string value of the specified cell (Model)            |
//+------------------------------------------------------------------+
string CTableControl::CellValueAt(const uint table,const uint row,const uint col)
  {
   CTable *table_model=this.GetTable(table);
   return(table_model!=NULL ? table_model.CellValueAt(row,col) : ::StringFormat("%s: Error. Failed to get table model",__FUNCTION__));
  }

Percorremos todas as linhas da representação visual da tabela, obtemos de cada linha a célula especificada e definimos nela o novo ponto de ancoragem. Ao final do laço, atualizamos o gráfico.

Método que retorna o valor de string da célula especificada:

//+------------------------------------------------------------------+
//| Return the specified table row (View)                            |
//+------------------------------------------------------------------+
CTableRowView *CTableControl::GetRowView(const uint table,const uint index)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return NULL;
     }
   return table_view.GetRowView(index);
  }

Obtemos do modelo da tabela a célula solicitada e retornamos seu valor como string. Em caso de erro, retornamos a mensagem correspondente.

Método que retorna a linha especificada da tabela:

//+------------------------------------------------------------------+
//| Return the specified table cell (View)                           |
//+------------------------------------------------------------------+
CTableCellView *CTableControl::GetCellView(const uint table,const uint row,const uint col)
  {
   CTableView *table_view=this.GetTableView(table);
   if(table_view==NULL)
     {
      ::PrintFormat("%s: Error. Failed to get CTableView object",__FUNCTION__);
      return NULL;
     }
   return table_view.GetCellView(row,col);
  }

Obtemos a representação visual da tabela pelo índice e retornamos dela um ponteiro para a linha solicitada.

Método que retorna a célula especificada da tabela:

//+------------------------------------------------------------------+
//|                                                   iTestTable.mq5 |
//|                                  Copyright 2025, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 0
#property indicator_plots   0

//+------------------------------------------------------------------+
//| Include libraries                                                |
//+------------------------------------------------------------------+
#include "Controls\Controls.mqh"    // Controls library

//--- Pointer to the CTableControl object
CTableControl *table_ctrl;

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Search for the chart subwindow
   int wnd=ChartWindowFind();

//--- Create table data
//--- Declare and fill the array of column headers with the dimension of 4
   string captions[]={"Column 0","Column 1","Column 2","Column 3"};
   
//--- Declare and fill the 15x4 data array
//--- Acceptable array types: double, long, datetime, color, string
   long array[15][4]={{ 1,  2,  3,  4},
                      { 5,  6,  7,  8},
                      { 9, 10, 11, 12},
                      {13, 14, 15, 16},
                      {17, 18, 19, 20},
                      {21, 22, 23, 24},
                      {25, 26, 27, 28},
                      {29, 30, 31, 32},
                      {33, 34, 35, 36},
                      {37, 38, 39, 40},
                      {41, 42, 43, 44},
                      {45, 46, 47, 48},
                      {49, 50, 51, 52},
                      {53, 54, 55, 56},
                      {57, 58, 59, 60}};
                      
//--- Create the table control graphical element
   table_ctrl=new CTableControl("TableControl0",0,wnd,30,30,460,184);
   if(table_ctrl==NULL)
      return INIT_FAILED;

//--- The chart should have one main element
   table_ctrl.SetAsMain();

//--- It is possible to set the table control parameters
   table_ctrl.SetID(0);                      // ID 
   table_ctrl.SetName("Table Control 0");    // Name

//--- Create the table 0 object (Model + View component) from the above-created 15x4 long array and the string array of column headers
   if(table_ctrl.TableCreate(array,captions)==NULL)
      return INIT_FAILED;
      
//--- Additionally, set the text output to be centered in the cell for columns 1, 2, 3, and to be left-aligned for column 0
   table_ctrl.ColumnSetTextAnchor(0,0,ANCHOR_LEFT,true,false);
   table_ctrl.ColumnSetTextAnchor(0,1,ANCHOR_CENTER,true,false);
   table_ctrl.ColumnSetTextAnchor(0,2,ANCHOR_CENTER,true,false);
   table_ctrl.ColumnSetTextAnchor(0,3,ANCHOR_CENTER,true,false);

//--- Draw the table
   table_ctrl.Draw(true);
   
//--- Get the table with index 0 and print it in the journal
   CTable *table=table_ctrl.GetTable(0);
   table.Print();
   
//--- Successful
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom deindicator initialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Remove the table control and destroy the library's shared resource manager
   delete table_ctrl;
   CCommonManager::DestroyInstance();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
//--- return value of prev_calculated for the next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- Call the OnChartEvent handler of the table control
   table_ctrl.OnChartEvent(id,lparam,dparam,sparam);
   
//--- If the event is mouse cursor movement
   if(id==CHARTEVENT_MOUSE_MOVE)
     {
      //--- get the cursor coordinates
      int x=table_ctrl.CursorX();
      int y=table_ctrl.CursorY();
      
      //--- set the X coordinate value to cell 0 of row 1
      table_ctrl.CellSetValue(0,1,0,x,false);
      
      //--- set the Y coordinate value to cell 1 of row 1
      //--- the text color in the cell depends on the sign of the Y coordinate (if the value is negative, the text is red
      table_ctrl.CellSetForeColor(0,1,1,(y<0 ? clrRed : table_ctrl.ForeColor()),false);
      table_ctrl.CellSetValue(0,1,1,y,true);
     }
  }
//+------------------------------------------------------------------+
//| Timer                                                            |
//+------------------------------------------------------------------+
void OnTimer(void)
  {
//--- Call the OnTimer handler of the table control
   table_ctrl.OnTimer();
  }
//+------------------------------------------------------------------+

Obtemos a representação visual da tabela pelo índice e retornamos dela um ponteiro para a célula solicitada da linha especificada.

Como podemos ver, essa classe é uma classe auxiliar simples para criar e manipular tabelas. Seus métodos oferecem recursos auxiliares para processar, alterar, definir e obter dados tabulares, utilizando os métodos das classes de modelo e de representação visual da tabela que implementamos anteriormente.

Vamos verificar o resultado.


Testando o resultado

Abriremos o arquivo do indicador de teste \MQL5\Indicators\Tables\iTestTable.mq5 e o reescreveremos da seguinte forma (os dados previamente preparados e a criação da tabela estão destacados com as cores correspondentes):

undefined

No manipulador de eventos, testaremos a gravação de alguns dados nas células em tempo real. Vamos ler as coordenadas do cursor e gravá-las nas duas primeiras células da segunda linha a partir do topo. Se a coordenada Y do cursor for negativa, exibiremos seu valor em vermelho.

Compilaremos o indicador e o executaremos no gráfico:

Como podemos ver, a interação da tabela com o usuário funciona conforme previsto.

É claro que ainda existem alguns detalhes a ajustar na exibição da tabela durante a interação com o cursor, mas tudo isso será corrigido e aprimorado gradualmente.



Conclusão

Até o momento, criamos uma ferramenta prática para exibir diferentes tipos de dados tabulares, permitindo personalizar, até certo ponto, a aparência da tabela criada com as configurações padrão.

Há várias formas de fornecer os dados usados na criação da tabela:

  • array bidimensional de dados da tabela e array de cabeçalhos das colunas,
  • número de colunas e linhas,
  • matriz: o cabeçalho é criado automaticamente no estilo do Excel,
  • lista CList de parâmetros definidos pelo usuário e array de cabeçalhos das colunas.

No futuro, talvez as classes de tabelas sejam aprimoradas para facilitar a adição e a remoção de linhas, colunas e células individuais. Por enquanto, porém, manteremos, por enquanto, este formato para criar e exibir tabelas.


Programas utilizados no artigo:

#
Nome Tipo
Descrição
1 Tables.mqh Biblioteca de classes Classes para criação do modelo da tabela
2 Base.mqh Biblioteca de classes Classes para criação do objeto base dos controles
3 Controls.mqh Biblioteca de classes Classes dos controles
4 iTestTable.mq5 Indicador de teste Indicador para testar o funcionamento do controle TableView
5 MQL5.zip Arquivo compactado Arquivo compactado com os arquivos apresentados acima, para extração no diretório MQL5 do terminal cliente

Todos os arquivos criados estão anexados ao artigo para estudo independente. O arquivo compactado pode ser extraído na pasta do terminal, e todos os arquivos serão colocados no diretório correto: \MQL5\Indicators\Tables.

O código-fonte completo do projeto, com todos os arquivos descritos no artigo, está disponível no repositório.


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

Arquivos anexados |
Tables.mqh (272.47 KB)
Base.mqh (300.19 KB)
Controls.mqh (804.81 KB)
iTestTable.mq5 (12.11 KB)
MQL5.zip (130.08 KB)
Últimos Comentários | Ir para discussão (3)
Alexey Viktorov
Alexey Viktorov | 28 out. 2025 em 16:19
Obrigado, Artem. Esse artigo é realmente muito útil.
Maxim Kuznetsov
Maxim Kuznetsov | 28 out. 2025 em 18:19
MetaQuotes:
Um clique do mouse no título da coluna da tabela (funcionamento do componente Controller) resultará em uma alteração na disposição dos dados no modelo da tabela (reorganização do componente Model),
Artem!! No MVC de verdade, clicar com o mouse em uma vista NÃO altera NADA no modelo. Um mesmo modelo pode ter várias vistas diferentes ao mesmo tempo, com diferentes ordenações e filtragens.
Nguyen Tuấn Anh
Nguyen Tuấn Anh | 7 mar. 2026 em 07:28
Obrigado pelo seu código e pelas instruções.
Você poderia aplicar a separação de interesses? Não coloque todas as classes em um único arquivo; separe-as por domínio e refatore o código.
Modelos matemáticos em estratégias de grade Modelos matemáticos em estratégias de grade
Neste artigo, veremos como aplicar a matemática às estratégias de grade. Analisaremos os princípios básicos de funcionamento da estratégia, suas vantagens e desvantagens. Você aprenderá a construir uma grade de negociação, definir parâmetros ideais e gerenciar os riscos de forma eficiente.
Redes neurais no trading: percepção adaptativa da dinâmica de mercado (Codificador) Redes neurais no trading: percepção adaptativa da dinâmica de mercado (Codificador)
O artigo apresenta uma arquitetura abrangente do codificador STE-FlowNet, que combina memória em pilha, processamento recorrente e um mecanismo de correlação para extrair dependências ocultas do mercado. O artigo mostra como esses módulos são integrados sequencialmente em uma única cadeia computacional, capaz de analisar as séries temporais sob múltiplas perspectivas.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Redes neurais no trading: Percepção adaptativa da dinâmica de mercado (STE-FlowNet) Redes neurais no trading: Percepção adaptativa da dinâmica de mercado (STE-FlowNet)
O framework STE-FlowNet oferece uma nova perspectiva para a análise de dados financeiros, reagindo a eventos reais do mercado, e não a timeframes fixos. Sua arquitetura preserva as dependências locais e temporais, permitindo rastrear até mesmo pequenos impulsos na dinâmica dos preços.