English Русский Português 한국어 Français Italiano
preview
MQL5中表格模型的实现:应用MVC概念

MQL5中表格模型的实现:应用MVC概念

MetaTrader 5示例 |
40 9
Artyom Trishkin
Artyom Trishkin

内容


概述

对于编程而言,应用程序架构对于确保可靠性、可扩展性和易于维护起着关键作用。有助于实现这些目标的方法之一,是采用名为MVC(模型-视图-控制器)的架构模式。

MVC概念允许将应用程序划分为三个相互关联的组件:模型(负责数据和逻辑管理)、视图(负责数据展示)和控制器(负责处理用户操作)。这种分离简化了代码的开发、测试和维护,使其更加结构化和灵活。

在本文中,我们将探讨如何运用MVC原则,在MQL5语言中实现一个表格模型。表格是存储、处理和展示数据的重要工具,合理组织表格能极大地简化信息处理工作。我们将创建用于操作表格的类:包括表格单元格类、表格行类以及表格模型类。为了在表格行内存储单元格,以及在表格模型内存储行,我们将使用MQL5标准库中的链表类,这些类能够高效地存储和使用数据。


关于MVC概念的一点介绍:它是什么,我们为何需要它?

想象一下应用程序就像一场戏剧演出。有一个剧本,描述了应该发生什么(这就是模型)。有舞台,即观众所看到的(这就是视图)。最后,还有导演,他管理整个过程并连接其他元素(这就是控制器)。这就是架构模式MVC——模型-视图-控制器的工作方式。

这一概念有助于在应用程序内部划分职责。模型负责数据和逻辑,视图负责展示和外观,控制器负责处理用户操作。这种划分使代码更加清晰、灵活,更便于团队协作。

假设您正在创建一个表格。模型知道它包含哪些行和单元格,并知道如何更改它们。视图在屏幕上绘制表格。当用户点击“添加行”时,控制器会做出反应,将任务交给模型,然后告诉视图进行更新。

当应用程序变得更加复杂时,MVC尤其有用:需要添加新功能、界面在变化,而且有多个开发人员参与工作。有了清晰的架构,进行更改、单独测试组件以及重用代码都变得更加容易。

但这种做法也有一些缺点。对于非常简单的项目,MVC可能显得多余——甚至不得不将本可以放在几个函数中的内容也进行分离。然而,对于可扩展的、严肃的应用程序,这种结构很快就会带来回报。

总结:

MVC是一种强大的架构模板,有助于组织代码,使其更易于理解、可测试和可扩展。对于需要分离数据逻辑、用户界面和管理的复杂应用程序,MVC尤其有用。对于小型项目,使用它则显得冗余。

模型-视图-控制器范式非常适合我们的任务。表格将由独立对象构成:

  • 表格单元格。
    一个对象,存储实数、整数或字符串类型之一的值,并配备有管理该值、设置值和获取值的工具;
  • 表格行。
    一个对象,存储表格单元格对象列表,并配备有管理单元格、其位置、添加和删除的工具;
  • 表格模型。
    一个对象,存储表格行对象列表,并配备有管理表格行和列、其位置、添加和删除的工具,还具有访问行和单元格控制的权限。

下图示意性地展示了4x4表格模型的结构:

图1. 4x4表格模型

现在,让我们从理论转向实践。


编写用于构建表格模型的类

我们将使用MQL5标准库来创建所有对象。

每个对象都将成为该库基类的派生类。这将使您能够将这些对象存储在对象列表中。

我们将把所有类都写在一个测试脚本文件中,这样所有内容都集中在一个文件中,便于查看和快速访问。未来,我们会将所编写的类分配到各自独立的包含文件中。


1. 以链表作为存储表格数据的基础

CList链表非常适合存储表格数据。与类似的CArrayObj列表不同,它实现了访问位于当前对象左侧和右侧相邻列表对象的方法。这将便于在行内移动单元格,或在表格中移动行,以及添加和删除它们。同时,列表本身会负责正确索引列表中移动、添加或删除的对象。

但这里有一个细节需要注意。如果您查阅将列表加载和保存到文件的方法,就会发现从文件加载时,列表类必须在虚拟的CreateElement()方法中创建一个新对象。

此类中的该方法只是简单地返回NULL

   //--- method of creating an element of the list
   virtual CObject  *CreateElement(void) { return(NULL); }

这意味着为了使用链表,并且假设需要文件操作,我们就必须从CList类继承并实现这个方法。

如果您查看将标准库对象保存到文件的方法,可以看到保存对象属性的以下算法:

  1. 将数据起始标记(-1)写入文件,
  2. 将对象类型写入文件,
  3. 将所有对象属性逐一写入文件。

第一点和第二点是所有标准库对象所实现的保存/加载方法所共有的。因此,按照同样的逻辑,我们希望知道列表中保存的对象类型,以便在从文件读取时,能够在从CList继承的列表类的虚拟CreateElement()方法中创建该类型的对象。

此外,所有可以加载到列表中的对象——必须在实现列表类之前声明或创建它们的类。这样一来,列表就会“知道”哪些对象是“候选对象”,以及需要创建哪些对象。

在终端目录\MQL5\Scripts\中,创建一个新文件夹TableModel\,并在其中创建一个新的测试脚本文件TableModelTest.mq5。

