Implementação do modelo de tabela em MQL5: Aplicação do conto MVC
Conteúdo
- Introdução
- Um pouco sobre o conceito MVC (Model-View-Controller)
- Escrevendo classes para construir o modelo de tabela
- Listas ligadas como base para armazenar dados tabulares
- Classe da célula da tabela
- Classe da linha da tabela
- Classe o modelo de tabela
- Vamos testar o resultado
- Considerações finais
Introdução
Na programação, a arquitetura de uma aplicação desempenha um papel importante para garantir confiabilidade, escalabilidade e facilidade de manutenção. Uma das abordagens que ajuda a alcançar esses objetivos é o uso do padrão arquitetônico MVC (Model-View-Controller).
A concepção MVC permite dividir a aplicação em três componentes inter-relacionados: o modelo (gerenciamento de dados e lógica), a visualização (exibição dos dados) e o controlador (tratamento das ações do usuário). Essa divisão simplifica o desenvolvimento, o teste e a manutenção do código, tornando-o mais estruturado e flexível.
Neste artigo, veremos como aplicar os princípios do MVC para implementar o modelo de tabela na linguagem MQL5. As tabelas são uma ferramenta importante para armazenar, processar e exibir dados, e sua organização correta pode simplificar significativamente o trabalho com as informações. Criaremos classes para trabalhar com tabelas: células da tabela, linhas e o modelo de tabela. Para armazenar as células nas linhas e as linhas no modelo de tabela, usaremos listas ligadas da Biblioteca Padrão do MQL5, que permitem guardar e utilizar dados de forma eficiente.
Um pouco sobre o conceito MVC: o que é e para que serve
Imagine uma aplicação como um teatro. Existe o roteiro, que descreve o que deve acontecer (este é o modelo). Existe o palco, que é o que o público vê (esta é a visualização). E, por fim, há o diretor, que administra todo o processo e conecta os demais elementos (este é o controlador). É assim que funciona o padrão arquitetônico MVC, Model-View-Controller.
Esse conceito ajuda a separar as responsabilidades dentro da aplicação. O modelo cuida dos dados e da lógica, a visualização, da aparência e da exibição, e o controlador, do tratamento das ações do usuário. Essa separação torna o código mais claro, flexível e apropriado para o trabalho em equipe.
Vamos supor que você esteja criando uma tabela. A modelo sabe quais linhas e células existem nela e é capaz de modificá-las. A visualização desenha a tabela na tela. E o controlador reage quando o usuário clica "Adicionar linha", pois repassa a tarefa para o modelo e, em seguida, informa à visualização que deve ser atualizada.
O MVC é especialmente útil quando a aplicação se torna mais complexa; novas funções são adicionadas, a interface muda e vários desenvolvedores trabalham no projeto. Graças a uma arquitetura bem definida, torna-se mais fácil fazer alterações, testar os componentes separadamente e reutilizar o código.
Claro que esse esquema também tem seus pontos negativos. Para projetos muito simples, o MVC pode acabar sendo excessivo, pois será preciso separar até aquilo que poderia caber em duas funções. Mas, em aplicações sérias e escaláveis, essa estrutura rapidamente se justifica.
Com base no que foi mencionado, fazemos um resumo:
MVC é um padrão arquitetônico poderoso que ajuda a organizar o código, tornando-o mais claro, testável e escalável. É especialmente útil para aplicações complexas, onde é importante separar a lógica dos dados, a interface do usuário e o controle. Para projetos pequenos, seu uso é desnecessário.
Vemos que o paradigma Model-View-Controller se encaixa muito bem na nossa tarefa. A tabela será composta por objetos independentes:
- A célula da tabela.
Um objeto que armazena um valor de um dos tipos, seja real, inteiro ou string, e que possui ferramentas para gerenciar esse valor, sua definição e obtenção; - A linha da tabela.
Um objeto que armazena uma lista de objetos de células da tabela, possuindo ferramentas para gerenciar as células, seu posicionamento, adição e remoção; - A modelo da tabela.
Um objeto que armazena uma lista de objetos de linhas da tabela, possuindo ferramentas para gerenciar as linhas e colunas da tabela, seu posicionamento, adição e remoção, e que também tem acesso aos elementos de controle das linhas e das células.
Na figura abaixo, a seguir, está representada de forma esquemática a estrutura do modelo de tabela no tamanho 4x4:

