Tabelas no paradigma MVC em MQL5: colunas configuráveis e ordenáveis
Sumário
- Introdução
- Aprimorando as classes da biblioteca
- Classe para simplificar a criação de tabelas
- Testando o resultado
- Conclusão
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:
- lista de modelos de tabelas,
- 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
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Modelos matemáticos em estratégias de grade
Redes neurais no trading: percepção adaptativa da dinâmica de mercado (Codificador)
Está chegando o novo MetaTrader 5 e MQL5
Redes neurais no trading: Percepção adaptativa da dinâmica de mercado (STE-FlowNet)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
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),
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.