连接链表文件声明未来的表格模型类

//+------------------------------------------------------------------+
//|                                               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;                  // Класс модели таблицы

此处对未来类的前置声明是必要的,这样从CList继承而来的链表类就能了解这些类的类型,以及了解它必须创建的对象类型。为此,我们将编写对象类型枚举辅助宏列表排序方式枚举

//+------------------------------------------------------------------+
//| Включаемые библиотеки                                            |
//+------------------------------------------------------------------+
#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;
  }
//+------------------------------------------------------------------+
//| Классы                                                           |
//+------------------------------------------------------------------+

返回对象类型描述的函数基于这样一个假设:所有对象类型常量的名称都以子字符串“OBJECT_TYPE_”开头。然后,您可以截取该子字符串之后的部分,将所得字符串中的所有字符转换为小写,将首字母转换为大写,并清除最终字符串左右两侧的所有空格和控制字符。

让我们编写自己的链表类。我们将在同一文件中继续编写后续代码:

//+------------------------------------------------------------------+
//| Классы                                                           |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Класс связанного списка объектов                                 |
//+------------------------------------------------------------------+
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);
  };

CListObj类是我们新的链表类,它继承自标准库的CList类。

该类中唯一的变量将用于记录所创建对象的类型。由于CreateElement()方法是虚拟的,并且必须与父类方法的签名完全相同,因此我们无法将所创建对象的类型传递给它。但是,我们可以将这个类型写入已声明的变量中,并从该变量中读取所创建对象的类型。

我们必须重新定义父类的两个虚拟方法:从文件上传的方法和创建新对象的方法。让我们来探讨一下。

从文件上传列表的方法如下:

//+------------------------------------------------------------------+
//| Загрузка списка из файла                                         |
//+------------------------------------------------------------------+
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;
  }

在此过程中,首先会检查列表的起始部分,包括其类型和大小(即列表中的元素数量);然后,根据元素数量循环,从文件中读取每个对象的数据起始标记及其类型。将得到的类型写入变量 m_element_type ,并调用创建新元素的方法。在该方法中,根据接收到的类型创建一个新元素,并将其写入node指针变量,该变量随后被添加到列表中。该方法的完整逻辑已在注释中详细说明。让我们来探究一下创建新列表项的方法。

创建列表项的方法如下:

//+------------------------------------------------------------------+
//| Метод создания элемента списка                                   |
//+------------------------------------------------------------------+
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;
     }
  }

这意味着在调用该方法之前,待创建对象的类型已写入m_element_type 变量中。根据项的类型,创建相应类型的新对象,并返回指向该对象的指针。未来开发新控件时,它们的类型将被写入ENUM_OBJECT_TYPE枚举中。同时,此处将添加新的情况以创建新类型的对象。基于标准CList的链表类现已准备就绪。现在,它可以存储所有已知类型的对象,将列表保存到文件,从文件上传列表并正确恢复它们。


2. 表格单元格类

表格单元格是表格中最简单的元素,用于存储某个值。单元格组成表示表格行的列表。每个列表代表表格的一行。在我们的表格中,单元格一次只能存储多种类型中的一个值——实数、整数或字符串值。

除简单值外,还可以为单元格分配ENUM_OBJECT_TYPE枚举中已知类型的一个对象。在这种情况下,单元格可以存储上述任一类型的值,外加一个指向对象的指针,该对象的类型写入一个特殊变量中。因此,未来可以指示视图组件在单元格中显示此类对象,以便使用控制器组件与其交互。

由于一个单元格可以存储几种不同类型的值,我们将使用联合体来写入、存储和返回它们。联合体是一种特殊类型的数据,它在同一内存区域中存储多个字段。联合体类似于结构体,但与结构体不同,联合体的不同项属于同一内存区域。而在结构体中,每个字段都分配有自己的内存区域。

让我们继续在已创建的文件中编写代码。让我们开始编写一个新类。在受保护的部分中,我们编写一个联合体声明变量

//+------------------------------------------------------------------+
//| Класс ячейки таблицы                                             |
//+------------------------------------------------------------------+
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:

在公有部分,编写访问受保护变量的方法、虚方法,以及针对单元格中存储的各类数据类型的类构造函数:

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) {}
  };

设置值的方法中,首先会设置单元格中存储的值的类型,然后检查单元格中编辑值功能的标识位。只有当设置该标识位时,新值才会被保存到单元格中:

//--- Устанавливает double-значение
   void              SetValue(const double value)
                       {
                        this.m_datatype=TYPE_DOUBLE;
                        if(this.m_editable)
                           this.m_datatype_value.SetValueD(value);
                       }

为何要如此设计?在创建新单元格时,其存储值的类型默认为实数类型。如果要更改值的类型,但同时单元格处于不可编辑状态,此时调用设置目标类型值的方法并传入任意值。存储值的类型会被修改,但单元格本身的值不会受到影响。

数据清理方法会将数值型值设置为0,字符串型值设置为空格:

//--- Очищает данные
   void              ClearData(void)
                       {
                        if(this.Datatype()==TYPE_STRING)
                           this.SetValue("");
                        else
                           this.SetValue(0.0);
                       }

后续我们会改进这一设计——清空单元格时不显示任何数据——为使单元格保持空白状态,我们将为单元格定义一个“空值”标记,清空单元格时,所有记录并显示的值都会被彻底清除。毕竟0本身也是一个有效值,而当前清空单元格时数字数据会用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);
   }