Fig.1 Modelo de tabela 4x4
Vamos passar da teoria para a implementação.
Escrevendo classes para construir o modelo de tabela
Para criar todos os objetos, usaremos a Biblioteca Padrão do MQL5.
Cada objeto será um descendente de uma classe base da biblioteca. Isso permitirá armazenar esses objetos em listas de objetos.
Escreveremos todas as classes em um único arquivo de script de teste, para que tudo fique em um só lugar, visível e de acesso rápido. Posteriormente, distribuiremos as classes escritas em seus próprios arquivos conectados separadamente.
1. Listas ligadas como base para armazenar dados tabulares
Para armazenar dados tabulares, o mais adequado é a lista ligada CList. Nela, ao contrário da lista semelhante CArrayObj, existem métodos de acesso aos objetos vizinhos da lista, localizados à esquerda e à direita do objeto atual. Isso permitirá implementar facilmente a movimentação das células na linha, ou a movimentação das linhas na tabela, sua adição e remoção; ao mesmo tempo, a própria lista cuidará da indexação correta dos objetos movidos, adicionados ou removidos na lista.
Mas aqui há um detalhe. Se analisarmos os métodos de carregamento e salvamento da lista em arquivo, veremos que, ao carregar a partir de um arquivo, a classe da lista deve criar um novo objeto no método virtual CreateElement().
Esse método, nessa classe, simplesmente retorna NULL:
//--- method of creating an element of the list virtual CObject *CreateElement(void) { return(NULL); }
E isso significa que, para trabalhar com listas ligadas, e levando em conta que precisaremos realizar operações com arquivos, devemos herdar da classe CList e implementar esse método na nossa própria classe.
Além disso, ao observar os métodos de salvamento de objetos da Biblioteca Padrão em arquivo, é possível notar o seguinte algoritmo de gravação das propriedades de um objeto:
- No arquivo, é gravado um marcador de início dos dados (-1),
- No arquivo, é gravado o tipo do objeto,
- No arquivo, são gravadas sucessivamente todas as propriedades do objeto.
O primeiro e o segundo pontos são característicos de todos os métodos de salvamento/carregamento implementados nos objetos da Biblioteca Padrão. Portanto, seguindo essa mesma lógica, precisamos conhecer o tipo do objeto salvo na lista, para que, ao ler a partir do arquivo, seja criado um objeto desse tipo no método virtual CreateElement() da classe da lista herdada de CList.
Além disso, todos os objetos que podem ser carregados na lista — isto é, seus respectivos classes, devem ser declarados ou criados antes da implementação da classe da lista. Nesse caso, a lista "saberá" de quais objetos "se trata" e quais precisam ser criados.
No diretório do terminal \MQL5\Scripts\ criaremos uma nova pasta TableModel, e nela, um novo arquivo de script de teste TableModelTest.mq5.
Conectaremos o arquivo da lista ligada e faremos a declaração prévia dos futuros classes do modelo de tabela:
//+------------------------------------------------------------------+ //| TableModelTest.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" //+------------------------------------------------------------------+ //| Включаемые библиотеки | //+------------------------------------------------------------------+ #include <Arrays\List.mqh> //--- Форвард-декларация классов class CTableCell; // Класс ячейки таблицы class CTableRow; // Класс строки таблицы class CTableModel; // Класс модели таблицы
A declaração antecipada dos futuros classes é necessária para que a classe da lista ligada, que será herdada de CList, conheça esses tipos de classes, assim como conheça os tipos de objetos que terá que criar. Para isso, escreveremos a enumeração dos tipos de objetos, os macros auxiliares e a enumeração das formas de ordenação das listas:
//+------------------------------------------------------------------+ //| Включаемые библиотеки | //+------------------------------------------------------------------+ #include <Arrays\List.mqh> //--- Форвард-декларация классов class CTableCell; // Класс ячейки таблицы class CTableRow; // Класс строки таблицы class CTableModel; // Класс модели таблицы //+------------------------------------------------------------------+ //| Макросы | //+------------------------------------------------------------------+ #define MARKER_START_DATA -1 // Маркер начала данных в файле #define MAX_STRING_LENGTH 128 // Максимальная длина строки в ячейке //+------------------------------------------------------------------+ //| Перечисления | //+------------------------------------------------------------------+ enum ENUM_OBJECT_TYPE // Перечисление типов объектов { OBJECT_TYPE_TABLE_CELL=10000, // Ячейка таблицы OBJECT_TYPE_TABLE_ROW, // Строка таблицы OBJECT_TYPE_TABLE_MODEL, // Модель таблицы }; enum ENUM_CELL_COMPARE_MODE // Режимы сравнения ячеек таблицы { CELL_COMPARE_MODE_COL, // Сравнение по номеру колонки CELL_COMPARE_MODE_ROW, // Сравнение по номеру строки CELL_COMPARE_MODE_ROW_COL, // Сравнение по строке и колонке }; //+------------------------------------------------------------------+ //| Функции | //+------------------------------------------------------------------+ //--- Возвращает тип объекта как строку string TypeDescription(const ENUM_OBJECT_TYPE type) { string array[]; int total=StringSplit(EnumToString(type),StringGetCharacter("_",0),array); string result=""; for(int i=2;i<total;i++) { array[i]+=" "; array[i].Lower(); array[i].SetChar(0,ushort(array[i].GetChar(0)-0x20)); result+=array[i]; } result.TrimLeft(); result.TrimRight(); return result; } //+------------------------------------------------------------------+ //| Классы | //+------------------------------------------------------------------+
A função que retorna a descrição do tipo do objeto é construída com base no pressuposto de que todos os nomes das constantes de tipos de objetos começam com a substring "OBJECT_TYPE_". Então, é possível pegar a substring que segue essa parte, converter todos os caracteres da string obtida para minúsculas, transformar o primeiro caractere em maiúscula e, na string final, remover à esquerda e à direita todos os espaços e caracteres de controle.
Escreveremos nossa própria classe de lista ligada. Continuamos escrevendo o código no mesmo arquivo:
//+------------------------------------------------------------------+ //| Классы | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Класс связанного списка объектов | //+------------------------------------------------------------------+ class CListObj : public CList { protected: ENUM_OBJECT_TYPE m_element_type; // Тип создаваемого объекта в CreateElement() public: //--- Виртуальный метод (1) загрузки списка из файла, (2) создания элемента списка virtual bool Load(const int file_handle); virtual CObject *CreateElement(void); };
A classe CListObj é a nossa nova classe de lista ligada, herdada da classe CList da Biblioteca Padrão.
A única variável na classe será a variável na qual será registrado o tipo do objeto criado. Como o método CreateElement() é virtual e deve ter exatamente a mesma assinatura que o método da classe-pai, não podemos passar para ele o tipo do objeto criado. Mas podemos registrar esse tipo na variável declarada e, a partir dela, ler o tipo do objeto que deve ser criado.
Precisamos sobrescrever dois métodos virtuais da classe-pai: o método de carregamento a partir de arquivo e o método de criação de um novo objeto. Vamos examiná-los.
Método que carrega a lista a partir de arquivo:
//+------------------------------------------------------------------+ //| Загрузка списка из файла | //+------------------------------------------------------------------+ bool CListObj::Load(const int file_handle) { //--- Переменные CObject *node; bool result=true; //--- Проверяем хэндл if(file_handle==INVALID_HANDLE) return(false); //--- Загрузка и проверка маркера начала списка - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return(false); //--- Загрузка и проверка типа списка if(::FileReadInteger(file_handle,INT_VALUE)!=Type()) return(false); //--- Чтение размера списка (количество объектов) uint num=::FileReadInteger(file_handle,INT_VALUE); //--- Последовательно заново создаём элементы списка с помощью вызова метода Load() объектов node this.Clear(); for(uint i=0; i<num; i++) { //--- Читаем и проверяем маркер начала данных объекта - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return false; //--- Читаем тип объекта this.m_element_type=(ENUM_OBJECT_TYPE)::FileReadInteger(file_handle,INT_VALUE); node=this.CreateElement(); if(node==NULL) return false; this.Add(node); //--- Сейчас файловый указатель смещён относительно начала маркера объекта на 12 байт (8 - маркер, 4 - тип) //--- Поставим указатель на начало данных объекта и загрузим свойства объекта из файла методом Load() элемента node. if(!::FileSeek(file_handle,-12,SEEK_CUR)) return false; result &=node.Load(file_handle); } //--- Результат return result; }
Aqui, primeiro é verificado o início da lista, seu tipo e tamanho — isto é, a quantidade de elementos na lista; depois, em um loop pela quantidade de elementos, são lidos do arquivo os marcadores de início dos dados de cada objeto e seu tipo. O tipo obtido é registrado na variável m_element_type, e o método de criação de um novo elemento é chamado. Nesse método, é criado um novo elemento com o tipo obtido e ele é gravado na variável-ponteiro node, que, por sua vez, é adicionada à lista. Toda a lógica do método está descrita em detalhes nos comentários. Vamos analisar o método de criação de um novo elemento da lista.
Método que cria um elemento da lista:
//+------------------------------------------------------------------+ //| Метод создания элемента списка | //+------------------------------------------------------------------+ CObject *CListObj::CreateElement(void) { //--- В зависимости от типа объекта в m_element_type, создаём новый объект switch(this.m_element_type) { case OBJECT_TYPE_TABLE_CELL : return new CTableCell(); case OBJECT_TYPE_TABLE_ROW : return new CTableRow(); case OBJECT_TYPE_TABLE_MODEL : return new CTableModel(); default : return NULL; } }
Aqui, pressupõe-se que, antes da chamada do método, a variável m_element_type já contém o tipo do objeto sendo criado. Dependendo do tipo do elemento, é criado um novo objeto do tipo correspondente e retornado o ponteiro para ele. No futuro, ao desenvolver novos elementos de controle, seus tipos serão incluídos na enumeração ENUM_OBJECT_TYPE e, aqui, novos cases serão adicionados para criar novos tipos de objetos. A classe de lista ligada baseada no CList padrão está pronta. Agora ela pode armazenar todos os objetos de tipos conhecidos, salvar listas em arquivo, carregá-las de arquivo e restaurá-las corretamente.
2. A classe da célula da tabela
A célula da tabela é o elemento mais simples da tabela, armazenando algum valor. As células são reunidas em listas que representam as linhas da tabela. Uma lista corresponde a uma linha da tabela. Na nossa tabela, as células poderão armazenar simultaneamente apenas um valor de entre vários tipos — seja um valor real, inteiro ou de string.
Além do valor simples, a célula pode receber um objeto de um dos tipos conhecidos da enumeração ENUM_OBJECT_TYPE. Nesse caso, a célula pode armazenar um valor de qualquer um dos tipos listados, mais o ponteiro para um objeto cujo tipo é registrado em uma variável especial. Assim, no futuro, o componente View (a visualização) poderá ser instruído a exibir na célula esse objeto, para que seja possível interagir com ele por meio do componente Controller.
Como em uma única célula é possível armazenar vários tipos de valores diferentes, para registrá-los, armazená-los e retorná-los usaremos uma união (union), um tipo especial de dado que consiste em várias variáveis compartilhando a mesma área de memória. A união é semelhante a uma estrutura, mas aqui, diferentemente da estrutura, os diferentes membros da união se referem ao mesmo trecho de memória. Já na estrutura, para cada campo é reservado seu próprio espaço.
Continuamos escrevendo o código no arquivo já criado. Começamos a escrever a nova classe. Na seção protegida, escreveremos a união e declararemos as variáveis:
//+------------------------------------------------------------------+ //| Класс ячейки таблицы | //+------------------------------------------------------------------+ class CTableCell : public CObject { protected: //--- Объединение для хранения значений ячейки (double, long, string) union DataType { protected: double double_value; long long_value; ushort ushort_value[MAX_STRING_LENGTH]; public: //--- Установка значений void SetValueD(const double value) { this.double_value=value; } void SetValueL(const long value) { this.long_value=value; } void SetValueS(const string value) { ::StringToShortArray(value,ushort_value); } //--- Возврат значений double ValueD(void) const { return this.double_value; } long ValueL(void) const { return this.long_value; } string ValueS(void) const { string res=::ShortArrayToString(this.ushort_value); res.TrimLeft(); res.TrimRight(); return res; } }; //--- Переменные DataType m_datatype_value; // Значение ENUM_DATATYPE m_datatype; // Тип данных CObject *m_object; // Объект в ячейке ENUM_OBJECT_TYPE m_object_type; // Тип объекта в ячейке int m_row; // Номер строки int m_col; // Номер столбца int m_digits; // Точность представления данных uint m_time_flags; // Флаги отображения даты/времени bool m_color_flag; // Флаг отображения наименования цвета bool m_editable; // Флаг редактируемой ячейки public:
Na seção pública, escreveremos os métodos de acesso às variáveis protegidas, os métodos virtuais e os construtores da classe para os diferentes tipos de dados armazenados na célula:
public: //--- Возврат координат и свойств ячейки uint Row(void) const { return this.m_row; } uint Col(void) const { return this.m_col; } ENUM_DATATYPE Datatype(void) const { return this.m_datatype; } int Digits(void) const { return this.m_digits; } uint DatetimeFlags(void) const { return this.m_time_flags; } bool ColorNameFlag(void) const { return this.m_color_flag; } bool IsEditable(void) const { return this.m_editable; } //--- Возвращает (1) double, (2) long, (3) string значение double ValueD(void) const { return this.m_datatype_value.ValueD(); } long ValueL(void) const { return this.m_datatype_value.ValueL(); } string ValueS(void) const { return this.m_datatype_value.ValueS(); } //--- Возвращает значение в виде форматированной строки string Value(void) const { switch(this.m_datatype) { case TYPE_DOUBLE : return(::DoubleToString(this.ValueD(),this.Digits())); case TYPE_LONG : return(::IntegerToString(this.ValueL())); case TYPE_DATETIME: return(::TimeToString(this.ValueL(),this.m_time_flags)); case TYPE_COLOR : return(::ColorToString((color)this.ValueL(),this.m_color_flag)); default : return this.ValueS(); } } string DatatypeDescription(void) const { string type=::StringSubstr(::EnumToString(this.m_datatype),5); type.Lower(); return type; } //--- Установка значений переменных void SetRow(const uint row) { this.m_row=(int)row; } void SetCol(const uint col) { this.m_col=(int)col; } void SetDatatype(const ENUM_DATATYPE datatype) { this.m_datatype=datatype; } void SetDigits(const int digits) { this.m_digits=digits; } void SetDatetimeFlags(const uint flags) { this.m_time_flags=flags; } void SetColorNameFlag(const bool flag) { this.m_color_flag=flag; } void SetEditable(const bool flag) { this.m_editable=flag; } void SetPositionInTable(const uint row,const uint col) { this.SetRow(row); this.SetCol(col); } //--- Назначает объект в ячейку void AssignObject(CObject *object) { if(object==NULL) { ::PrintFormat("%s: Error. Empty object passed",__FUNCTION__); return; } this.m_object=object; this.m_object_type=(ENUM_OBJECT_TYPE)object.Type(); } //--- Снимает назначение объекта void UnassignObject(void) { this.m_object=NULL; this.m_object_type=-1; } //--- Устанавливает double-значение void SetValue(const double value) { this.m_datatype=TYPE_DOUBLE; if(this.m_editable) this.m_datatype_value.SetValueD(value); } //--- Устанавливает long-значение void SetValue(const long value) { this.m_datatype=TYPE_LONG; if(this.m_editable) this.m_datatype_value.SetValueL(value); } //--- Устанавливает datetime-значение void SetValue(const datetime value) { this.m_datatype=TYPE_DATETIME; if(this.m_editable) this.m_datatype_value.SetValueL(value); } //--- Устанавливает color-значение void SetValue(const color value) { this.m_datatype=TYPE_COLOR; if(this.m_editable) this.m_datatype_value.SetValueL(value); } //--- Устанавливает string-значение void SetValue(const string value) { this.m_datatype=TYPE_STRING; if(this.m_editable) this.m_datatype_value.SetValueS(value); } //--- Очищает данные void ClearData(void) { if(this.Datatype()==TYPE_STRING) this.SetValue(""); else this.SetValue(0.0); } //--- (1) Возвращает, (2) выводит в журнал описание объекта string Description(void); void Print(void); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта 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_CELL);} //--- Конструкторы/деструктор CTableCell(void) : m_row(0), m_col(0), m_datatype(-1), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueD(0); } //--- Принимает double-значение CTableCell(const uint row,const uint col,const double value,const int digits) : m_row((int)row), m_col((int)col), m_datatype(TYPE_DOUBLE), m_digits(digits), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueD(value); } //--- Принимает long-значение CTableCell(const uint row,const uint col,const long value) : m_row((int)row), m_col((int)col), m_datatype(TYPE_LONG), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueL(value); } //--- Принимает datetime-значение CTableCell(const uint row,const uint col,const datetime value,const uint time_flags) : m_row((int)row), m_col((int)col), m_datatype(TYPE_DATETIME), m_digits(0), m_time_flags(time_flags), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueL(value); } //--- Принимает color-значение CTableCell(const uint row,const uint col,const color value,const bool color_names_flag) : m_row((int)row), m_col((int)col), m_datatype(TYPE_COLOR), m_digits(0), m_time_flags(0), m_color_flag(color_names_flag), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueL(value); } //--- Принимает string-значение CTableCell(const uint row,const uint col,const string value) : m_row((int)row), m_col((int)col), m_datatype(TYPE_STRING), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueS(value); } ~CTableCell(void) {} };
Nos métodos para definir valores, foi feito de modo que primeiro seja definido o tipo do valor armazenado na célula, e depois, verificando o sinalizador que indica se a célula pode ser editada, o novo valor é salvo na célula somente se o sinalizador estiver ativado:
//--- Устанавливает double-значение void SetValue(const double value) { this.m_datatype=TYPE_DOUBLE; if(this.m_editable) this.m_datatype_value.SetValueD(value); }
Por que isso foi feito exatamente assim? Ao criar uma nova célula, ela é inicializada com o tipo de valor real. Se for necessário alterar o tipo de valor, mas a célula não for editável, é possível chamar o método de definição do valor apropriado com qualquer valor. O tipo do valor armazenado será alterado, mas o valor em si contido na célula não será afetado.
O método de limpeza dos dados define os valores numéricos como zero, e nas strings insere um espaço:
//--- Очищает данные void ClearData(void) { if(this.Datatype()==TYPE_STRING) this.SetValue(""); else this.SetValue(0.0); }
Mais adiante, faremos isso de outra forma, pois uma célula limpa não deve exibir nenhum dado; a célula deve ficar vazia. Faremos então um "valor vazio" para a célula, e, ao limpá-la, todos os valores registrados e exibidos nela serão apagados. Afinal, zero também é um valor válido, e no momento, ao limpar a célula, os dados numéricos são preenchidos com zeros. Isso não é correto.
Nos construtores paramétricos da classe, são passadas as coordenadas da célula na tabela, isto é, o número da linha e da coluna, e o valor do tipo requerido (double, long, datetime, color, string). Para alguns tipos de valores são necessárias informações adicionais:
- double — precisão do valor exibido (quantidade de casas decimais),
- datetime — sinalizadores de exibição de tempo (data/horas-minutos/segundos),
- color — sinalizador de exibição dos nomes das cores padrão conhecidas.
Nos construtores com esses tipos de valores armazenados nas células, são transmitidos parâmetros adicionais para definir o formato dos valores exibidos nas células:
//--- Принимает double-значение CTableCell(const uint row,const uint col,const double value,const int digits) : m_row((int)row), m_col((int)col), m_datatype(TYPE_DOUBLE), m_digits(digits), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueD(value); } //--- Принимает long-значение CTableCell(const uint row,const uint col,const long value) : m_row((int)row), m_col((int)col), m_datatype(TYPE_LONG), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueL(value); } //--- Принимает datetime-значение CTableCell(const uint row,const uint col,const datetime value,const uint time_flags) : m_row((int)row), m_col((int)col), m_datatype(TYPE_DATETIME), m_digits(0), m_time_flags(time_flags), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueL(value); } //--- Принимает color-значение CTableCell(const uint row,const uint col,const color value,const bool color_names_flag) : m_row((int)row), m_col((int)col), m_datatype(TYPE_COLOR), m_digits(0), m_time_flags(0), m_color_flag(color_names_flag), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueL(value); } //--- Принимает string-значение CTableCell(const uint row,const uint col,const string value) : m_row((int)row), m_col((int)col), m_datatype(TYPE_STRING), m_digits(0), m_time_flags(0), m_color_flag(false), m_editable(true), m_object(NULL), m_object_type(-1) { this.m_datatype_value.SetValueS(value); }
O método de comparação de dois objetos:
//+------------------------------------------------------------------+ //| Сравнение двух объектов | //+------------------------------------------------------------------+ int CTableCell::Compare(const CObject *node,const int mode=0) const { const CTableCell *obj=node; switch(mode) { case CELL_COMPARE_MODE_COL : return(this.Col()>obj.Col() ? 1 : this.Col()<obj.Col() ? -1 : 0); case CELL_COMPARE_MODE_ROW : return(this.Row()>obj.Row() ? 1 : this.Row()<obj.Row() ? -1 : 0); //---CELL_COMPARE_MODE_ROW_COL default : return ( this.Row()>obj.Row() ? 1 : this.Row()<obj.Row() ? -1 : this.Col()>obj.Col() ? 1 : this.Col()<obj.Col() ? -1 : 0 ); } }
O método permite comparar os parâmetros de dois objetos por um dos três critérios de comparação, isto é, pelo número da coluna, pelo número da linha ou simultaneamente pelos números de linha e coluna.
Esse método é necessário para possibilitar a ordenação das linhas da tabela pelos valores das colunas. Essa tarefa será realizada pelo componente Controller nos artigos seguintes.
O método para salvar as propriedades da célula em arquivo:
//+------------------------------------------------------------------+ //| Сохранение в файл | //+------------------------------------------------------------------+ bool CTableCell::Save(const int file_handle) { //--- Проверяем хэндл if(file_handle==INVALID_HANDLE) return(false); //--- Сохраняем маркер начала данных - 0xFFFFFFFFFFFFFFFF if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long)) return(false); //--- Сохраняем тип объекта if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем тип данных if(::FileWriteInteger(file_handle,this.m_datatype,INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем тип объекта в ячейке if(::FileWriteInteger(file_handle,this.m_object_type,INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем номер строки if(::FileWriteInteger(file_handle,this.m_row,INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем номер столбца if(::FileWriteInteger(file_handle,this.m_col,INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем точность представления данных if(::FileWriteInteger(file_handle,this.m_digits,INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем флаги отображения даты/времени if(::FileWriteInteger(file_handle,this.m_time_flags,INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем флаг отображения наименования цвета if(::FileWriteInteger(file_handle,this.m_color_flag,INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем флаг редактируемой ячейки if(::FileWriteInteger(file_handle,this.m_editable,INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем значение if(::FileWriteStruct(file_handle,this.m_datatype_value)!=sizeof(this.m_datatype_value)) return(false); //--- Всё успешно return true; }
Depois de escrever no arquivo o marcador inicial dos dados e o tipo do objeto, todas as propriedades da célula são salvas sucessivamente. A união deve ser salva como estrutura usando a função FileWriteStruct().
O método para carregar as propriedades da célula a partir de arquivo:
//+------------------------------------------------------------------+ //| Загрузка из файла | //+------------------------------------------------------------------+ bool CTableCell::Load(const int file_handle) { //--- Проверяем хэндл if(file_handle==INVALID_HANDLE) return(false); //--- Загружаем и проверяем маркер начала данных - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return(false); //--- Загружаем тип объекта if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type()) return(false); //--- Загружаем тип данных this.m_datatype=(ENUM_DATATYPE)::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем тип объекта в ячейке this.m_object_type=(ENUM_OBJECT_TYPE)::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем номер строки this.m_row=::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем номер столбца this.m_col=::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем точность представления данных this.m_digits=::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем флаги отображения даты/времени this.m_time_flags=::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем флаг отображения наименования цвета this.m_color_flag=::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем флаг редактируемой ячейки this.m_editable=::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем значение if(::FileReadStruct(file_handle,this.m_datatype_value)!=sizeof(this.m_datatype_value)) return(false); //--- Всё успешно return true; }
Após a leitura e a verificação do marcador de início dos dados e do tipo do objeto, todas as propriedades do objeto são carregadas sucessivamente na mesma ordem em que foram salvas. A união é lida por meio de FileReadStruct().
O método que retorna a descrição do objeto:
//+------------------------------------------------------------------+ //| Возвращает описание объекта | //+------------------------------------------------------------------+ string CTableCell::Description(void) { return(::StringFormat("%s: Row %u, Col %u, %s <%s>Value: %s", TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.Row(),this.Col(), (this.m_editable ? "Editable" : "Uneditable"),this.DatatypeDescription(),this.Value())); }
Aqui é criada uma string com alguns dos parâmetros da célula e ela é retornada, por exemplo, para double, no seguinte formato:
Table Cell: Row 2, Col 2, Uneditable <double>Value: 0.00
O método que imprime no diário a descrição do objeto:
//+------------------------------------------------------------------+ //| Выводит в журнал описание объекта | //+------------------------------------------------------------------+ void CTableCell::Print(void) { ::Print(this.Description()); }
Aqui simplesmente é impressa no diário a descrição do objeto.
//+------------------------------------------------------------------+ //| Класс строки таблицы | //+------------------------------------------------------------------+ class CTableRow : public CObject { protected: CTableCell m_cell_tmp; // Объект ячейки для поиска в списке CListObj m_list_cells; // Список ячеек uint m_index; // Индекс строки //--- Добавляет указанную ячейку в конец списка bool AddNewCell(CTableCell *cell); public: //--- (1) Устанавливает, (2) возвращает индекс строки void SetIndex(const uint index) { this.m_index=index; } uint Index(void) const { return this.m_index; } //--- Устанавливает позиции строки и колонки всем ячейкам void CellsPositionUpdate(void); //--- Создаёт новую ячейку и добавляет в конец списка CTableCell *CreateNewCell(const double value); CTableCell *CreateNewCell(const long value); CTableCell *CreateNewCell(const datetime value); CTableCell *CreateNewCell(const color value); CTableCell *CreateNewCell(const string value); //--- Возвращает (1) ячейку по индексу, (2) количество ячеек CTableCell *GetCell(const uint index) { return this.m_list_cells.GetNodeAtIndex(index); } uint CellsTotal(void) const { return this.m_list_cells.Total(); } //--- Устанавливает значение в указанную ячейку void CellSetValue(const uint index,const double value); void CellSetValue(const uint index,const long value); void CellSetValue(const uint index,const datetime value); void CellSetValue(const uint index,const color value); void CellSetValue(const uint index,const string value); //--- (1) назначает в ячейку, (2) снимает с ячейки назначенный объект void CellAssignObject(const uint index,CObject *object); void CellUnassignObject(const uint index); //--- (1) Удаляет (2) перемещает ячейку bool CellDelete(const uint index); bool CellMoveTo(const uint cell_index, const uint index_to); //--- Обнуляет данные ячеек строки void ClearData(void); //--- (1) Возвращает, (2) выводит в журнал описание объекта string Description(void); void Print(const bool detail, const bool as_table=false, const int cell_width=10); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта 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_ROW); } //--- Конструкторы/деструктор CTableRow(void) : m_index(0) {} CTableRow(const uint index) : m_index(index) {} ~CTableRow(void){} };
3. A classe da linha da tabela
A linha da tabela é, essencialmente, uma lista comum de células. A classe da linha deve ser capaz de adicionar novas células, remover e mover para outra posição as células já existentes na lista. A classe deve possuir um conjunto mínima e suficientemente completo de métodos para gerenciar as células da lista.
Continuamos escrevendo o código no mesmo arquivo. Nos parâmetros da classe há apenas uma variável, que é o índice da linha na tabela. Todo o restante são métodos para trabalhar com as propriedades da linha e com a lista de suas células.
Vamos analisar os métodos da classe.
O método para comparar duas linhas da tabela:
//+------------------------------------------------------------------+ //| Сравнение двух объектов | //+------------------------------------------------------------------+ int CTableRow::Compare(const CObject *node,const int mode=0) const { const CTableRow *obj=node; return(this.Index()>obj.Index() ? 1 : this.Index()<obj.Index() ? -1 : 0); }
Como as linhas só podem ser comparadas por seu único parâmetro, isto é, o índice da linha, essa comparação é exatamente o que é implementado aqui. O método será necessário para ordenar as linhas da tabela.
Métodos sobrecarregados para criar células com diferentes tipos de dados armazenados:
//+------------------------------------------------------------------+ //| Создаёт новую double-ячейку и добавляет в конец списка | //+------------------------------------------------------------------+ CTableCell *CTableRow::CreateNewCell(const double value) { //--- Создаём новый объект ячейки, хранящей значение с типом double CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,2); if(cell==NULL) { ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal()); return NULL; } //--- Добавляем созданную ячейку в конец списка if(!this.AddNewCell(cell)) { delete cell; return NULL; } //--- Возвращаем указатель на объект return cell; } //+------------------------------------------------------------------+ //| Создаёт новую long-ячейку и добавляет в конец списка | //+------------------------------------------------------------------+ CTableCell *CTableRow::CreateNewCell(const long value) { //--- Создаём новый объект ячейки, хранящей значение с типом long CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value); if(cell==NULL) { ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal()); return NULL; } //--- Добавляем созданную ячейку в конец списка if(!this.AddNewCell(cell)) { delete cell; return NULL; } //--- Возвращаем указатель на объект return cell; } //+------------------------------------------------------------------+ //| Создаёт новую datetime-ячейку и добавляет в конец списка | //+------------------------------------------------------------------+ CTableCell *CTableRow::CreateNewCell(const datetime value) { //--- Создаём новый объект ячейки, хранящей значение с типом datetime CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,TIME_DATE|TIME_MINUTES|TIME_SECONDS); if(cell==NULL) { ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal()); return NULL; } //--- Добавляем созданную ячейку в конец списка if(!this.AddNewCell(cell)) { delete cell; return NULL; } //--- Возвращаем указатель на объект return cell; } //+------------------------------------------------------------------+ //| Создаёт новую color-ячейку и добавляет в конец списка | //+------------------------------------------------------------------+ CTableCell *CTableRow::CreateNewCell(const color value) { //--- Создаём новый объект ячейки, хранящей значение с типом color CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value,true); if(cell==NULL) { ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal()); return NULL; } //--- Добавляем созданную ячейку в конец списка if(!this.AddNewCell(cell)) { delete cell; return NULL; } //--- Возвращаем указатель на объект return cell; } //+------------------------------------------------------------------+ //| Создаёт новую string-ячейку и добавляет в конец списка | //+------------------------------------------------------------------+ CTableCell *CTableRow::CreateNewCell(const string value) { //--- Создаём новый объект ячейки, хранящей значение с типом string CTableCell *cell=new CTableCell(this.m_index,this.CellsTotal(),value); if(cell==NULL) { ::PrintFormat("%s: Error. Failed to create new cell in row %u at position %u",__FUNCTION__, this.m_index, this.CellsTotal()); return NULL; } //--- Добавляем созданную ячейку в конец списка if(!this.AddNewCell(cell)) { delete cell; return NULL; } //--- Возвращаем указатель на объект return cell; }
Cinco métodos para criar uma nova célula (double, long, datetime, color, string) e adicioná-la ao final da lista. Os parâmetros adicionais de formato de exibição dos dados da célula são definidos com valores padrão. Eles poderão ser alterados depois da criação da célula. Primeiro é criado o novo objeto da célula, depois ele é adicionado ao final da lista. Se o objeto não for criado, ele é excluído para evitar vazamentos de memória.
O método que adiciona uma célula ao final da lista:
//+------------------------------------------------------------------+ //| Добавляет ячейку в конец списка | //+------------------------------------------------------------------+ bool CTableRow::AddNewCell(CTableCell *cell) { //--- Если передан пустой объект - сообщаем и возвращаем false if(cell==NULL) { ::PrintFormat("%s: Error. Empty CTableCell object passed",__FUNCTION__); return false; } //--- Устанавливаем индекс ячейки в списке и добавляем созданную ячейку в конец списка cell.SetPositionInTable(this.m_index,this.CellsTotal()); if(this.m_list_cells.Add(cell)==WRONG_VALUE) { ::PrintFormat("%s: Error. Failed to add cell (%u,%u) to list",__FUNCTION__,this.m_index,this.CellsTotal()); return false; } //--- Успешно return true; }
Qualquer célula recém-criada sempre é adicionada ao final da lista. Depois, ela poderá ser movida para a posição necessária por meio dos métodos da classe do modelo de tabela, que será criada mais adiante.
Métodos sobrecarregados para definir o valor em uma célula indicada:
//+------------------------------------------------------------------+ //| Устанавливает double-значение в указанную ячейку | //+------------------------------------------------------------------+ void CTableRow::CellSetValue(const uint index,const double value) { //--- Получаем из списка нужную ячейку и записываем в неё новое значение CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.SetValue(value); } //+------------------------------------------------------------------+ //| Устанавливает long-значение в указанную ячейку | //+------------------------------------------------------------------+ void CTableRow::CellSetValue(const uint index,const long value) { //--- Получаем из списка нужную ячейку и записываем в неё новое значение CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.SetValue(value); } //+------------------------------------------------------------------+ //| Устанавливает datetime-значение в указанную ячейку | //+------------------------------------------------------------------+ void CTableRow::CellSetValue(const uint index,const datetime value) { //--- Получаем из списка нужную ячейку и записываем в неё новое значение CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.SetValue(value); } //+------------------------------------------------------------------+ //| Устанавливает color-значение в указанную ячейку | //+------------------------------------------------------------------+ void CTableRow::CellSetValue(const uint index,const color value) { //--- Получаем из списка нужную ячейку и записываем в неё новое значение CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.SetValue(value); } //+------------------------------------------------------------------+ //| Устанавливает string-значение в указанную ячейку | //+------------------------------------------------------------------+ void CTableRow::CellSetValue(const uint index,const string value) { //--- Получаем из списка нужную ячейку и записываем в неё новое значение CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.SetValue(value); }
Obtemos pelo índice a célula desejada da lista e definimos para ela o valor. Se a célula não for editável, o método SetValue() do objeto da célula definirá apenas o tipo do valor a ser configurado. O próprio valor não será definido.
Método que atribui um objeto à célula:
//+------------------------------------------------------------------+ //| Назначает в ячейку объект | //+------------------------------------------------------------------+ void CTableRow::CellAssignObject(const uint index,CObject *object) { //--- Получаем из списка нужную ячейку и записываем в неё указатель на объект CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.AssignObject(object); }
Obtemos pelo índice o objeto célula e, por meio do seu método AssignObject(), atribuímos à célula o ponteiro para o objeto.
Método que cancela o objeto atribuído à célula:
//+------------------------------------------------------------------+ //| Отменяет для ячейки назначенный объект | //+------------------------------------------------------------------+ void CTableRow::CellUnassignObject(const uint index) { //--- Получаем из списка нужную ячейку и отменяем в ней указатель на объект и его тип CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.UnassignObject(); }
Obtemos pelo índice o objeto célula e, por meio do seu método UnassignObject(), removemos da célula o ponteiro anteriormente atribuído.
Método que exclui uma célula:
//+------------------------------------------------------------------+ //| Удаляет ячейку | //+------------------------------------------------------------------+ bool CTableRow::CellDelete(const uint index) { //--- Удаляем ячейку в списке по индексу if(!this.m_list_cells.Delete(index)) return false; //--- Обновляем индексы для оставшихся ячеек в списке this.CellsPositionUpdate(); return true; }
Com o método Delete() da classe CList, removemos da lista a célula. Depois que a célula foi removida da lista, os índices das demais células são alterados. Com o método CellsPositionUpdate(), atualizamos os índices de todas as células restantes na lista.
Método que move a célula para a posição indicada:
//+------------------------------------------------------------------+ //| Перемещает ячейку на указанную позицию | //+------------------------------------------------------------------+ bool CTableRow::CellMoveTo(const uint cell_index,const uint index_to) { //--- Выбираем нужную ячейку по индексу в списке, делая её текущей CTableCell *cell=this.GetCell(cell_index); //--- Перемещаем текущую ячейку на указанную позицию в списке if(cell==NULL || !this.m_list_cells.MoveToIndex(index_to)) return false; //--- Обновляем индексы всех ячеек в списке this.CellsPositionUpdate(); return true; }
Para que a classe CList possa operar com um objeto, esse objeto deve ser o atual na lista. Ele se torna atual, por exemplo, quando é selecionado. Portanto, aqui primeiro obtemos a célula pelo índice. A célula se torna atual e, em seguida, usando o método MoveToIndex() da classe CList, movemos o objeto para a posição requerida na lista. Após mover o objeto com sucesso para a nova posição, é necessário corrigir os índices dos demais objetos, o que é feito por meio do método CellsPositionUpdate().
Método que define as posições de linha e coluna para todas as células da lista:
//+------------------------------------------------------------------+ //| Устанавливает позиции строки и колонки всем ячейкам | //+------------------------------------------------------------------+ void CTableRow::CellsPositionUpdate(void) { //--- В цикле по всем ячейкам в списке for(int i=0;i<this.m_list_cells.Total();i++) { //--- получаем очередную ячейку и устанавливаем в неё индексы строки и столбца CTableCell *cell=this.GetCell(i); if(cell!=NULL) cell.SetPositionInTable(this.Index(),this.m_list_cells.IndexOf(cell)); } }
A classe CList permite determinar o índice do objeto atual na lista. Para isso, o objeto deve estar selecionado. Aqui, percorremos em um loop todos os objetos célula da lista, selecionamos cada um e descobrimos seu índice com o método IndexOf() da classe CList. O índice da linha e o índice encontrado da célula são definidos no objeto célula usando o seu método SetPositionInTable().
Método que zera os dados das células da linha:
//+------------------------------------------------------------------+ //| Обнуляет данные ячеек строки | //+------------------------------------------------------------------+ void CTableRow::ClearData(void) { //--- В цикле по всем ячейкам в списке for(uint i=0;i<this.CellsTotal();i++) { //--- получаем очередную ячейку и устанавливаем в неё пустое значение CTableCell *cell=this.GetCell(i); if(cell!=NULL) cell.ClearData(); } }
Zeramos cada célula da lista em um loop por meio do método ClearData() do objeto célula. Para dados string, uma string vazia é escrita na célula, e para dados numéricos, o valor zero.
O método que retorna a descrição do objeto:
//+------------------------------------------------------------------+ //| Возвращает описание объекта | //+------------------------------------------------------------------+ string CTableRow::Description(void) { return(::StringFormat("%s: Position %u, Cells total: %u", TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.Index(),this.CellsTotal())); }
A partir das propriedades e dos dados do objeto, é montada uma string que é retornada, por exemplo, neste formato:
Table Row: Position 1, Cells total: 4:
O método que imprime no diário a descrição do objeto:
//+------------------------------------------------------------------+ //| Выводит в журнал описание объекта | //+------------------------------------------------------------------+ void CTableRow::Print(const bool detail, const bool as_table=false, const int cell_width=10) { //--- Количество ячеек int total=(int)this.CellsTotal(); //--- Если вывод в табличном виде string res=""; if(as_table) { //--- создаём строку таблицы из значений всех ячеек string head=" Row "+(string)this.Index(); string res=::StringFormat("|%-*s |",cell_width,head); for(int i=0;i<total;i++) { CTableCell *cell=this.GetCell(i); if(cell==NULL) continue; res+=::StringFormat("%*s |",cell_width,cell.Value()); } //--- Выводим строку в журнал ::Print(res); return; } //--- Выводим заголовок в виде описания строки ::Print(this.Description()+(detail ? ":" : "")); //--- Если детализированное описание if(detail) { //--- Вывод не в табличном виде //--- В цикле по спискук ячеек строки for(int i=0; i<total; i++) { //--- получаем текущую ячейку и добавляем в итоговую строку её описание CTableCell *cell=this.GetCell(i); if(cell!=NULL) res+=" "+cell.Description()+(i<total-1 ? "\n" : ""); } //--- Выводим в журнал созданную в цикле строку ::Print(res); } }
Para a saída não tabular de dados no diário, primeiro é impresso no diário o cabeçalho na forma de descrição da linha. Em seguida, se o sinalizador de saída detalhada estiver definido, em um laço sobre a lista de células são impressas no diário as descrições de cada célula.
Como resultado, a saída detalhada de uma linha de tabela no diário fica, por exemplo, assim (para o modo não tabular):
Table Row: Position 0, Cells total: 4: Table Cell: Row 0, Col 0, Editable <long>Value: 10 Table Cell: Row 0, Col 1, Editable <long>Value: 21 Table Cell: Row 0, Col 2, Editable <long>Value: 32 Table Cell: Row 0, Col 3, Editable <long>Value: 43
Para a saída em formato tabular, o resultado será, por exemplo, o seguinte:
| Row 0 | 0 | 1 | 2 | 3 |
O método que salva a linha da tabela em arquivo:
//+------------------------------------------------------------------+ //| Сохранение в файл | //+------------------------------------------------------------------+ bool CTableRow::Save(const int file_handle) { //--- Проверяем хэндл if(file_handle==INVALID_HANDLE) return(false); //--- Сохраняем маркер начала данных - 0xFFFFFFFFFFFFFFFF if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long)) return(false); //--- Сохраняем тип объекта if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем индекс if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем список ячеек if(!this.m_list_cells.Save(file_handle)) return(false); //--- Успешно return true; }
Gravamos o marcador de início dos dados e, em seguida, o tipo do objeto. Esse é o cabeçalho padrão de cada objeto no arquivo. Depois disso, segue a gravação em arquivo das propriedades do objeto. Aqui são gravados em arquivo o índice da linha e, em seguida, a lista de células.
O método que carrega uma linha a partir de arquivo:
//+------------------------------------------------------------------+ //| Загрузка из файла | //+------------------------------------------------------------------+ bool CTableRow::Load(const int file_handle) { //--- Проверяем хэндл if(file_handle==INVALID_HANDLE) return(false); //--- Загружаем и проверяем маркер начала данных - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return(false); //--- Загружаем тип объекта if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type()) return(false); //--- Загружаем индекс this.m_index=::FileReadInteger(file_handle,INT_VALUE); //--- Загружаем список ячеек if(!this.m_list_cells.Load(file_handle)) return(false); //--- Успешно return true; }
Aqui, tudo segue a mesma ordem usada na gravação. Primeiro é carregado e verificado o marcador de início dos dados e o tipo do objeto, depois é carregado o índice da linha e toda a lista de células por completo.
4. A classe do modelo de tabela
A modelo de tabela, em sua forma simples, representa uma lista ligada de linhas que, por sua vez, contêm listas ligadas de células. A nosso modelo, que criaremos hoje, receberá na entrada um array bidimensional de um dos cinco tipos (double, long, datetime, color, string) e criará a partir dele uma tabela virtual. Mais adiante ampliaremos essa classe para aceitar outros argumentos, a fim de criar tabelas a partir de outros dados de entrada. Esse próprio modelo será considerado o modelo padrão.
Continuamos escrevendo o código no mesmo arquivo \MQL5\Scripts\TableModel\TableModelTest.mq5.
A classe do modelo de tabela é, essencialmente, uma lista comum de linhas com métodos de gerenciamento de linhas, colunas e células. Escrevemos o corpo da classe com todas as variáveis e métodos e, em seguida, analisamos os métodos declarados da classe:
//+------------------------------------------------------------------+ //| Класс модели таблицы | //+------------------------------------------------------------------+ class CTableModel : public CObject { protected: CTableRow m_row_tmp; // Объект строки для поиска в списке CListObj m_list_rows; // Список строк таблицы //--- Создаёт модель таблицы из двумерного массива template<typename T> void CreateTableModel(T &array[][]); //--- Возвращает корректный тип данных ENUM_DATATYPE GetCorrectDatatype(string type_name) { return ( //--- Целочисленное значение type_name=="bool" || type_name=="char" || type_name=="uchar" || type_name=="short"|| type_name=="ushort" || type_name=="int" || type_name=="uint" || type_name=="long" || type_name=="ulong" ? TYPE_LONG : //--- Вещественное значение type_name=="float"|| type_name=="double" ? TYPE_DOUBLE : //--- Значение даты/времени type_name=="datetime" ? TYPE_DATETIME : //--- Значение цвета type_name=="color" ? TYPE_COLOR : /*--- Строковое значение */ TYPE_STRING ); } //--- Создаёт и добавляет новую пустую строку в конец списка CTableRow *CreateNewEmptyRow(void); //--- Добавляет строку в конец списка bool AddNewRow(CTableRow *row); //--- Устанавливает позиции строки и колонки всем ячейкам таблицы void CellsPositionUpdate(void); public: //--- Возвращает (1) ячейку, (2) строку по индексу, количество (3) строк, ячеек (4) в указанной строке, (5) в таблице CTableCell *GetCell(const uint row, const uint col); CTableRow *GetRow(const uint index) { return this.m_list_rows.GetNodeAtIndex(index); } uint RowsTotal(void) const { return this.m_list_rows.Total(); } uint CellsInRow(const uint index); uint CellsTotal(void); //--- Устанавливает (1) значение, (2) точность, (3) флаги отображения времени, (4) флаг отображения имён цветов в указанную ячейку template<typename T> void CellSetValue(const uint row, const uint col, const T value); void CellSetDigits(const uint row, const uint col, const int digits); void CellSetTimeFlags(const uint row, const uint col, const uint flags); void CellSetColorNamesFlag(const uint row, const uint col, const bool flag); //--- (1) Назначает, (2) отменяет объект в ячейке void CellAssignObject(const uint row, const uint col,CObject *object); void CellUnassignObject(const uint row, const uint col); //--- (1) Удаляет (2) перемещает ячейку bool CellDelete(const uint row, const uint col); bool CellMoveTo(const uint row, const uint cell_index, const uint index_to); //--- (1) Возвращает, (2) выводит в журнал описание ячейки, (3) назначенный в ячейку объект string CellDescription(const uint row, const uint col); void CellPrint(const uint row, const uint col); CObject *CellGetObject(const uint row, const uint col); public: //--- Создаёт новую строку и (1) добавляет в конец списка, (2) вставляет в указанную позицию списка CTableRow *RowAddNew(void); CTableRow *RowInsertNewTo(const uint index_to); //--- (1) Удаляет (2) перемещает строку, (3) очищает данные строки bool RowDelete(const uint index); bool RowMoveTo(const uint row_index, const uint index_to); void RowResetData(const uint index); //--- (1) Возвращает, (2) выводит в журнал описание строки string RowDescription(const uint index); void RowPrint(const uint index,const bool detail); //--- (1) Удаляет (2) перемещает столбец, (3) очищает данные столбца bool ColumnDelete(const uint index); bool ColumnMoveTo(const uint row_index, const uint index_to); void ColumnResetData(const uint index); //--- (1) Возвращает, (2) выводит в журнал описание таблицы string Description(void); void Print(const bool detail); void PrintTable(const int cell_width=10); //--- (1) Очищает данные, (2) уничтожает модель void ClearData(void); void Destroy(void); //--- Виртуальные методы (1) сравнения, (2) сохранения в файл, (3) загрузки из файла, (4) тип объекта 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_MODEL); } //--- Конструкторы/деструктор CTableModel(void){} CTableModel(double &array[][]) { this.CreateTableModel(array); } CTableModel(long &array[][]) { this.CreateTableModel(array); } CTableModel(datetime &array[][]) { this.CreateTableModel(array); } CTableModel(color &array[][]) { this.CreateTableModel(array); } CTableModel(string &array[][]) { this.CreateTableModel(array); } ~CTableModel(void){} };
Em essência, aqui há apenas um objeto de lista ligada de linhas da tabela e métodos para gerenciar linhas, células e colunas, além de construtores que aceitam diferentes tipos de arrays bidimensionais.
No construtor da classe é passado um array e é chamado o método para criar o modelo de tabela:
//+------------------------------------------------------------------+ //| Создаёт модель таблицы из двумерного массива | //+------------------------------------------------------------------+ template<typename T> void CTableModel::CreateTableModel(T &array[][]) { //--- Получаем из свойств массива количество строк и столбцов таблицы int rows_total=::ArrayRange(array,0); int cols_total=::ArrayRange(array,1); //--- В цикле по индексам строк for(int r=0; r<rows_total; r++) { //--- создаём новую пустую строку и добавляем её в конец списка строк CTableRow *row=this.CreateNewEmptyRow(); //--- Если строка создана и добавлена в список, if(row!=NULL) { //--- В цикле по количеству ячеек в строке //--- создаём все ячейки, добавляя каждую новую в конец списка ячеек строки for(int c=0; c<cols_total; c++) row.CreateNewCell(array[r][c]); } } }
A lógica do método está totalmente descrita nos comentários. A primeira dimensão do array corresponde às linhas da tabela, a segunda dimensão corresponde às células de cada linha. Ao criar as células, é usado o tipo de dado do array que foi passado ao método.
Dessa forma, podemos criar vários modelos de tabela cujas células, inicialmente, armazenam tipos diferentes de dados (double, long, datetime, color e string). Posteriormente, após a criação do modelo de tabela, os tipos de dados armazenados nas células podem ser alterados.
O método que cria uma nova linha vazia e a adiciona ao final da lista:
//+------------------------------------------------------------------+ //| Создаёт новую пустую строку и добавляет в конец списка | //+------------------------------------------------------------------+ CTableRow *CTableModel::CreateNewEmptyRow(void) { //--- Создаём новый объект строки CTableRow *row=new CTableRow(this.m_list_rows.Total()); if(row==NULL) { ::PrintFormat("%s: Error. Failed to create new row at position %u",__FUNCTION__, this.m_list_rows.Total()); return NULL; } //--- Если строку не удалось добавить в список - удаляем созданный новый объект и возвращаем NULL if(!this.AddNewRow(row)) { delete row; return NULL; } //--- Успешно - возвращаем указатель на созданный объект return row; }
O método cria um novo objeto da classe CTableRow e o adiciona ao final da lista de linhas por meio do método AddNewRow(). Em caso de erro ao adicionar, o novo objeto criado é removido e é retornado NULL. Em caso de execução bem-sucedida, o método retorna o ponteiro para a linha recém-adicionada à lista.
O método que adiciona um objeto linha ao final da lista:
//+------------------------------------------------------------------+ //| Добавляет строку в конец списка | //+------------------------------------------------------------------+ bool CTableModel::AddNewRow(CTableRow *row) { //--- Если передан пустой объект - сообщаем об этом и возвращаем false if(row==NULL) { ::PrintFormat("%s: Error. Empty CTableRow object passed",__FUNCTION__); return false; } //--- Устанавливаем строке её индекс в списке и добавляем её в конец списка row.SetIndex(this.RowsTotal()); if(this.m_list_rows.Add(row)==WRONG_VALUE) { ::PrintFormat("%s: Error. Failed to add row (%u) to list",__FUNCTION__,row.Index()); return false; } //--- Успешно return true; }
Ambos os métodos analisados acima estão na seção protegida da classe, trabalham em conjunto e servem para uso interno ao adicionar novas linhas à tabela.
O método para criar uma nova linha e adicioná-la ao final da lista:
//+------------------------------------------------------------------+ //| Создаёт новую строку и добавляет в конец списка | //+------------------------------------------------------------------+ CTableRow *CTableModel::RowAddNew(void) { //--- Создаём новую пустую строку и добавляем её в конец списка строк CTableRow *row=this.CreateNewEmptyRow(); if(row==NULL) return NULL; //--- Создаём ячейки по количеству ячеек первой строки for(uint i=0;i<this.CellsInRow(0);i++) row.CreateNewCell(0.0); row.ClearData(); //--- Успешно - возвращаем указатель на созданный объект return row; }
Este é um método público. Ele é usado para adicionar à tabela uma nova linha com células. A quantidade de células da linha criada é obtida a partir da primeira linha da tabela.
O método para criar e adicionar uma nova linha em uma posição específica da lista:
//+------------------------------------------------------------------+ //| Создаёт и добавляет новую строку в указанную позицию списка | //+------------------------------------------------------------------+ CTableRow *CTableModel::RowInsertNewTo(const uint index_to) { //--- Создаём новую пустую строку и добавляем её в конец списка строк CTableRow *row=this.CreateNewEmptyRow(); if(row==NULL) return NULL; //--- Создаём ячейки по количеству ячеек первой строки for(uint i=0;i<this.CellsInRow(0);i++) row.CreateNewCell(0.0); row.ClearData(); //--- Смещаем строку на позицию index_to this.RowMoveTo(this.m_list_rows.IndexOf(row),index_to); //--- Успешно - возвращаем указатель на созданный объект return row; }
Às vezes é necessário inserir uma nova linha não ao final da lista de linhas, mas entre as já existentes. Esse método primeiro cria uma nova linha no final da lista, preenche essa linha com células, limpa as células e depois move a linha para a posição desejada.
O método que define o valor em uma célula indicada:
//+------------------------------------------------------------------+ //| Устанавливает значение в указанную ячейку | //+------------------------------------------------------------------+ template<typename T> void CTableModel::CellSetValue(const uint row,const uint col,const T value) { //--- Получаем ячейку по индексам строки и столбца CTableCell *cell=this.GetCell(row,col); if(cell==NULL) return; //--- Получаем корректный тип устанавливаемых данных (double, long, datetime, color, string) ENUM_DATATYPE type=this.GetCorrectDatatype(typename(T)); //--- В зависимости от типа данных вызываем соответствующий типу данных //--- метод ячейки для установки значения, явно указывая требуемый тип switch(type) { case TYPE_DOUBLE : cell.SetValue((double)value); break; case TYPE_LONG : cell.SetValue((long)value); break; case TYPE_DATETIME: cell.SetValue((datetime)value); break; case TYPE_COLOR : cell.SetValue((color)value); break; case TYPE_STRING : cell.SetValue((string)value); break; default : break; } }
Primeiro obtemos o ponteiro para a célula desejada pelas coordenadas da sua linha e coluna e, em seguida, definimos nela o valor. Qualquer que seja o valor passado ao método para ser definido na célula, o método selecionará apenas o tipo correto para a definição — double, long, datetime, color ou string.
O método que define a precisãoda exibição dos dados na célula indicada:
//+------------------------------------------------------------------+ //| Устанавливает точность отображения данных в указанную ячейку | //+------------------------------------------------------------------+ void CTableModel::CellSetDigits(const uint row,const uint col,const int digits) { //--- Получаем ячейку по индексам строки и столбца и //--- вызываем её соответствующий метод для установки значения CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.SetDigits(digits); }
O método é relevante apenas para células que armazenam valores do tipo real. Ele serve para indicar a quantidade de casas decimais exibidas no valor mostrado pela célula.
O método que define os sinalizadores de exibição de tempo na célula indicada:
//+------------------------------------------------------------------+ //| Устанавливает флаги отображения времени в указанную ячейку | //+------------------------------------------------------------------+ void CTableModel::CellSetTimeFlags(const uint row,const uint col,const uint flags) { //--- Получаем ячейку по индексам строки и столбца и //--- вызываем её соответствующий метод для установки значения CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.SetDatetimeFlags(flags); }
Relevante para células que exibem valores do tipo datetime. Define o formato de exibição da hora pela célula (um dos TIME_DATE|TIME_MINUTES|TIME_SECONDS, ou uma combinação deles).
TIME_DATE obtém o resultado no formato " yyyy.mm.dd ",
TIME_MINUTES obtém o resultado no formato " hh:mi ",
TIME_SECONDS obtém o resultado no formato " hh:mi:ss ".
O método que define o sinalizadorde exibição dos nomes das cores na célula indicada:
//+------------------------------------------------------------------+ //| Устанавливает флаг отображения имён цветов в указанную ячейку | //+------------------------------------------------------------------+ void CTableModel::CellSetColorNamesFlag(const uint row,const uint col,const bool flag) { //--- Получаем ячейку по индексам строки и столбца и //--- вызываем её соответствующий метод для установки значения CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.SetColorNameFlag(flag); }
Relevante apenas para células que exibem valores do tipo color. Indica a necessidade de exibir os nomes das cores, caso a cor armazenada na célula esteja presente na tabela de cores.
O método que atribui um objeto à célula:
//+------------------------------------------------------------------+ //| Назначает объект в ячейку | //+------------------------------------------------------------------+ void CTableModel::CellAssignObject(const uint row,const uint col,CObject *object) { //--- Получаем ячейку по индексам строки и столбца и //--- вызываем её соответствующий метод для установки значения CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.AssignObject(object); }
O método que cancela a atribuição do objeto na célula:
//+------------------------------------------------------------------+ //| Отменяет назначение объекта в ячейке | //+------------------------------------------------------------------+ void CTableModel::CellUnassignObject(const uint row,const uint col) { //--- Получаем ячейку по индексам строки и столбца и //--- вызываем её соответствующий метод для установки значения CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.UnassignObject(); }
Os dois métodos apresentados acima permitem atribuir algum objeto à célula ou remover essa atribuição. O objeto deve ser descendente da classe CObject. No contexto dos artigos sobre tabelas, o objeto pode ser, por exemplo, algum dos objetos conhecidos da enumeração ENUM_OBJECT_TYPE. No momento, na lista existem apenas os objetos da célula, da linha e do modelo de tabela. Atribuí-los a uma célula não faz sentido. Mas a enumeração será ampliada ao longo da escrita dos artigos sobre o componente View, onde serão criados elementos de controle. Estes sim serão apropriados para serem atribuídos a uma célula, como, por exemplo, um elemento de controle "lista suspensa".
O método que exclui a célula indicada:
//+------------------------------------------------------------------+ //| Удаляет ячейку | //+------------------------------------------------------------------+ bool CTableModel::CellDelete(const uint row,const uint col) { //--- Получаем строку по индексу и возвращаем результат удаления ячейки из списка CTableRow *row_obj=this.GetRow(row); return(row_obj!=NULL ? row_obj.CellDelete(col) : false); }
O método obtém o objeto da linha pelo seu índice e chama o método dessa linha que exclui a célula indicada. Para uma única célula em uma única linha, esse método ainda não faz sentido, pois reduziria a quantidade de células apenas nessa linha da tabela. Isso causaria uma dessincronização das células com as linhas vizinhas. Ainda não existe tratamento para uma remoção desse tipo, na qual seria necessário "expandir" a célula vizinha à célula removida até o tamanho de duas células, para não violar a estrutura da tabela. Mas esse método é usado como parte do método de remoção de uma coluna inteira da tabela, onde as células são removidas simultaneamente em todas as linhas, sem violar a integridade de toda a tabela.
O método para mover uma célula da tabela:
//+------------------------------------------------------------------+ //| Перемещает ячейку | //+------------------------------------------------------------------+ bool CTableModel::CellMoveTo(const uint row,const uint cell_index,const uint index_to) { //--- Получаем строку по индексу и возвращаем результат перемещения ячейки на новую позицию CTableRow *row_obj=this.GetRow(row); return(row_obj!=NULL ? row_obj.CellMoveTo(cell_index,index_to) : false); }
Obtemos o objeto da linha pelo seu índice e chamamos seu método de remoção da célula indicada.
O método que retorna a quantidade de células na linha especificada:
//+------------------------------------------------------------------+ //| Возвращает количество ячеек в указанной строке | //+------------------------------------------------------------------+ uint CTableModel::CellsInRow(const uint index) { CTableRow *row=this.GetRow(index); return(row!=NULL ? row.CellsTotal() : 0); }
Obtemos a linha pelo índice e retornamos a quantidade de células nela, chamando o método CellsTotal() da linha.
O método que retorna a quantidadede células na tabela:
//+------------------------------------------------------------------+ //| Возвращает количество ячеек в таблице | //+------------------------------------------------------------------+ uint CTableModel::CellsTotal(void) { //--- подсчёт ячеек в цикле по строкам (медленно при большом количестве строк) uint res=0, total=this.RowsTotal(); for(int i=0; i<(int)total; i++) { CTableRow *row=this.GetRow(i); res+=(row!=NULL ? row.CellsTotal() : 0); } return res; }
O método percorre todas as linhas da tabela e soma a quantidade de células de cada linha no resultado final, que é retornado. Em uma tabela com grande quantidade de linhas, essa contagem pode ser lenta. Depois que forem criados todos os métodos que influenciam a quantidade de células da tabela, faremos esse cálculo apenas no momento em que sua quantidade for alterada.
O método que retorna a célula indicadada tabela:
//+------------------------------------------------------------------+ //| Возвращает указанную ячейку таблицы | //+------------------------------------------------------------------+ CTableCell *CTableModel::GetCell(const uint row,const uint col) { //--- Получаем строку по индексу row и возвращаем по индексу col ячейку строки CTableRow *row_obj=this.GetRow(row); return(row_obj!=NULL ? row_obj.GetCell(col) : NULL); }
Obtemos a linha pelo índice row e retornamos o ponteiro para o objeto da célula pelo índice col usando o método GetCell() da linha.
O método que retorna a descriçãode uma célula:
//+------------------------------------------------------------------+ //| Возвращает описание ячейки | //+------------------------------------------------------------------+ string CTableModel::CellDescription(const uint row,const uint col) { CTableCell *cell=this.GetCell(row,col); return(cell!=NULL ? cell.Description() : ""); }
Obtemos a linha pelo índice, obtemos a célula a partir da linha e retornamos a sua descrição.
O método que imprime no diário a descrição de uma célula:
//+------------------------------------------------------------------+ //| Выводит в журнал описание ячейки | //+------------------------------------------------------------------+ void CTableModel::CellPrint(const uint row,const uint col) { //--- Получаем ячейку по индексу строки и колонки и возвращаем её описание CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.Print(); }
Obtemos o ponteiro para a célula pelos índices da linha e da coluna e, usando o método Print() do objeto célula, imprimimos no diário a sua descrição.
O método que exclui a linha indicada:
//+------------------------------------------------------------------+ //| Удаляет строку | //+------------------------------------------------------------------+ bool CTableModel::RowDelete(const uint index) { //--- Удаляем строку из списка по индексу if(!this.m_list_rows.Delete(index)) return false; //--- После удаления строки необходимо обновить все индексы всех ячеек таблицы this.CellsPositionUpdate(); return true; }
Com o método Delete() da classe CList, excluímos pelo índice o objeto linha da lista. Após excluir a linha, os índices das linhas restantes e das células em cada uma delas deixam de ser corretos, e é necessário corrigi-los por meio do método CellsPositionUpdate().
O método que move uma linha para a posição indicada:
//+------------------------------------------------------------------+ //| Перемещает строку на указанную позицию | //+------------------------------------------------------------------+ bool CTableModel::RowMoveTo(const uint row_index,const uint index_to) { //--- Получаем строку по индексу, делая её текущей CTableRow *row=this.GetRow(row_index); //--- Перемещаем текущую строку на указанную позицию в списке if(row==NULL || !this.m_list_rows.MoveToIndex(index_to)) return false; //--- После перемещения строки необходимо обновить все индексы всех ячеек таблицы this.CellsPositionUpdate(); return true; }
Na classe CList, muitos métodos trabalham com o objeto atual da lista. Obtemos o ponteiro para a linha desejada, tornando-a a linha atual, e a movemos para a posição necessária usando o método MoveToIndex() da classe CList. Depois de mover a linha para a nova posição, é necessário atualizar os índices das demais linhas, o que fazemos com o método CellsPositionUpdate().
O método que define as posições de linha e coluna para todas as células:
//+------------------------------------------------------------------+ //| Устанавливает позиции строки и колонки всем ячейкам | //+------------------------------------------------------------------+ void CTableModel::CellsPositionUpdate(void) { //--- В цикле по списку строк for(int i=0;i<this.m_list_rows.Total();i++) { //--- получаем очередную строку CTableRow *row=this.GetRow(i); if(row==NULL) continue; //--- устанавливаем строке индекс, найденный методом IndexOf() списка row.SetIndex(this.m_list_rows.IndexOf(row)); //--- Обновляем индексы позиций ячеек строки row.CellsPositionUpdate(); } }
Percorremos a lista de todas as linhas da tabela, selecionamos cada linha seguinte e definimos para ela o índice correto, obtido pelo método IndexOf() da classe CList. Em seguida, chamamos o método CellsPositionUpdate() da linha, que define o índice correto para cada célula da linha.
O método que limpa os dados de todas as células da linha:
//+------------------------------------------------------------------+ //| Очищает строку (только данные в ячйках) | //+------------------------------------------------------------------+ void CTableModel::RowResetData(const uint index) { //--- Получаем строку из списка и очищаем данные ячеек строки методом ClearData() CTableRow *row=this.GetRow(index); if(row!=NULL) row.ClearData(); }
Cada célula da linha é redefinida para um valor "vazio". Por enquanto, para simplificar, o valor vazio das células numéricas é zero, mas isso será modificado posteriormente, pois zero também é um valor que precisa ser exibido na célula. Já a redefinição do valor pressupõe a exibição de um campo vazio.
O método que limpa os dadosde todas as células da tabela:
//+------------------------------------------------------------------+ //| Очищает таблицу (данные всех ячеек) | //+------------------------------------------------------------------+ void CTableModel::ClearData(void) { //--- В цикле по всем строкам таблицы очищаем данные каждой строки for(uint i=0;i<this.RowsTotal();i++) this.RowResetData(i); }
Percorremos todas as linhas da tabela e, para cada uma delas, chamamos o método RowResetData(), analisado anteriormente.
O método que retorna a descrição da linha:
//+------------------------------------------------------------------+ //| Возвращает описание строки | //+------------------------------------------------------------------+ string CTableModel::RowDescription(const uint index) { //--- Получаем строку по индексу и возвращаем её описание CTableRow *row=this.GetRow(index); return(row!=NULL ? row.Description() : ""); }
Obtemos o ponteiro para a linha pelo índice e retornamos a sua descrição.
O método que imprime no diário a descrição da linha:
//+------------------------------------------------------------------+ //| Выводит в журнал описание строки | //+------------------------------------------------------------------+ void CTableModel::RowPrint(const uint index,const bool detail) { CTableRow *row=this.GetRow(index); if(row!=NULL) row.Print(detail); }
Obtemos o ponteiro para a linha e chamamos o método Print() do objeto obtido.
O método que exclui uma coluna da tabela:
//+------------------------------------------------------------------+ //| Удаляет столбец | //+------------------------------------------------------------------+ bool CTableModel::ColumnDelete(const uint index) { bool res=true; for(uint i=0;i<this.RowsTotal();i++) { CTableRow *row=this.GetRow(i); if(row!=NULL) res &=row.CellDelete(index); } return res; }
Em um loop sobre todas as linhas da tabela, obtemos cada linha e excluímos nela a célula desejada pelo índice da coluna. Assim, são excluídas todas as células da tabela que possuem o mesmo índice de coluna.
O método que move uma coluna da tabela:
//+------------------------------------------------------------------+ //| Перемещает столбец | //+------------------------------------------------------------------+ bool CTableModel::ColumnMoveTo(const uint col_index,const uint index_to) { bool res=true; for(uint i=0;i<this.RowsTotal();i++) { CTableRow *row=this.GetRow(i); if(row!=NULL) res &=row.CellMoveTo(col_index,index_to); } return res; }
Em um loop sobre todas as linhas da tabela, obtemos cada linha e movemos a célula desejada para a nova posição. Assim, são movidas todas as células da tabela que possuem o mesmo índice de coluna.
O método que limpa os dados das células de uma coluna:
//+------------------------------------------------------------------+ //| Очищает данные столбца | //+------------------------------------------------------------------+ void CTableModel::ColumnResetData(const uint index) { //--- В цикле по всем строкам таблицы for(uint i=0;i<this.RowsTotal();i++) { //--- получаем из каждой строки ячейку с индексом столбца и очищаем её CTableCell *cell=this.GetCell(i, index); if(cell!=NULL) cell.ClearData(); } }
Em um loop sobre todas as linhas da tabela, obtemos cada linha e limpamos os dados da célula desejada. Assim, são limpas todas as células da tabela que possuem o mesmo índice de coluna.
O método que retorna a descrição do objeto:
//+------------------------------------------------------------------+ //| Возвращает описание объекта | //+------------------------------------------------------------------+ string CTableModel::Description(void) { return(::StringFormat("%s: Rows %u, Cells in row %u, Cells Total %u", TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.RowsTotal(),this.CellsInRow(0),this.CellsTotal())); }
É criada e retornada uma string a partir de alguns parâmetros do modelo de tabela nesse formato:
Table Model: Rows 4, Cells in row 4, Cells Total 16:
O método que imprime no diário a descrição do objeto:
//+------------------------------------------------------------------+ //| Выводит в журнал описание объекта | //+------------------------------------------------------------------+ void CTableModel::Print(const bool detail) { //--- Выводим в журнал заголовок ::Print(this.Description()+(detail ? ":" : "")); //--- Если детализированное описание, if(detail) { //--- В цикле по всем строкам таблицы for(uint i=0; i<this.RowsTotal(); i++) { //--- получаем очередную строку и выводим в журнал её детализированное описание CTableRow *row=this.GetRow(i); if(row!=NULL) row.Print(true,false); } } }
Primeiro é impresso o cabeçalho na forma da descrição do modelo e, depois, se o sinalizador de saída detalhada estiver definido, são impressas em loop as descrições detalhadas de todas as linhas do modelo de tabela.
O método que imprime no diárioa descrição do objeto em formato tabular:
//+------------------------------------------------------------------+ //| Выводит в журнал описание объекта в табличном виде | //+------------------------------------------------------------------+ void CTableModel::PrintTable(const int cell_width=10) { //--- Получаем указатель на первую строку (индекс 0) CTableRow *row=this.GetRow(0); if(row==NULL) return; //--- По количеству ячеек первой строки таблицы создаём строку заголовка таблицы uint total=row.CellsTotal(); string head=" n/n"; string res=::StringFormat("|%*s |",cell_width,head); for(uint i=0;i<total;i++) { if(this.GetCell(0, i)==NULL) continue; string cell_idx=" Column "+(string)i; res+=::StringFormat("%*s |",cell_width,cell_idx); } //--- Выводим строку заголовка в журнал ::Print(res); //--- Пройдём в цикле по всем строкам таблицы и распечатаем их в табличном виде for(uint i=0;i<this.RowsTotal();i++) { CTableRow *row=this.GetRow(i); if(row!=NULL) row.Print(true,true,cell_width); } }
Primeiro, pelo número de células da primeira linha da tabela, criamos e imprimimos no diário o cabeçalho da tabela com os nomes das colunas. Depois, percorremos em um loop todas as linhas da tabela e imprimimos cada uma delas em formato tabular.
O método que destrói o modelo de tabela:
//+------------------------------------------------------------------+ //| Уничтожает модель | //+------------------------------------------------------------------+ void CTableModel::Destroy(void) { //--- Очищаем список строк m_list_rows.Clear(); }
A lista de linhas da tabela é simplesmente limpo com destruição de todos os objetos usando o método Clear() da classe CList.
O método para salvar o modelo de tabela em arquivo:
//+------------------------------------------------------------------+ //| Сохранение в файл | //+------------------------------------------------------------------+ bool CTableModel::Save(const int file_handle) { //--- Проверяем хэндл if(file_handle==INVALID_HANDLE) return(false); //--- Сохраняем маркер начала данных - 0xFFFFFFFFFFFFFFFF if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long)) return(false); //--- Сохраняем тип объекта if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE) return(false); //--- Сохраняем список строк if(!this.m_list_rows.Save(file_handle)) return(false); //--- Успешно return true; }
Depois de salvar o marcador de início dos dados e o tipo da lista, salvamos no arquivo a lista de linhas por meio do método Save() da classe CList.
O método para carregar o modelo de tabela a partir de um arquivo:
//+------------------------------------------------------------------+ //| Загрузка из файла | //+------------------------------------------------------------------+ bool CTableModel::Load(const int file_handle) { //--- Проверяем хэндл if(file_handle==INVALID_HANDLE) return(false); //--- Загружаем и проверяем маркер начала данных - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return(false); //--- Загружаем тип объекта if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type()) return(false); //--- Загружаем список строк if(!this.m_list_rows.Load(file_handle)) return(false); //--- Успешно return true; }
Depois de carregar e verificar o marcador de início dos dados e o tipo da lista, carregamos a lista de linhas do arquivo usando o método Load() da classe CListObj, analisado no início do artigo.
as classes para criar o modelo de tabela estão prontos. Agora escreveremos um script para testar o funcionamento do modelo.
Vamos testar o resultado
Continuamos escrevendo o código no mesmo arquivo. Escreveremos um script no qual criaremos um array bidimensional com dimensão 4x4 (4 linhas com 4 células em cada) com tipo, por exemplo, long. Em seguida, abrimos um arquivo para gravar nele os dados do modelo de tabela e para carregar dados da tabela a partir do arquivo. Criamos o modelo de tabela e verificamos o funcionamento de alguns de seus métodos.
Cada vez que a tabela for alterada, imprimiremos no diário o resultado obtido usando a função TableModelPrint(), onde é feita a escolha de como imprimir o modelo de tabela. Com o valor do macro PRINT_AS_TABLE igual a true, a impressão no diário é realizada pelo método PrintTable() da classe CTableModel; com o valor false , pelo método Print() da mesma classe.
No script, criaremos o modelo de tabela, a imprimiremos em formato tabular e salvaremos o modelo em arquivo. Depois adicionaremos linhas, removeremos colunas, mudaremos a permissão de edição...
Depois, novamente carregaremos do arquivo a versão original inicial da tabela e imprimiremos o resultado.
#define PRINT_AS_TABLE true // Распечатывать модель как таблицу //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Объявляем и заполняем массив с размерностью 4x4 //--- Тип массива может быть double, long, datetime, color, string long array[4][4]={{ 1, 2, 3, 4}, { 5, 6, 7, 8}, { 9, 10, 11, 12}, {13, 14, 15, 16}}; //--- Создаём модель таблицы из вышесозданного long-массива array 4x4 CTableModel *tm=new CTableModel(array); //--- Если модель не создана - уходим if(tm==NULL) return; //--- Распечатаем модель в табличном виде Print("The table model has been successfully created:"); tm.PrintTable(); //--- Удалим объект модели таблицы delete tm; }
Como resultado, obteremos no diário algo assim:
The table model has been successfully created: | n/n | Column 0 | Column 1 | Column 2 | Column 3 | | Row 0 | 1 | 2 | 3 | 4 | | Row 1 | 5 | 6 | 7 | 8 | | Row 2 | 9 | 10 | 11 | 12 | | Row 3 | 13 | 14 | 15 | 16 |
Para verificar o funcionamento com o modelo de tabela, adição, remoção e movimentação de linhas e colunas, e o trabalho com arquivo, completaremos o script:
#define PRINT_AS_TABLE true // Распечатывать модель как таблицу //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Объявляем и заполняем массив с размерностью 4x4 //--- Тип массива может быть double, long, datetime, color, string long array[4][4]={{ 1, 2, 3, 4}, { 5, 6, 7, 8}, { 9, 10, 11, 12}, {13, 14, 15, 16}}; //--- Создаём модель таблицы из вышесозданного long-массива array 4x4 CTableModel *tm=new CTableModel(array); //--- Если модель не создана - уходим if(tm==NULL) return; //--- Распечатаем модель в табличном виде Print("The table model has been successfully created:"); tm.PrintTable(); //--- Проверим работу с файлами и функционал модели таблицы //--- Открываем файл для записи в него данных модели таблицы int handle=FileOpen(MQLInfoString(MQL_PROGRAM_NAME)+".bin",FILE_READ|FILE_WRITE|FILE_BIN|FILE_COMMON); if(handle==INVALID_HANDLE) return; //--- Сохраним в файл оригинальную созданную таблицу if(tm.Save(handle)) Print("\nThe table model has been successfully saved to file."); //--- Теперь вставим в таблицу новую строку в позицию 2 //--- Получим последнюю ячейку созданной строки и сделаем её нередактируемой //--- Распечатаем в журнале изменённую модель таблицы if(tm.RowInsertNewTo(2)) { Print("\nInsert a new row at position 2 and set cell 3 to non-editable"); CTableCell *cell=tm.GetCell(2,3); if(cell!=NULL) cell.SetEditable(false); TableModelPrint(tm); } //--- Теперь удалим столбец таблицы с индексом 1 и //--- распечатаем в журнале полученную модель таблицы if(tm.ColumnDelete(1)) { Print("\nRemove column from position 1"); TableModelPrint(tm); } //--- При сохранении данных таблицы файловый указатель был смещён на последние записанные данные //--- Поставим указатель в начало файла, загрузим ранее сохранённую оригинальную таблицу и распечатаем её if(FileSeek(handle,0,SEEK_SET) && tm.Load(handle)) { Print("\nLoad the original table view from the file:"); TableModelPrint(tm); } //--- Закроем открытый файл и удалим объект модели таблицы FileClose(handle); delete tm; } //+------------------------------------------------------------------+ //| Распечатывает модель таблицы | //+------------------------------------------------------------------+ void TableModelPrint(CTableModel *tm) { if(PRINT_AS_TABLE) tm.PrintTable(); // Распечатать модель как таблицу else tm.Print(true); // Распечатать детализированные данные таблицы }
Obtemos então no diário o seguinte resultado:
The table model has been successfully created: | n/n | Column 0 | Column 1 | Column 2 | Column 3 | | Row 0 | 1 | 2 | 3 | 4 | | Row 1 | 5 | 6 | 7 | 8 | | Row 2 | 9 | 10 | 11 | 12 | | Row 3 | 13 | 14 | 15 | 16 | The table model has been successfully saved to file. Insert a new row at position 2 and set cell 3 to non-editable | n/n | Column 0 | Column 1 | Column 2 | Column 3 | | Row 0 | 1 | 2 | 3 | 4 | | Row 1 | 5 | 6 | 7 | 8 | | Row 2 | 0.00 | 0.00 | 0.00 | 0.00 | | Row 3 | 9 | 10 | 11 | 12 | | Row 4 | 13 | 14 | 15 | 16 | Remove column from position 1 | n/n | Column 0 | Column 1 | Column 2 | | Row 0 | 1 | 3 | 4 | | Row 1 | 5 | 7 | 8 | | Row 2 | 0.00 | 0.00 | 0.00 | | Row 3 | 9 | 11 | 12 | | Row 4 | 13 | 15 | 16 | Load the original table view from the file: | n/n | Column 0 | Column 1 | Column 2 | Column 3 | | Row 0 | 1 | 2 | 3 | 4 | | Row 1 | 5 | 6 | 7 | 8 | | Row 2 | 9 | 10 | 11 | 12 | | Row 3 | 13 | 14 | 15 | 16 |
Se for necessário imprimir no diário os dados do modelo de tabela não em formato tabular, basta definir o valor false no macro PRINT_AS_TABLE:
#define PRINT_AS_TABLE false // Распечатывать модель как таблицу
Nesse caso, obtemos no diário a seguinte saída:
The table model has been successfully created: | n/n | Column 0 | Column 1 | Column 2 | Column 3 | | Row 0 | 1 | 2 | 3 | 4 | | Row 1 | 5 | 6 | 7 | 8 | | Row 2 | 9 | 10 | 11 | 12 | | Row 3 | 13 | 14 | 15 | 16 | The table model has been successfully saved to file. Insert a new row at position 2 and set cell 3 to non-editable Table Model: Rows 5, Cells in row 4, Cells Total 20: Table Row: Position 0, Cells total: 4: Table Cell: Row 0, Col 0, Editable <long>Value: 1 Table Cell: Row 0, Col 1, Editable <long>Value: 2 Table Cell: Row 0, Col 2, Editable <long>Value: 3 Table Cell: Row 0, Col 3, Editable <long>Value: 4 Table Row: Position 1, Cells total: 4: Table Cell: Row 1, Col 0, Editable <long>Value: 5 Table Cell: Row 1, Col 1, Editable <long>Value: 6 Table Cell: Row 1, Col 2, Editable <long>Value: 7 Table Cell: Row 1, Col 3, Editable <long>Value: 8 Table Row: Position 2, Cells total: 4: Table Cell: Row 2, Col 0, Editable <double>Value: 0.00 Table Cell: Row 2, Col 1, Editable <double>Value: 0.00 Table Cell: Row 2, Col 2, Editable <double>Value: 0.00 Table Cell: Row 2, Col 3, Uneditable <double>Value: 0.00 Table Row: Position 3, Cells total: 4: Table Cell: Row 3, Col 0, Editable <long>Value: 9 Table Cell: Row 3, Col 1, Editable <long>Value: 10 Table Cell: Row 3, Col 2, Editable <long>Value: 11 Table Cell: Row 3, Col 3, Editable <long>Value: 12 Table Row: Position 4, Cells total: 4: Table Cell: Row 4, Col 0, Editable <long>Value: 13 Table Cell: Row 4, Col 1, Editable <long>Value: 14 Table Cell: Row 4, Col 2, Editable <long>Value: 15 Table Cell: Row 4, Col 3, Editable <long>Value: 16 Remove column from position 1 Table Model: Rows 5, Cells in row 3, Cells Total 15: Table Row: Position 0, Cells total: 3: Table Cell: Row 0, Col 0, Editable <long>Value: 1 Table Cell: Row 0, Col 1, Editable <long>Value: 3 Table Cell: Row 0, Col 2, Editable <long>Value: 4 Table Row: Position 1, Cells total: 3: Table Cell: Row 1, Col 0, Editable <long>Value: 5 Table Cell: Row 1, Col 1, Editable <long>Value: 7 Table Cell: Row 1, Col 2, Editable <long>Value: 8 Table Row: Position 2, Cells total: 3: Table Cell: Row 2, Col 0, Editable <double>Value: 0.00 Table Cell: Row 2, Col 1, Editable <double>Value: 0.00 Table Cell: Row 2, Col 2, Uneditable <double>Value: 0.00 Table Row: Position 3, Cells total: 3: Table Cell: Row 3, Col 0, Editable <long>Value: 9 Table Cell: Row 3, Col 1, Editable <long>Value: 11 Table Cell: Row 3, Col 2, Editable <long>Value: 12 Table Row: Position 4, Cells total: 3: Table Cell: Row 4, Col 0, Editable <long>Value: 13 Table Cell: Row 4, Col 1, Editable <long>Value: 15 Table Cell: Row 4, Col 2, Editable <long>Value: 16 Load the original table view from the file: Table Model: Rows 4, Cells in row 4, Cells Total 16: Table Row: Position 0, Cells total: 4: Table Cell: Row 0, Col 0, Editable <long>Value: 1 Table Cell: Row 0, Col 1, Editable <long>Value: 2 Table Cell: Row 0, Col 2, Editable <long>Value: 3 Table Cell: Row 0, Col 3, Editable <long>Value: 4 Table Row: Position 1, Cells total: 4: Table Cell: Row 1, Col 0, Editable <long>Value: 5 Table Cell: Row 1, Col 1, Editable <long>Value: 6 Table Cell: Row 1, Col 2, Editable <long>Value: 7 Table Cell: Row 1, Col 3, Editable <long>Value: 8 Table Row: Position 2, Cells total: 4: Table Cell: Row 2, Col 0, Editable <long>Value: 9 Table Cell: Row 2, Col 1, Editable <long>Value: 10 Table Cell: Row 2, Col 2, Editable <long>Value: 11 Table Cell: Row 2, Col 3, Editable <long>Value: 12 Table Row: Position 3, Cells total: 4: Table Cell: Row 3, Col 0, Editable <long>Value: 13 Table Cell: Row 3, Col 1, Editable <long>Value: 14 Table Cell: Row 3, Col 2, Editable <long>Value: 15 Table Cell: Row 3, Col 3, Editable <long>Value: 16
Essa saída fornece mais informações de depuração. Por exemplo, aqui vemos que a definição do sinalizador de proibição de edição em uma célula é refletida nos logs do programa.
Toda a funcionalidade declarada funciona como esperado; linhas são adicionadas, movidas, colunas são removidas, podemos alterar as propriedades das células e trabalhar com arquivos.
Considerações finais
Bem, esta é a primeira versão de um modelo de tabela simples, que permite criar uma tabela a partir de um array bidimensional de dados. Mais adiante, desenvolveremos outros modelos de tabela especializados, o componente View da tabela e o pleno trabalho com dados tabulares como um dos elementos do controle da interface gráfica do usuário.
Ao artigo está anexado o arquivo do script criado hoje com todas as classes incluídas. Você pode carregá-lo para estudo independente.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/17653
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.
Como construir e otimizar um sistema de trading baseado em volume (Chaikin Money Flow - CMF)
Automatizando Estratégias de Trading em MQL5 (Parte 2): O Sistema Kumo Breakout com Ichimoku e Awesome Oscillator
Integre seu próprio LLM ao EA (Parte 5): Desenvolva e Teste Estratégia de Trading com LLMs (III) – Adapter-Tuning
- 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
Quando uma classe de SomeObject é carregada de um arquivo por meio da chamada do método Load() desse mesmo SomeObject, ele verifica "eu realmente me li do arquivo?" (é isso que você está perguntando). Se não, isso significa que algo deu errado e, portanto, não há motivo para continuar o carregamento.
O que tenho aqui é uma LISTA (CListObj) lendo um tipo de objeto de um arquivo. A lista não sabe o que está lá (qual objeto) no arquivo. Mas ela precisa conhecer esse tipo de objeto para criá-lo em seu método CreateElement(). É por isso que ela não verifica o tipo do objeto carregado do arquivo. Afinal de contas, haverá uma comparação com Type(), que nesse método retorna o tipo de uma lista, não de um objeto.
Obrigado, já resolvi, estou entendendo.
Eu li e depois reli novamente.
é algo diferente de um "modelo" em MVC. Algum ListStorage, por exemplo
Gostaria de saber. É possível obter algum análogo dos dataframes do Python e do R dessa forma? Essas tabelas são aquelas em que colunas diferentes podem conter dados de tipos diferentes (de um conjunto limitado de tipos, mas incluindo string).
É possível. Se estivermos falando de colunas diferentes de uma tabela, então, na implementação descrita, cada célula da tabela pode ter um tipo de dados diferente.