比较两个对象的方法如下:

//+------------------------------------------------------------------+
//| Сравнение двух объектов                                          |
//+------------------------------------------------------------------+
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
                                      );
     }
  }

该方法允许您根据三种比较标准之一(按列号、按行号、同时按行号和列号)比较两个对象的参数。

该方法对于能够按表格列的值对表格行进行排序是必要的。后续文章中将由控制器(Controller)处理此功能。

将单元格属性保存至文件的方法如下:

//+------------------------------------------------------------------+
//| Сохранение в файл                                                |
//+------------------------------------------------------------------+
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;
  }

在将起始数据标记和对象类型写入文件后,所有单元格属性将依次保存。联合体必须通过FileWriteStruct()以结构体形式进行保存。

从文件加载单元格属性的方法如下:

//+------------------------------------------------------------------+
//| Загрузка из файла                                                |
//+------------------------------------------------------------------+
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;
  }

在读取并校验数据起始标识符和对象类型后,所有对象属性将按照保存时的相同顺序依次加载。联合体使用FileReadStruct()进行读取。

返回对象描述信息的方法如下:

//+------------------------------------------------------------------+
//| Возвращает описание объекта                                      |
//+------------------------------------------------------------------+
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()));
  }

在此方法中,会从单元格的部分参数中构建一行信息并返回,例如对于双精度型,格式如下:

  表格单元格:行2,列2,不可编辑<双精度型>值:0.00

将对象描述信息输出到日志的方法如下:

//+------------------------------------------------------------------+
//| Выводит в журнал описание объекта                                |
//+------------------------------------------------------------------+
void CTableCell::Print(void)
  {
   ::Print(this.Description());
  }

在此方法中,对象描述信息会被直接打印到日志中。

//+------------------------------------------------------------------+
//| Класс строки таблицы                                             |
//+------------------------------------------------------------------+
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. 表格行类

表格行本质上是一个由单元格组成的链表。行类必须支持在链表中添加、删除单元格,以及将单元格重新排序至新位置。该类必须具备管理链表中单元格的最小必要方法集。

我们继续在同一个文件中编写代码。类参数中仅有一个可用变量——表格中的行索引。其余均为用于操作行属性及其单元格链表的方法。

下面让我们来探究类方法。

比较两行表格的方法如下:

//+------------------------------------------------------------------+
//| Сравнение двух объектов                                          |
//+------------------------------------------------------------------+
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);
  }

由于行只能通过其唯一参数(行索引)进行比较,因此在此处实现该比较逻辑。该方法将用于对表格行进行排序。

为创建存储不同类型数据的单元格而重载的方法如下:

//+------------------------------------------------------------------+
//| Создаёт новую 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;
  }

提供五种创建新单元格的方法(双精度型长整型日期时间型颜色字符串),并将其添加至链表末尾。单元格数据输出格式的附加参数均设置默认值。可在单元格创建后进行修改。首先创建新单元格对象,然后将其添加至链表末尾。如果对象创建失败,则立即释放以避免内存泄漏。

将单元格添加至链表末尾的方法如下:

//+------------------------------------------------------------------+
//| Добавляет ячейку в конец списка                                  |
//+------------------------------------------------------------------+
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;
  }

任何新创建的单元格都会被自动添加至链表末尾。随后,可通过后续将创建的表格模型类的方法,将其移动至合适位置。

为指定单元格设置值的重载方法如下:

//+------------------------------------------------------------------+
//| Устанавливает 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);
  }

通过索引从链表中获取目标单元格,并为其设置对应值。如果单元格为不可编辑状态,单元格对象的SetValue()方法将仅更新待设置值的类型信息。而不会实际修改值本身。

将对象赋值给单元格的方法如下:

//+------------------------------------------------------------------+
//| Назначает в ячейку объект                                        |
//+------------------------------------------------------------------+
void CTableRow::CellAssignObject(const uint index,CObject *object)
  {
//--- Получаем из списка нужную ячейку и записываем в неё указатель на объект
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.AssignObject(object);
  }

我们通过索引获取单元格对象,并调用其AssignObject()方法将对象指针赋值给该单元格。

取消单元格已分配对象的方法如下:

//+------------------------------------------------------------------+
//| Отменяет для ячейки назначенный объект                           |
//+------------------------------------------------------------------+
void CTableRow::CellUnassignObject(const uint index)
  {
//--- Получаем из списка нужную ячейку и отменяем в ней указатель на объект и его тип
   CTableCell *cell=this.GetCell(index);
   if(cell!=NULL)
      cell.UnassignObject();
  }

我们通过索引获取单元格对象,并调用其UnassignObject()方法移除已分配给该单元格的对象指针。

删除单元格的方法如下:

//+------------------------------------------------------------------+
//| Удаляет ячейку                                                   |
//+------------------------------------------------------------------+
bool CTableRow::CellDelete(const uint index)
  {
//--- Удаляем ячейку в списке по индексу
   if(!this.m_list_cells.Delete(index))
      return false;
//--- Обновляем индексы для оставшихся ячеек в списке
   this.CellsPositionUpdate();
   return true;
  }

我们使用CList类的Delete()方法从链表中删除该单元格。删除单元格后,链表中剩余单元格的索引会发生变化。通过调用CellsPositionUpdate()方法,我们更新链表中所有剩余单元格的索引。

将单元格移动到指定位置的方法如下:

//+------------------------------------------------------------------+
//| Перемещает ячейку на указанную позицию                           |
//+------------------------------------------------------------------+
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;
  }

为了让CList类对链表中的某个对象进行操作,该对象必须为当前选中对象。例如通过选中操作使其成为当前对象。因此,我们首先通过索引从链表中获取目标单元格对象。使其成为当前对象,随后,调用CList类的MoveToIndex()方法将该对象移动到链表中的指定位置。成功移动对象后,必须调整剩余对象的索引,这一操作通过CellsPositionUpdate()方法完成。

为链表中所有单元格设置行和列位置的方法如下:

//+------------------------------------------------------------------+
//| Устанавливает позиции строки и колонки всем ячейкам              |
//+------------------------------------------------------------------+
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));
     }
  }

CList类允许您查找链表中当前选中对象的索引。但前提是该对象必须处于选中状态。在此方法中,我们遍历链表中的所有单元格对象,依次选中每个单元格,并使用CList类的IndexOf()方法获取其索引。随后,通过单元格对象的SetPositionInTable()方法,将行索引和查找到的单元格索引设置到该单元格对象中。

重置行单元格数据的方法如下:

//+------------------------------------------------------------------+
//| Обнуляет данные ячеек строки                                     |
//+------------------------------------------------------------------+
void CTableRow::ClearData(void)
  {
//--- В цикле по всем ячейкам в списке
   for(uint i=0;i<this.CellsTotal();i++)
     {
      //--- получаем очередную ячейку и устанавливаем в неё пустое значение
      CTableCell *cell=this.GetCell(i);
      if(cell!=NULL)
         cell.ClearData();
     }
  }

在循环中,通过调用单元格对象的ClearData()方法,依次重置链表中的下一个单元格。对于字符串类型数据,向单元格写入空行;对于数值类型数据,则写入0值。

返回对象描述信息的方法如下:

//+------------------------------------------------------------------+
//| Возвращает описание объекта                                      |
//+------------------------------------------------------------------+
string CTableRow::Description(void)
  {
   return(::StringFormat("%s: Position %u, Cells total: %u",
                         TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.Index(),this.CellsTotal()));
  }

从对象的属性和数据中收集一行信息,并按照以下格式返回,例如:

表格行:位置1,单元格总数:4

将对象描述信息输出到日志的方法如下:

//+------------------------------------------------------------------+
//| Выводит в журнал описание объекта                                |
//+------------------------------------------------------------------+
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);
     }
  }

对于日志中非表格形式的数据展示,首先会在日志中显示表头作为行描述信息。随后,如果启用了详细显示标识,则会通过遍历单元格列表,在日志中逐项输出每个单元格的描述信息。

因此,表格行的详细日志输出效果如下(以非表格视图为例):

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

对于表格形式的展示,输出结果示例如下:

| Row 0     |         0 |         1 |         2 |         3 |

将表格行数据保存到文件的方法如下:

//+------------------------------------------------------------------+
//| Сохранение в файл                                                |
//+------------------------------------------------------------------+
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;
  }

先保存数据的起始标记,随后保存对象类型。这是文件中每个对象的标准头部信息。之后,在对象的属性文件中写入对应条目。这里,将行索引保存至文件,随后保存至单元格列表。

从文件加载表格行数据的方法如下:

//+------------------------------------------------------------------+
//| Загрузка из файла                                                |
//+------------------------------------------------------------------+
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;
  }

所有数据的加载顺序与保存时完全一致。首先,加载数据起始标记和对象类型,并进行校验。随后,加载行索引和完整的单元格列表。


4. 表格模型类

最简单的表格模型是行链表结构,而每行又包含单元格的链表。我们今天构建的模型将接收一个二维数组作为输入(双精度型长整型日期时间型颜色字符串),并基于此构建虚拟表格。后续,我们将扩展此类,使其支持通过其他输入数据创建表格。该模型将作为默认模型使用。

让我们继续在文件\MQL5\Scripts\TableModel\TableModelTest.mq5中编写代码。

表格模型类本质上是行的链表结构,包含管理行、列和单元格的方法。编写类主体(包含所有变量和方法),随后分析该类已声明的方法:

//+------------------------------------------------------------------+
//| Класс модели таблицы                                             |
//+------------------------------------------------------------------+
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){}
  };

本质上,该类仅包含一个用于管理表格行链表的对象,以及用于操作行、单元格和列的方法。同时提供支持不同类型二维数组的构造函数。

将数组传递给类构造函数后,会调用创建表格模型的方法

//+------------------------------------------------------------------+
//| Создаёт модель таблицы из двумерного массива                     |
//+------------------------------------------------------------------+
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]);
        }
     }
  }

方法的逻辑已经在注释中说明。数组的第一维对应表格的行,第二维对应每行的单元格。创建单元格时,其数据类型与传入方法的数组类型一致。

因此,我们可以创建多个表格模型,其单元格初始存储不同类型的数据(双精度型长整型日期时间型颜色以及字符串)。后续,在表格模型创建完成后,单元格中存储的数据类型仍可修改。

创建一个新空行并添加到列表末尾的方法:

//+------------------------------------------------------------------+
//| Создаёт новую пустую строку и добавляет в конец списка           |
//+------------------------------------------------------------------+
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;
  }

该方法会创建一个新的CTableRow类对象,并通过AddNewRow()方法将其添加到行列表的末尾。如果添加过程中发生错误,则删除已创建的新对象并返回NULL。成功时,该方法返回指向新添加行的指针。

将行对象添加到列表末尾的方法如下:

//+------------------------------------------------------------------+
//| Добавляет строку в конец списка                                  |
//+------------------------------------------------------------------+
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;
  }

由于上述两个方法均位于类的受保护 区域,需成对使用,且在向表格添加新行时由内部调用。

创建新行并添加到列表末尾的方法如下:

//+------------------------------------------------------------------+
//| Создаёт новую строку и добавляет в конец списка                  |
//+------------------------------------------------------------------+
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;
  }

这是一个公有方法。它用于向表格中添加一行包含单元格的新行。新创建行的单元格数量将与表格首行的单元格数量保持一致。

在指定列表位置创建并添加新行的方法如下:

//+------------------------------------------------------------------+
//| Создаёт и добавляет новую строку в указанную позицию списка      |
//+------------------------------------------------------------------+
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;
  }

有时您需要在行列表的末尾之外的位置插入新行,例如在现有行之间插入。该方法会先在列表末尾创建新行并填充单元格,随后清空这些单元格,最后将该行移动到目标位置。

为指定单元格设置值的方法如下:

//+------------------------------------------------------------------+
//| Устанавливает значение в указанную ячейку                        |
//+------------------------------------------------------------------+
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;
     }
  }

首先,通过行和列的坐标获取目标单元格的指针,然后为其设置值。无论传入该方法用于填充单元格的值是什么类型,该方法均会自动选择正确的数据类型进行存储——双精度型长整型日期时间型颜色字符串

设置指定单元格数据显示精度的方法如下:

//+------------------------------------------------------------------+
//| Устанавливает точность отображения данных в указанную ячейку     |
//+------------------------------------------------------------------+
void CTableModel::CellSetDigits(const uint row,const uint col,const int digits)
  {
//--- Получаем ячейку по индексам строки и столбца и
//--- вызываем её соответствующий метод для установки значения
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.SetDigits(digits);
  }

该方法仅适用于存储实数类型的单元格。其用于指定单元格显示数值时保留的小数位数。

为指定单元格设置时间显示标识的方法如下:

//+------------------------------------------------------------------+
//| Устанавливает флаги отображения времени в указанную ячейку       |
//+------------------------------------------------------------------+
void CTableModel::CellSetTimeFlags(const uint row,const uint col,const uint flags)
  {
//--- Получаем ячейку по индексам строки и столбца и
//--- вызываем её соответствующий метод для установки значения
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.SetDatetimeFlags(flags);
  }

适用于显示日期时间值的单元格。通过单元格设置时间显示格式(可选值包括:TIME_DATE|TIME_MINUTES|TIME_SECONDS,或它们的组合)

TIME_DATE显示结果为 "yyyy.mm.dd",
TIME_MINUTES显示结果为 "hh:mi",
TIME_SECONDS显示结果为 "hh:mi:ss"。

为指定单元格设置颜色名称显示标识的方法如下:

//+------------------------------------------------------------------+
//| Устанавливает флаг отображения имён цветов в указанную ячейку    |
//+------------------------------------------------------------------+
void CTableModel::CellSetColorNamesFlag(const uint row,const uint col,const bool flag)
  {
//--- Получаем ячейку по индексам строки и столбца и
//--- вызываем её соответствующий метод для установки значения
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.SetColorNameFlag(flag);
  }

仅适用于显示颜色值的单元格。当单元格中存储的颜色存在于颜色表中时,该方法用于指示需要显示对应的颜色名称。

将对象赋值给单元格的方法如下:

//+------------------------------------------------------------------+
//| Назначает объект в ячейку                                        |
//+------------------------------------------------------------------+
void CTableModel::CellAssignObject(const uint row,const uint col,CObject *object)
  {
//--- Получаем ячейку по индексам строки и столбца и
//--- вызываем её соответствующий метод для установки значения
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.AssignObject(object);
  }

取消单元格已分配对象的方法如下:

//+------------------------------------------------------------------+
//| Отменяет назначение объекта в ячейке                             |
//+------------------------------------------------------------------+
void CTableModel::CellUnassignObject(const uint row,const uint col)
  {
//--- Получаем ячейку по индексам строки и столбца и
//--- вызываем её соответствующий метод для установки значения
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.UnassignObject();
  }

上述两种方法允许您为单元格分配对象或取消分配。该对象必须是CObject类的派生类。在关于表格的文章中,对象可以是例如来自ENUM_OBJECT_TYPE枚举的已知对象列表之一。目前,该列表仅包含单元格对象、行和表格模型。将它们分配给单元格并无实际意义。但随着我们撰写关于视图组件的文章(其中将创建控件),该枚举范围会逐步扩展。届时将控件(例如“下拉列表”控件)分配给单元格才具有实际价值。

删除指定单元格的方法如下:

//+------------------------------------------------------------------+
//| Удаляет ячейку                                                   |
//+------------------------------------------------------------------+
bool CTableModel::CellDelete(const uint row,const uint col)
  {
//--- Получаем строку по индексу и возвращаем результат удаления ячейки из списка
   CTableRow *row_obj=this.GetRow(row);
   return(row_obj!=NULL ? row_obj.CellDelete(col) : false);
  }

该方法通过行索引获取行对象,并调用其删除指定单元格的方法。对于单行中的单个单元格,此方法目前尚无实际意义,因为它仅会减少表格中某一行的单元格数量。导致该行单元格与相邻行不同步。目前尚未实现此类删除操作的处理逻辑(例如将删除单元格旁的单元格“扩展”为两个单元格的宽度以维持表格结构)。然而,使用该方法作为表格列删除方法的一部分——在列删除操作中,所有行的对应单元格会被一次性删除,从而确保整个表格的完整性不受破坏。

移动表格单元格的方法如下:

//+------------------------------------------------------------------+
//| Перемещает ячейку                                                |
//+------------------------------------------------------------------+
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);
  }

通过行索引获取行对象,并调用其删除指定单元格的方法。

返回指定行中单元格数量的方法如下:

//+------------------------------------------------------------------+
//| Возвращает количество ячеек в указанной строке                   |
//+------------------------------------------------------------------+
uint CTableModel::CellsInRow(const uint index)
  {
   CTableRow *row=this.GetRow(index);
   return(row!=NULL ? row.CellsTotal() : 0);
  }

通过行索引获取行对象,并调用CellsTotal()方法返回该行中的单元格数量。

返回表格中单元格总数的方法如下:

//+------------------------------------------------------------------+
//| Возвращает количество ячеек в таблице                            |
//+------------------------------------------------------------------+
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;
  }

该方法遍历表格中的所有行,将每行的单元格数量累加到总结果中并返回。当表格包含大量行时,这种逐行计数的方式可能会比较耗时。建议在所有影响表格单元格数量的方法创建完成后,仅在单元格数量发生变化时才重新计算总数。

返回指定表格单元格的方法如下:

//+------------------------------------------------------------------+
//| Возвращает указанную ячейку таблицы                              |
//+------------------------------------------------------------------+
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);
  }

通过行索引获取行对象,再利用行对象的 GetCell() 方法根据列索引返回指向单元格对象的指针。

返回单元格描述信息的方法如下:

//+------------------------------------------------------------------+
//| Возвращает описание ячейки                                       |
//+------------------------------------------------------------------+
string CTableModel::CellDescription(const uint row,const uint col)
  {
   CTableCell *cell=this.GetCell(row,col);
   return(cell!=NULL ? cell.Description() : "");
  }

通过索引获取行对象,从行中获取单元格并返回其描述信息。

将单元格描述信息输出到日志的方法如下:

//+------------------------------------------------------------------+
//| Выводит в журнал описание ячейки                                 |
//+------------------------------------------------------------------+
void CTableModel::CellPrint(const uint row,const uint col)
  {
//--- Получаем ячейку по индексу строки и колонки и возвращаем её описание
   CTableCell *cell=this.GetCell(row,col);
   if(cell!=NULL)
      cell.Print();
  }

通过行索引和列索引获取指向单元格的指针,并利用单元格对象的Print()方法将其描述信息输出到日志中。

删除指定行的方法如下:

//+------------------------------------------------------------------+
//| Удаляет строку                                                   |
//+------------------------------------------------------------------+
bool CTableModel::RowDelete(const uint index)
  {
//--- Удаляем строку из списка по индексу
   if(!this.m_list_rows.Delete(index))
      return false;
//--- После удаления строки необходимо обновить все индексы всех ячеек таблицы
   this.CellsPositionUpdate();
   return true;
  }

使用CList类的Delete()方法,根据索引从列表中删除对应的行对象。删除行后,剩余行及其单元格的索引将与实际不符,必须通过CellsPositionUpdate()方法进行调整以恢复正确的索引对应关系。

将行移动到指定位置的方法如下:

//+------------------------------------------------------------------+
//| Перемещает строку на указанную позицию                           |
//+------------------------------------------------------------------+
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;
  }

在CList类中,许多方法均针对当前列表对象进行操作。先获取目标行的指针并将其设为当前行,再通过CList类的MoveToIndex()方法将其移动到指定位置。将行移动至新位置后,需更新剩余行的索引信息,这一操作通过CellsPositionUpdate()方法完成。

为所有单元格设置行和列位置的方法如下:

//+------------------------------------------------------------------+
//| Устанавливает позиции строки и колонки всем ячейкам              |
//+------------------------------------------------------------------+
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();
     }
  }

遍历表格中的所有行列表,依次选取每一行,并使用CList类的IndexOf()方法为其设置正确的索引值。随后调用行的CellsPositionUpdate()方法,为该行中的每个单元格设置正确的索引。

清空一行中所有单元格数据的方法如下:

//+------------------------------------------------------------------+
//| Очищает строку (только данные в ячйках)                          |
//+------------------------------------------------------------------+
void CTableModel::RowResetData(const uint index)
  {
//--- Получаем строку из списка и очищаем данные ячеек строки методом ClearData()
   CTableRow *row=this.GetRow(index);
   if(row!=NULL)
      row.ClearData();
  }

该行中的每个单元格均被重置为“空”值。目前为简化处理,数值型单元格的空值设为0,但后续会修改此设定(因为0本身也是需要显示的单元格值)。重置值意味着显示为空单元格字段。

清空表格中所有单元格数据的方法如下:

//+------------------------------------------------------------------+
//| Очищает таблицу (данные всех ячеек)                              |
//+------------------------------------------------------------------+
void CTableModel::ClearData(void)
  {
//--- В цикле по всем строкам таблицы очищаем данные каждой строки
   for(uint i=0;i<this.RowsTotal();i++)
      this.RowResetData(i);
  }

遍历表格中的所有行,并对每一行调用上述RowResetData()方法。

返回行描述信息的方法如下:

//+------------------------------------------------------------------+
//| Возвращает описание строки                                       |
//+------------------------------------------------------------------+
string CTableModel::RowDescription(const uint index)
  {
//--- Получаем строку по индексу и возвращаем её описание
   CTableRow *row=this.GetRow(index);
   return(row!=NULL ? row.Description() : "");
  }

通过索引获取行指针,并返回该行的描述信息。

将行描述信息输出到日志的方法如下:

//+------------------------------------------------------------------+
//| Выводит в журнал описание строки                                 |
//+------------------------------------------------------------------+
void CTableModel::RowPrint(const uint index,const bool detail)
  {
   CTableRow *row=this.GetRow(index);
   if(row!=NULL)
      row.Print(detail);
  }

获取指向行的指针,并调用该对象的Print()方法。

删除表格列的方法如下:

//+------------------------------------------------------------------+
//| Удаляет столбец                                                  |
//+------------------------------------------------------------------+
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;
  }

遍历表格中的所有行,依次获取每一行,并根据列索引删除其中对应的单元格。此操作将删除表格中所有具有相同列索引的单元格。

移动表格列的方法如下:

//+------------------------------------------------------------------+
//| Перемещает столбец                                               |
//+------------------------------------------------------------------+
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;
  }

遍历表格中的所有行,依次获取每一行,并将指定列索引的单元格移动到新位置。此操作会移动表格中所有具有相同列索引的单元格。

清空列单元格数据的方法如下:

//+------------------------------------------------------------------+
//| Очищает данные столбца                                           |
//+------------------------------------------------------------------+
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();
     }
  }

遍历表格中的所有行,依次获取每一行,并清空指定列索引的单元格数据。此操作会清空表格中所有具有相同列索引的单元格数据。

返回对象描述信息的方法如下:

//+------------------------------------------------------------------+
//| Возвращает описание объекта                                      |
//+------------------------------------------------------------------+
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()));
  }

根据表格模型的某些参数创建一行,并且按照以下格式返回:

表格模型:行数4,每行单元格数4,单元格总数16

将对象描述信息输出到日志的方法如下:

//+------------------------------------------------------------------+
//| Выводит в журнал описание объекта                                |
//+------------------------------------------------------------------+
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);
        }
     }
  }

首先,将表头作为模型描述打印输出,随后,如果启用了详细输出标识,则在循环中逐行打印表格模型的所有详细描述信息。

以表格形式将对象描述输出至日志的方法如下:

//+------------------------------------------------------------------+
//| Выводит в журнал описание объекта в табличном виде               |
//+------------------------------------------------------------------+
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);
     }
  }

首先,根据表格首行的单元格数量创建表头,并将列名称打印到日志中。然后,通过循环遍历表格的所有行,并以表格形式逐行打印。

销毁表格模型的方法如下:

//+------------------------------------------------------------------+
//| Уничтожает модель                                                |
//+------------------------------------------------------------------+
void CTableModel::Destroy(void)
  {
//--- Очищаем список строк
   m_list_rows.Clear();
  }

表格行列表会直接被清空,并通过调用CList类的Clear()方法销毁所有对象。

将表格模型保存到文件的方法如下:

//+------------------------------------------------------------------+
//| Сохранение в файл                                                |
//+------------------------------------------------------------------+
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;
  }

在保存数据起始标记和列表类型后,使用CList类的 Save()方法将行列表保存到文件中。

从文件加载表格模型的方法如下:

//+------------------------------------------------------------------+
//| Загрузка из файла                                                |
//+------------------------------------------------------------------+
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;
  }

在加载并检查数据起始标记和列表类型后,使用文章开头介绍的CListObj类的Load()方法从文件中加载行列表。

所有用于创建表格模型的类已准备就绪。现在,编写一个脚本来测试模型的功能。


测试结果

继续在同一文件中编写代码。编写一个脚本,创建一个4x4的二维数组(4行,每行4个单元格),类型例如为 long。接着,打开一个文件,将表格模型的数据写入其中,并从文件加载数据到表格中。创建表格模型并测试其部分方法的运行情况。

每次表格发生更改时,我们将使用TableModelPrint()函数记录结果,该函数决定如何打印表格模型。如果宏PRINT_AS_TABLE的值为true,则使用CTableModel类的PrintTable()方法记录日志;如果值为false,则使用同一类的Print()方法。

在脚本中,创建一个表格模型,以表格形式打印出来,并将模型保存到文件中。然后添加行、删除列,并修改编辑权限……

接下来,再次从文件中加载表格的初始原始版本,并打印结果。

#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;
  }

最终,我们在日志中得到的脚本运行结果如下:

表格模型已成功创建:
|       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 |

为测试表格模型的操作(包括添加、删除和移动行列,以及文件读写功能),完成以下脚本:

#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);   // Распечатать детализированные данные таблицы
  }

在日志中获取以下结果:

表格模型已成功创建:
|       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 |

表格模型已成功保存至文件。



在第2个位置插入新行,并将第3个单元格设置为不可编辑状态
|       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 |

从位置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 |

从文件中加载原始表格视图:
|       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 |

如果您需要以非表格形式将表格模型的数据输出到日志中,请将宏PRINT_AS_TABLE设置为false

#define  PRINT_AS_TABLE    false  // Распечатывать модель как таблицу

此时,日志中将显示以下内容:

表格模型已成功创建:
|       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 |

表格模型已成功保存至文件。

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

这种输出方式提供了更详细的调试信息。例如,由此可见,将单元格的编辑禁止标识设置为有效时,会在程序日志中显示

所有声明的功能均按预期运行:可以添加和移动行、删除列、修改单元格属性,并支持文件操作。


结论

这是首个简易表格模型的版本,其支持通过二维数据数组创建表格。接下来,我们将开发其他专用表格模型,创建表格视图组件,并逐步实现完整的表格数据处理功能,使其成为用户图形界面中的核心控件之一。

当前创建的包含所有类的脚本文件已随文章附上。您可以下载用于自主学习。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/17653

附加的文件 |
TableModelTest.mq5 (136.43 KB)
最近评论 | 前往讨论 (9)
Alexey Viktorov
Alexey Viktorov | 4 4月 2025 在 15:31
Artyom Trishkin #:

当调用 SomeObject 的 Load() 方法从文件中加载 SomeObject 类时,它会检查 "我是否真的从文件中读取了自己?如果没有,就意味着出错了,所以没有必要继续加载。

我这里所使用的是一个从文件中读取对象类型的列表(CListObj)。列表不知道文件中有什么(什么对象)。但它必须知道该对象类型,才能在 CreateElement() 方法中创建该对象。这就是它不检查从文件中加载的对象类型的原因。毕竟,Type() 方法返回的是列表类型,而不是对象类型。

谢谢,我想明白了,我明白了。

Maxim Kuznetsov
Maxim Kuznetsov | 5 4月 2025 在 08:05

我读了一遍,又重读了一遍。

是 MVC 中 "模型 "以外的任何东西。例如一些 ListStorage

Rashid Umarov
Rashid Umarov | 5 4月 2025 在 08:37
我们言归正传。请保留您的意见。
Aleksey Nikolayev
Aleksey Nikolayev | 5 4月 2025 在 09:38
我想知道有没有可能以这种方式获得类似 python 和 R 数据框的数据?在这些表格中,不同列可以包含不同类型的数据(来自一组有限的类型,但包括字符串)。
Artyom Trishkin
Artyom Trishkin | 5 4月 2025 在 11:29
Aleksey Nikolayev #:
我想知道有没有可能以这种方式获得类似 python 和 R 数据框的数据?在这些表格中,不同列可以包含不同类型的数据(来自一组有限的类型,但包括字符串)。

可以。如果我们说的是一个表格的不同列,那么在所述实现中,表格的每个单元格都可以有不同的数据类型。

从新手到专家:对K线进行编程 从新手到专家:对K线进行编程
在本文中,我们将迈出 MQL5 编程的第一步,即使是完全零基础的初学者也能上手。我们将向您展示,如何将熟悉的 K线形态 转换为一个功能完备的自定义指标。K线形态之所以有价值,是因为它们反映了真实的价格行为,并预示着市场的转变。与其手动扫描图表——这种方法容易出错且效率低下——我们将讨论如何通过一个指标来自动化这个过程,该指标会自动识别并标记出这些形态。在此过程中,我们将探讨一些关键概念,例如索引、时间序列、平均真实波幅(用于在多变的市场波动性中提高准确性),以及如何开发一个可自定义、可复用的 K线形态库,以便在未来的项目中使用。
开发多币种 EA 交易(第 23 部分):整理自动项目优化阶段的输送机(二) 开发多币种 EA 交易(第 23 部分):整理自动项目优化阶段的输送机(二)
我们的目标是创建一个系统,用于自动定期优化最终 EA 中使用的交易策略。随着系统的发展,它变得越来越复杂,因此有必要不时地将其视为一个整体,以确定瓶颈和次优解决方案。
使用MQL5经济日历进行交易(第七部分):基于资源型新闻事件分析的策略测试准备 使用MQL5经济日历进行交易(第七部分):基于资源型新闻事件分析的策略测试准备
在本文中,我们通过将经济日历数据作为非实盘分析资源嵌入到MQL5交易系统中,为策略测试做好准备。我们实现了按时间、货币和影响程度加载和筛选事件的功能,并在策略测试器中验证其有效性。这使得基于新闻事件的策略能够进行高效的回测。
开发多币种 EA 交易(第 22 部分):开始向设置的热插拔过渡 开发多币种 EA 交易(第 22 部分):开始向设置的热插拔过渡
如果要自动进行周期性优化,我们需要考虑自动更新交易账户上已经运行的 EA 设置。这样一来,我们就可以在策略测试器中运行 EA,并在单次运行中更改其设置。