Implémentation d'un modèle de table dans MQL5 : Application du concept MVC
Sommaire
- Introduction
- Un peu de concept MVC (Modèle-Vue-Contrôleur)
- Écrire des classes pour construire un modèle de tableau
- Les listes chaînées comme base de stockage des données tabulaires
- Classe de cellules du tableau
- Classe de ligne du tableau
- Classe de modèle du tableau
- Tester le résultat
- Conclusion
Introduction
En programmation, l'architecture des applications joue un rôle clé pour garantir la fiabilité, l'évolutivité et la facilité d'assistance. L'une des approches permettant d'atteindre ces objectifs consiste à tirer parti d'un modèle d'architecture appelé MVC (Modèle-Vue-Contrôleur).
Le concept MVC permet de diviser une application en 3 composants interdépendants : le modèle (gestion des données et de la logique), la vue (affichage des données) et le contrôleur (traitement des actions de l'utilisateur). Cette séparation simplifie le développement, le test et la maintenance du code, le rendant plus structuré et plus flexible.
Dans cet article, nous examinons comment appliquer les principes MVC pour mettre en œuvre un modèle de tableau dans le langage MQL5. Les tableaux sont un outil important pour le stockage, le traitement et l'affichage des données, et une bonne organisation peut faciliter grandement le travail avec les informations. Nous allons créer des classes pour travailler avec des tableaux : cellules de tableau, lignes et modèle de tableau. Pour stocker les cellules dans les lignes et les lignes dans le modèle de tableau, nous utiliserons les classes de listes liées (linked list) de la bibliothèque standard MQL5 qui permettent un stockage et une utilisation efficaces des données.
Quelques mots sur le concept MVC : qu'est-ce que c'est et pourquoi le voulons-nous ?
Imaginez l'application comme une production théâtrale. Il existe un scénario qui décrit ce qui devrait se passer (c'est le modèle). Il y a la scène - ce que le spectateur voit (c'est la vue). Enfin, il y a le directeur qui gère l'ensemble du processus et relie les autres éléments (c'est le contrôleur). C'est ainsi que fonctionne le modèle architectural MVC (Modèle-Vue-Contrôleur).
Ce concept permet de séparer les responsabilités au sein de l'application. Le modèle est responsable des données et de la logique, la vue est responsable de l'affichage et de l'apparence, et le contrôleur est responsable du traitement des actions de l'utilisateur. Cette séparation rend le code plus clair, plus souple et plus pratique pour le travail en équipe.
Supposons que vous créiez un tableau. Le modèle connaît les lignes et les cellules qu'il contient et sait comment les modifier. La vue dessine le tableau à l'écran. Le contrôleur réagit lorsque l'utilisateur clique sur "Ajouter une ligne" et transmet la tâche au modèle, puis demande à la vue de se mettre à jour.
Le modèle MVC est particulièrement utile lorsque l'application devient plus complexe : de nouvelles fonctionnalités sont ajoutées, l'interface change et plusieurs développeurs travaillent. Avec une architecture claire, il est plus facile d'apporter des modifications, de tester les composants individuellement et de réutiliser le code.
Cette approche présente également quelques inconvénients. Pour les projets très simples, le modèle MVC peut être redondant : il faudra séparer même ce qui pourrait tenir dans quelques fonctions. Toutefois, pour les applications sérieuses et évolutives, cette structure s'avère rapidement payante.
En résumé :
Le modèle MVC est un modèle architectural puissant qui permet d'organiser le code, de le rendre plus compréhensible, testable et évolutif. Il est particulièrement utile pour les applications complexes où la séparation de la logique des données, de l'interface utilisateur et de la gestion est importante. Pour les petits projets, son utilisation est superflue.
Le paradigme Modèle-Vue-Contrôleur convient parfaitement à notre tâche. Le tableau sera créé à partir d'objets indépendants :
- Cellule
Un objet qui stocke une valeur de l'un de ces types : réel, entier ou chaîne - est équipé d'outils permettant de gérer la valeur, de la définir et de la récupérer - Ligne
Un objet qui stocke une liste d'objets dans des cellules de tableau est doté d'outils de gestion des cellules, de leur emplacement, de l'ajout et de la suppression - Modèle de tableau
Un objet qui stocke une liste d'objets de chaînes de tableau, est équipé d'outils permettant de gérer les chaînes de tableau et les colonnes, leur emplacement, l'ajout et la suppression, et a également accès aux contrôles des chaînes et des cellules
La figure ci-dessous montre schématiquement la structure d'un modèle de tableau 4x4 :

Fig. 1 : Modèle de tableau 4x4
Passons maintenant de la théorie à la pratique.
Écrire des classes pour construire un modèle de tableau
Nous utiliserons la bibliothèque standard MQL5 pour créer tous les objets.
Chaque objet hérite de la classe de basede la bibliothèque. Cela vous permettra de stocker ces objets dans des listes d'objets.
Nous écrirons toutes les classes dans un seul fichier de script de test afin que tout soit dans un seul fichier, visible et rapidement accessible. À l'avenir, nous distribuerons les classes écrites dans des fichiers include distincts.
1. Les listes chaînées comme base de stockage des données tabulaires
La liste chaînée CList est très bien adaptée au stockage de données tabulaires. Contrairement à la liste CArrayObj similaire, elle implémente des méthodes d'accès aux objets de liste voisins situés à gauche et à droite de l'objet actuel. Il sera ainsi facile de déplacer des cellules dans une ligne, ou de déplacer des lignes dans un tableau, d'en ajouter et d'en supprimer. Parallèlement, la liste elle-même se charge de l'indexation correcte des objets déplacés, ajoutés ou supprimés dans la liste.
Mais il y a une nuance à apporter. Si vous vous reportez aux méthodes de chargement et d'enregistrement d'une liste dans un fichier, vous constaterez que lors du chargement à partir d'un fichier, la classe de liste doit créer un nouvel objet dans la méthode virtuelle CreateElement().
Cette méthode de cette classe renvoie simplement NULL :
//--- method of creating an element of the list virtual CObject *CreateElement(void) { return(NULL); }
Cela signifie que pour travailler avec des listes chaînées, et à condition que nous ayons besoin d'opérations sur les fichiers, nous devons hériter de la classe CList et implémenter cette méthode dans notre classe.
Si vous examinez les méthodes d'enregistrement des objets de la bibliothèque standard dans un fichier, vous pouvez voir l'algorithme suivant pour l'enregistrement des propriétés des objets :
- Le marqueur de début de données (-1) est écrit dans le fichier,
- Le type d'objet est écrit dans le fichier,
- Toutes les propriétés de l'objet sont écrites dans le fichier une par une.
Les premier et deuxième points sont inhérents à toutes les méthodes de sauvegarde/chargement implémentées que possèdent les objets de la bibliothèque standard. Par conséquent, en suivant la même logique, nous voulons connaître le type de l'objet enregistré dans la liste, afin de pouvoir, lors de la lecture d'un fichier, créer un objet de ce type dans la méthode virtuelle CreateElement() de la classe de liste héritée de CList.
Plus tous les objets qui peuvent être chargés dans la liste — leurs classes doivent être déclarées ou créées avant que la classe de liste ne soit implémentée. Dans ce cas, la liste "saura" quels objets sont "en question" et lesquels doivent être créés.
Dans le répertoire du terminal \MQL5\Scripts\, créez un nouveau dossier TableModel\, et dans celui-ci, créez un nouveau fichier de script de test TableModelTest.mq5.
Connectez le fichier de listes chaînées et déclarez les futures classes de modèles de tableaux :
//+------------------------------------------------------------------+ //| 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 libraries | //+------------------------------------------------------------------+ #include <Arrays\List.mqh> //--- Forward declaration of classes class CTableCell; // Table cell class class CTableRow; // Table row class class CTableModel; // Table model class
La déclaration préalable des classes futures est nécessaire ici pour que la classe de liste chaînée qui hérite de CList connaisse ces types de classes, ainsi que les types d'objets qu'elle devra créer. Pour ce faire, nous écrirons des énumérations de types d'objets, des macros auxiliaires et des énumérations pour les façons de trier les listes :
//+------------------------------------------------------------------+ //| Include libraries | //+------------------------------------------------------------------+ #include <Arrays\List.mqh> //--- Forward declaration of classes class CTableCell; // Table cell class class CTableRow; // Table row class class CTableModel; // Table model class //+------------------------------------------------------------------+ //| Macros | //+------------------------------------------------------------------+ #define MARKER_START_DATA -1 // Data start marker in a file #define MAX_STRING_LENGTH 128 // Maximum length of a string in a cell //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum ENUM_OBJECT_TYPE // Enumeration of object types { OBJECT_TYPE_TABLE_CELL=10000, // Table cell OBJECT_TYPE_TABLE_ROW, // Table row OBJECT_TYPE_TABLE_MODEL, // Table model }; enum ENUM_CELL_COMPARE_MODE // Table cell comparison modes { CELL_COMPARE_MODE_COL, // Comparison by column number CELL_COMPARE_MODE_ROW, // Comparison by string number CELL_COMPARE_MODE_ROW_COL, // Comparison by row and column }; //+------------------------------------------------------------------+ //| Functions | //+------------------------------------------------------------------+ //--- Return the object type as a string 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; } //+------------------------------------------------------------------+ //| Classes | //+------------------------------------------------------------------+
La fonction qui renvoie la description du type d'objet est basée sur l'hypothèse que tous les noms des constantes de type objet commencent par la chaîne de caractères "OBJECT_TYPE_". Vous pouvez ensuite prendre la sous-chaîne qui suit celle-ci, convertir tous les caractères de la ligne résultante en minuscules, convertir le premier caractère en majuscules et supprimer tous les espaces et caractères de contrôle de la chaîne finale à gauche et à droite.
Écrivons notre propre classe de listes chaînées. Nous continuerons à écrire le code dans le même fichier :
//+------------------------------------------------------------------+ //| Classes | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Linked object list class | //+------------------------------------------------------------------+ class CListObj : public CList { protected: ENUM_OBJECT_TYPE m_element_type; // Created object type in CreateElement() public: //--- Virtual method (1) for loading a list from a file, (2) for creating a list element virtual bool Load(const int file_handle); virtual CObject *CreateElement(void); };
La classe CListObj est notre nouvelle classe de liste chaînée, héritée de la classe CList de la bibliothèque standard.
La seule variable de la classe sera celle dans laquelle sera inscrit le type de l'objet créé. Comme la méthode CreateElement() est virtuelle et doit avoir exactement la même signature que la méthode de la classe mère, nous ne pouvons pas lui transmettre le type de l'objet en cours de création. Mais nous pouvons écrire ce type dans une variable déclarée et lire le type de l'objet créé à partir de cette variable.
Nous devons redéfinir 2 méthodes virtuelles de la classe mère : la méthode de chargement à partir d'un fichier et la méthode de création d'un nouvel objet. Examinons-les.
Méthode de chargement de la liste à partir d'un fichier :
//+------------------------------------------------------------------+ //| Load a list from the file | //+------------------------------------------------------------------+ bool CListObj::Load(const int file_handle) { //--- Variables CObject *node; bool result=true; //--- Check the handle if(file_handle==INVALID_HANDLE) return(false); //--- Load and check the list start marker - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return(false); //--- Load and check the list type if(::FileReadInteger(file_handle,INT_VALUE)!=Type()) return(false); //--- Read the list size (number of objects) uint num=::FileReadInteger(file_handle,INT_VALUE); //--- Sequentially recreate the list elements by calling the Load() method of node objects this.Clear(); for(uint i=0; i<num; i++) { //--- Read and check the object data start marker - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return false; //--- Read the object type this.m_element_type=(ENUM_OBJECT_TYPE)::FileReadInteger(file_handle,INT_VALUE); node=this.CreateElement(); if(node==NULL) return false; this.Add(node); //--- Now the file pointer is offset relative to the beginning of the object marker by 12 bytes (8 - marker, 4 - type) //--- Set the pointer to the beginning of the object data and load the object properties from the file using the Load() method of the node element. if(!::FileSeek(file_handle,-12,SEEK_CUR)) return false; result &=node.Load(file_handle); } //--- Result return result; }
Ici, le début de la liste est d'abord contrôlé, ainsi que son type et sa taille, c'est-à-dire le nombre d'éléments de la liste ; puis, dans une boucle sur le nombre d'éléments, les marqueurs de début de données de chaque objet et son type sont lus à partir du fichier. Le type résultant est écrit dans la variable m_element_type et une méthode de création d'un nouvel élément est appelée. Dans cette méthode, un nouvel élément du type reçu est créé et écrit dans la variable pointeur node, qui est à son tour ajoutée à la liste. Toute la logique de la méthode est expliquée en détail dans les commentaires. Considérons une méthode de création d'un nouvel élément de liste.
Méthode de création d'un élément de liste :
//+------------------------------------------------------------------+ //| List element creation method | //+------------------------------------------------------------------+ CObject *CListObj::CreateElement(void) { //--- Create a new object depending on the object type in 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; } }
Cela signifie qu'avant d'appeler la méthode, le type de l'objet créé est déjà inscrit dans la variable m_element_type. En fonction du type d'élément, un nouvel objet du type approprié est créé et un pointeur sur cet objet est renvoyé. À l'avenir, lors du développement de nouveaux contrôles, leurs types seront inscrits dans l'énumération ENUM_OBJECT_TYPE. Et de nouveaux cas seront ajoutés ici pour créer de nouveaux types d'objets. La classe de liste chaînée basée sur le standard CList est prête. Elle peut désormais stocker tous les objets de types connus, enregistrer les listes dans un fichier, les charger à partir de ce fichier et les restaurer correctement.
2. Classe de cellules du tableau
Une cellule de tableau est l'élément le plus simple d'un tableau qui stocke une certaine valeur. Les cellules composent des listes représentant les lignes d'un tableau. Chaque liste représente une ligne du tableau. Dans notre tableau, les cellules ne pourront stocker qu'une seule valeur de plusieurs types à la fois - une valeur réelle, un nombre entier ou une chaîne de caractères.
En plus d'une simple valeur, une cellule peut se voir attribuer un objet d'un type connu de l'énumération ENUM_OBJECT_TYPE. Dans ce cas, la cellule peut stocker une valeur de l'un des types énumérés, ainsi qu'un pointeur vers un objet dont le type est écrit dans une variable spéciale. Ainsi, à l'avenir, on pourra demander au composant View d'afficher un tel objet dans une cellule afin d'interagir avec lui à l'aide du composant Controller.
Étant donné que plusieurs types de valeurs peuvent être stockés dans une cellule, nous utiliserons l'union pour les écrire, les stocker et les renvoyer. L'union est un type spécial de données qui stocke plusieurs champs dans la même zone de mémoire. Une union est similaire à une structure, mais ici, contrairement à une structure, les différents termes de l'union appartiennent à la même zone de mémoire. Dans la structure, chaque champ se voit attribuer sa propre zone de mémoire.
Continuons à écrire le code dans le fichier déjà créé. Commençons à écrire une nouvelle classe. Dans la section protégée, nous écrivons une union et déclarons des variables :
//+------------------------------------------------------------------+ //| Table cell class | //+------------------------------------------------------------------+ class CTableCell : public CObject { protected: //--- Combining for storing cell values (double, long, string) union DataType { protected: double double_value; long long_value; ushort ushort_value[MAX_STRING_LENGTH]; public: //--- Set values 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); } //--- Return values 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; } }; //--- Variables DataType m_datatype_value; // Value ENUM_DATATYPE m_datatype; // Data type CObject *m_object; // Cell object ENUM_OBJECT_TYPE m_object_type; // Object type in the cell int m_row; // Row index int m_col; // Column index int m_digits; // Data representation accuracy uint m_time_flags; // Date/time display flags bool m_color_flag; // Color name display flag bool m_editable; // Editable cell flag public:
Dans la section publique, écrivez des méthodes d'accès aux variables protégées, aux méthodes virtuelles et aux constructeurs de classe pour différents types de données stockées dans une cellule :
public: //--- Return cell coordinates and properties 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; } //--- Return (1) double, (2) long and (3) string value 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(); } //--- Return the value as a formatted string 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; } //--- Set variable values 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); } //--- Assign an object to a cell 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(); } //--- Remove the object assignment void UnassignObject(void) { this.m_object=NULL; this.m_object_type=-1; } //--- Set double value void SetValue(const double value) { this.m_datatype=TYPE_DOUBLE; if(this.m_editable) this.m_datatype_value.SetValueD(value); } //--- Set long value void SetValue(const long value) { this.m_datatype=TYPE_LONG; if(this.m_editable) this.m_datatype_value.SetValueL(value); } //--- Set datetime value void SetValue(const datetime value) { this.m_datatype=TYPE_DATETIME; if(this.m_editable) this.m_datatype_value.SetValueL(value); } //--- Set color value void SetValue(const color value) { this.m_datatype=TYPE_COLOR; if(this.m_editable) this.m_datatype_value.SetValueL(value); } //--- Set string value void SetValue(const string value) { this.m_datatype=TYPE_STRING; if(this.m_editable) this.m_datatype_value.SetValueS(value); } //--- Clear data void ClearData(void) { if(this.Datatype()==TYPE_STRING) this.SetValue(""); else this.SetValue(0.0); } //--- (1) Return and (2) display the object description in the journal string Description(void); void Print(void); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(OBJECT_TYPE_TABLE_CELL);} //--- Constructors/destructor 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); } //--- Accept a double value 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); } //--- Accept a long value 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); } //--- Accept a datetime value 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); } //--- Accept color value 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); } //--- Accept string value 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) {} };
Dans les méthodes de définition des valeurs, le type de la valeur stockée dans la cellule est d'abord défini, puis l'indicateur de la fonction de modification des valeurs dans la cellule est vérifié. Ce n'est que lorsque l'indicateur est activé que la nouvelle valeur est enregistrée dans la cellule :
//--- Set double value void SetValue(const double value) { this.m_datatype=TYPE_DOUBLE; if(this.m_editable) this.m_datatype_value.SetValueD(value); }
Pourquoi procède-t-on ainsi ? Lors de la création d'une nouvelle cellule, celle-ci est créée avec le type réel de la valeur stockée. Si vous souhaitez modifier le type de valeur, mais que la cellule n'est pas modifiable, vous pouvez appeler la méthode pour définir la valeur du type souhaité avec n'importe quelle valeur. Le type de la valeur stockée sera modifié, mais la valeur de la cellule elle-même ne sera pas affectée.
La méthode de nettoyage des données met à zéro les valeurs numériques et insère un espace dans les chaînes de caractères :
//--- Clear data void ClearData(void) { if(this.Datatype()==TYPE_STRING) this.SetValue(""); else this.SetValue(0.0); }
Plus tard, nous procéderons différemment — afin qu'aucune donnée ne soit affichée dans les cellules effacées — pour que la cellule reste vide, nous créerons une valeur "vide" pour la cellule, puis, lorsque la cellule sera effacée, toutes les valeurs enregistrées et affichées dans cette cellule seront supprimées. Après tout, le zéro est aussi une valeur à part entière et, lorsque la cellule est effacée, les données numériques sont remplies de zéros. Ce n'est pas le cas.
Dans les constructeurs paramétriques de la classe, les coordonnées des cellules du tableau sont transmises — le nombre de lignes et de colonnes et la valeur du type requis (double, long, datetime, color, string). Certains types de valeurs nécessitent des informations supplémentaires :
- double — précision de la valeur de sortie (nombre de décimales),
- datetime — indicateurs de temps (date / heures / minutes / secondes),
- color — drapeau permettant d'afficher les noms des couleurs standard connues.
Dans les constructeurs avec ces types de valeurs stockées dans les cellules , des paramètres supplémentaires sont transmis pour définir le format des valeurs affichées dans les cellules :
//--- Accept a double value 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); } //--- Accept a long value 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); } //--- Accept a datetime value 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); } //--- Accept color value 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); } //--- Accept string value 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); }
Méthode de comparaison de deux objets :
//+------------------------------------------------------------------+ //| Compare two objects | //+------------------------------------------------------------------+ 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 ); } }
Cette méthode permet de comparer les paramètres de deux objets selon l'un des trois critères de comparaison suivants : par numéro de colonne, par numéro de ligne et simultanément par numéro de ligne et de colonne.
Cette méthode est nécessaire pour pouvoir trier les lignes du tableau en fonction des valeurs des colonnes du tableau. Cette question sera traitée par le Contrôleur dans des articles ultérieurs.
Méthode d'enregistrement des propriétés des cellules dans un fichier :
//+------------------------------------------------------------------+ //| Save to file | //+------------------------------------------------------------------+ bool CTableCell::Save(const int file_handle) { //--- Check the handle if(file_handle==INVALID_HANDLE) return(false); //--- Save data start marker - 0xFFFFFFFFFFFFFFFF if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long)) return(false); //--- Save the object type if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE) return(false); //--- Save the data type if(::FileWriteInteger(file_handle,this.m_datatype,INT_VALUE)!=INT_VALUE) return(false); //--- Save the object type in the cell if(::FileWriteInteger(file_handle,this.m_object_type,INT_VALUE)!=INT_VALUE) return(false); //--- Save the row index if(::FileWriteInteger(file_handle,this.m_row,INT_VALUE)!=INT_VALUE) return(false); //--- Save the column index if(::FileWriteInteger(file_handle,this.m_col,INT_VALUE)!=INT_VALUE) return(false); //--- Maintain the accuracy of data representation if(::FileWriteInteger(file_handle,this.m_digits,INT_VALUE)!=INT_VALUE) return(false); //--- Save date/time display flags if(::FileWriteInteger(file_handle,this.m_time_flags,INT_VALUE)!=INT_VALUE) return(false); //--- Save the color name display flag if(::FileWriteInteger(file_handle,this.m_color_flag,INT_VALUE)!=INT_VALUE) return(false); //--- Save the edited cell flag if(::FileWriteInteger(file_handle,this.m_editable,INT_VALUE)!=INT_VALUE) return(false); //--- Save the value if(::FileWriteStruct(file_handle,this.m_datatype_value)!=sizeof(this.m_datatype_value)) return(false); //--- All is successful return true; }
Après avoir écrit le marqueur de données de départ et le type d'objet dans le fichier, toutes les propriétés de la cellule sont sauvegardées à leur tour. L'union doit être sauvegardée sous forme de structure à l'aide de FileWriteStruct().
Méthode de chargement des propriétés des cellules à partir d'un fichier :
//+------------------------------------------------------------------+ //| Load from file | //+------------------------------------------------------------------+ bool CTableCell::Load(const int file_handle) { //--- Check the handle if(file_handle==INVALID_HANDLE) return(false); //--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return(false); //--- Load the object type if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type()) return(false); //--- Load the data type this.m_datatype=(ENUM_DATATYPE)::FileReadInteger(file_handle,INT_VALUE); //--- Load the object type in the cell this.m_object_type=(ENUM_OBJECT_TYPE)::FileReadInteger(file_handle,INT_VALUE); //--- Load the row index this.m_row=::FileReadInteger(file_handle,INT_VALUE); //--- Load the column index this.m_col=::FileReadInteger(file_handle,INT_VALUE); //--- Load the precision of the data representation this.m_digits=::FileReadInteger(file_handle,INT_VALUE); //--- Load date/time display flags this.m_time_flags=::FileReadInteger(file_handle,INT_VALUE); //--- Load the color name display flag this.m_color_flag=::FileReadInteger(file_handle,INT_VALUE); //--- Load the edited cell flag this.m_editable=::FileReadInteger(file_handle,INT_VALUE); //--- Load the value if(::FileReadStruct(file_handle,this.m_datatype_value)!=sizeof(this.m_datatype_value)) return(false); //--- All is successful return true; }
Après avoir lu et vérifié les marqueurs de début de données et le type d'objet, toutes les propriétés de l'objet sont chargées à tour de rôle dans le même ordre que celui dans lequel elles ont été sauvegardées. Les unions sont lues à l'aide de FileReadStruct().
Méthode qui renvoie la description de l'objet :
//+------------------------------------------------------------------+ //| Return the object description | //+------------------------------------------------------------------+ 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())); }
Ici, une ligne est créée à partir de certains paramètres de la cellule et renvoyée, par exemple, pour le type double, sous ce format :
Table Cell: Row 2, Col 2, Uneditable <double>Value: 0.00
Méthode qui produit une description de l'objet dans le journal :
//+------------------------------------------------------------------+ //| Display the object description in the journal | //+------------------------------------------------------------------+ void CTableCell::Print(void) { ::Print(this.Description()); }
Ici, la description de l'objet est simplement écrite dans le journal.
//+------------------------------------------------------------------+ //| Table row class | //+------------------------------------------------------------------+ class CTableRow : public CObject { protected: CTableCell m_cell_tmp; // Cell object to search in the list CListObj m_list_cells; // List of cells uint m_index; // Row index //--- Add the specified cell to the end of the list bool AddNewCell(CTableCell *cell); public: //--- (1) Set and (2) return the row index void SetIndex(const uint index) { this.m_index=index; } uint Index(void) const { return this.m_index; } //--- Set the row and column positions to all cells void CellsPositionUpdate(void); //--- Create a new cell and add it to the end of the list 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); //--- Return (1) the cell by index and (2) the number of cells CTableCell *GetCell(const uint index) { return this.m_list_cells.GetNodeAtIndex(index); } uint CellsTotal(void) const { return this.m_list_cells.Total(); } //--- Set the value to the specified cell 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) assign to a cell and (2) remove an assigned object from the cell void CellAssignObject(const uint index,CObject *object); void CellUnassignObject(const uint index); //--- (1) Delete and (2) move the cell bool CellDelete(const uint index); bool CellMoveTo(const uint cell_index, const uint index_to); //--- Reset the data of the row cells void ClearData(void); //--- (1) Return and (2) display the object description in the journal string Description(void); void Print(const bool detail, const bool as_table=false, const int cell_width=10); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(OBJECT_TYPE_TABLE_ROW); } //--- Constructors/destructor CTableRow(void) : m_index(0) {} CTableRow(const uint index) : m_index(index) {} ~CTableRow(void){} };
3. Classe de ligne du tableau
Une ligne de tableau est essentiellement une liste chaînée de cellules. La classe de ligne doit permettre d'ajouter, de supprimer et de réorganiser les cellules de la liste à un nouvel emplacement. La classe doit disposer d'un ensemble minimum de méthodes pour gérer les cellules de la liste.
Continuons à écrire le code dans le même fichier. Une seule variable est disponible dans les paramètres de classe : l'indice de ligne dans le tableau. Toutes les autres méthodes permettent de travailler avec les propriétés d'une ligne et avec une liste de ses cellules.
Considérons les méthodes de classe.
Méthode de comparaison de deux lignes de tableau :
//+------------------------------------------------------------------+ //| Compare two objects | //+------------------------------------------------------------------+ 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); }
Étant donné que les lignes ne peuvent être comparées qu'en fonction de leur seul paramètre — l'index de la ligne — cette comparaison est mise en œuvre ici. Cette méthode sera nécessaire pour trier les lignes du tableau.
Méthodes surchargées pour créer des cellules avec différents types de données stockées :
//+------------------------------------------------------------------+ //| Create a new double cell and add it to the end of the list | //+------------------------------------------------------------------+ CTableCell *CTableRow::CreateNewCell(const double value) { //--- Create a new cell object storing a value of double type 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; } //--- Add the created cell to the end of the list if(!this.AddNewCell(cell)) { delete cell; return NULL; } //--- Return the pointer to the object return cell; } //+------------------------------------------------------------------+ //| Create a new long cell and add it to the end of the list | //+------------------------------------------------------------------+ CTableCell *CTableRow::CreateNewCell(const long value) { //--- Create a new cell object storing a long value 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; } //--- Add the created cell to the end of the list if(!this.AddNewCell(cell)) { delete cell; return NULL; } //--- Return the pointer to the object return cell; } //+------------------------------------------------------------------+ //| Create a new datetime cell and add it to the end of the list | //+------------------------------------------------------------------+ CTableCell *CTableRow::CreateNewCell(const datetime value) { //--- Create a new cell object storing a value of datetime type 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; } //--- Add the created cell to the end of the list if(!this.AddNewCell(cell)) { delete cell; return NULL; } //--- Return the pointer to the object return cell; } //+------------------------------------------------------------------+ //| Create a new color cell and add it to the end of the list | //+------------------------------------------------------------------+ CTableCell *CTableRow::CreateNewCell(const color value) { //--- Create a new cell object storing a value of color type 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; } //--- Add the created cell to the end of the list if(!this.AddNewCell(cell)) { delete cell; return NULL; } //--- Return the pointer to the object return cell; } //+------------------------------------------------------------------+ //| Create a new string cell and add it to the end of the list | //+------------------------------------------------------------------+ CTableCell *CTableRow::CreateNewCell(const string value) { //--- Create a new cell object storing a value of string type 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; } //--- Add the created cell to the end of the list if(!this.AddNewCell(cell)) { delete cell; return NULL; } //--- Return the pointer to the object return cell; }
Cinq méthodes pour créer une nouvelle cellule (double, long, datetime, color, string) et l'ajouter à la fin de la liste. Les autres paramètres du format de sortie des données dans la cellule sont définis avec des valeurs par défaut. Ils peuvent être modifiés après la création de la cellule. Tout d'abord, un nouvel objet cellule est créé, puis ajouté à la fin de la liste. Si l'objet n'a pas été créé, il est supprimé pour éviter les fuites de mémoire.
Méthode qui ajoute une cellule à la fin de la liste :
//+------------------------------------------------------------------+ //| Add a cell to the end of the list | //+------------------------------------------------------------------+ bool CTableRow::AddNewCell(CTableCell *cell) { //--- If an empty object is passed, report it and return 'false' if(cell==NULL) { ::PrintFormat("%s: Error. Empty CTableCell object passed",__FUNCTION__); return false; } //--- Set the cell index in the list and add the created cell to the end of the list 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; } //--- Successful return true; }
Toute nouvelle cellule est toujours ajoutée à la fin de la liste. Elle peut ensuite être déplacée à la bonne position avec les méthodes de la classe de modèle de tableau, qui sera créée ultérieurement.
Méthodes surchargées pour définir des valeurs dans la cellule spécifiée :
//+------------------------------------------------------------------+ //| Set the double value to the specified cell | //+------------------------------------------------------------------+ void CTableRow::CellSetValue(const uint index,const double value) { //--- Get the required cell from the list and set a new value into it CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.SetValue(value); } //+------------------------------------------------------------------+ //| Set a long value to the specified cell | //+------------------------------------------------------------------+ void CTableRow::CellSetValue(const uint index,const long value) { //--- Get the required cell from the list and set a new value into it CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.SetValue(value); } //+------------------------------------------------------------------+ //| Set the datetime value to the specified cell | //+------------------------------------------------------------------+ void CTableRow::CellSetValue(const uint index,const datetime value) { //--- Get the required cell from the list and set a new value into it CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.SetValue(value); } //+------------------------------------------------------------------+ //| Set the color value to the specified cell | //+------------------------------------------------------------------+ void CTableRow::CellSetValue(const uint index,const color value) { //--- Get the required cell from the list and set a new value into it CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.SetValue(value); } //+------------------------------------------------------------------+ //| Set a string value to the specified cell | //+------------------------------------------------------------------+ void CTableRow::CellSetValue(const uint index,const string value) { //--- Get the required cell from the list and set a new value into it CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.SetValue(value); }
En utilisant l'index, nous obtenons la cellule requise dans la liste et lui attribuons une valeur. Si la cellule n'est pas modifiable, la méthode SetValue() de l'objet cellule ne définira pour la cellule que le type de valeur à définir. La valeur elle-même ne sera pas définie.
Méthode qui affecte un objet à une cellule :
//+------------------------------------------------------------------+ //| Assign an object to the cell | //+------------------------------------------------------------------+ void CTableRow::CellAssignObject(const uint index,CObject *object) { //--- Get the required cell from the list and set a pointer to the object into it CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.AssignObject(object); }
Nous obtenons un objet cellule par son index et utilisons sa méthode AssignObject() pour assigner un pointeur sur l'objet à la cellule.
Méthode permettant d'annuler l'affectation d'un objet à une cellule :
//+------------------------------------------------------------------+ //| Cancel the assigned object for the cell | //+------------------------------------------------------------------+ void CTableRow::CellUnassignObject(const uint index) { //--- Get the required cell from the list and cancel the pointer to the object and its type in it CTableCell *cell=this.GetCell(index); if(cell!=NULL) cell.UnassignObject(); }
Nous récupérons l'objet cellule par son index et utilisons sa méthode UnassignObject() pour supprimer le pointeur sur l'objet assigné à la cellule.
Méthode qui supprime une cellule :
//+------------------------------------------------------------------+ //| Delete a cell | //+------------------------------------------------------------------+ bool CTableRow::CellDelete(const uint index) { //--- Delete a cell in the list by index if(!this.m_list_cells.Delete(index)) return false; //--- Update the indices for the remaining cells in the list this.CellsPositionUpdate(); return true; }
La méthode Delete() de la classe CList permet de supprimer la cellule de la liste. Une fois qu'une cellule a été supprimée de la liste, les index des cellules restantes sont modifiés. La méthode CellsPositionUpdate() permet de mettre à jour les index de toutes les cellules restantes de la liste.
Méthode qui déplace une cellule à la position spécifiée :
//+------------------------------------------------------------------+ //| Moves the cell to the specified position | //+------------------------------------------------------------------+ bool CTableRow::CellMoveTo(const uint cell_index,const uint index_to) { //--- Select the desired cell by index in the list, turning it into the current one CTableCell *cell=this.GetCell(cell_index); //--- Move the current cell to the specified position in the list if(cell==NULL || !this.m_list_cells.MoveToIndex(index_to)) return false; //--- Update the indices of all cells in the list this.CellsPositionUpdate(); return true; }
Pour que la classe CList puisse agir sur un objet, cet objet de la liste doit être l'objet courant. Il devient l’objet courant lorsqu'il est sélectionné par exemple. C'est pourquoi nous commençons par récupérer l'objet cellule de la liste par son index. La cellule devient la cellule courante, puis, en utilisant la méthode MoveToIndex() de la classe CList, nous déplaçons l'objet à la position requise dans la liste. Après avoir déplacé avec succès un objet vers une nouvelle position, les index des objets restants doivent être ajustés, ce qui est fait à l'aide de la méthode CellsPositionUpdate().
Méthode qui définit la position des lignes et des colonnes pour toutes les cellules de la liste :
//+------------------------------------------------------------------+ //| Set the row and column positions to all cells | //+------------------------------------------------------------------+ void CTableRow::CellsPositionUpdate(void) { //--- In the loop through all cells in the list for(int i=0;i<this.m_list_cells.Total();i++) { //--- get the next cell and set the row and column indices in it CTableCell *cell=this.GetCell(i); if(cell!=NULL) cell.SetPositionInTable(this.Index(),this.m_list_cells.IndexOf(cell)); } }
La classe CList permet de trouver l'index de l'objet courant dans la liste. Pour ce faire, l'objet doit être sélectionné. Ici, nous parcourons en boucle tous les objets cellules de la liste, nous sélectionnons chacun d'entre eux et nous déterminons son indice à l'aide de la méthode IndexOf() de la classe CList. L'indice de la ligne et l'indice de la cellule trouvée sont attribués à l'objet cellule à l'aide de sa méthode SetPositionInTable().
Méthode qui réinitialise les données des cellules de la ligne :
//+------------------------------------------------------------------+ //| Reset the cell data of a row | //+------------------------------------------------------------------+ void CTableRow::ClearData(void) { //--- In the loop through all cells in the list for(uint i=0;i<this.CellsTotal();i++) { //--- get the next cell and set an empty value to it CTableCell *cell=this.GetCell(i); if(cell!=NULL) cell.ClearData(); } }
Dans la boucle, réinitialisez chaque cellule suivante de la liste à l'aide de la méthode ClearData() de l'objet cellule. Pour les données de type string, une ligne vide est écrite dans la cellule, et pour les données numériques, zéro est écrit.
Méthode qui renvoie la description de l'objet :
//+------------------------------------------------------------------+ //| Return the object description | //+------------------------------------------------------------------+ string CTableRow::Description(void) { return(::StringFormat("%s: Position %u, Cells total: %u", TypeDescription((ENUM_OBJECT_TYPE)this.Type()),this.Index(),this.CellsTotal())); }
Une ligne est collectée à partir des propriétés et des données de l'objet et renvoyée dans le format suivant, par exemple :
Table Row: Position 1, Cells total: 4:
Méthode qui produit une description de l'objet dans le journal :
//+------------------------------------------------------------------+ //| Display the object description in the journal | //+------------------------------------------------------------------+ void CTableRow::Print(const bool detail, const bool as_table=false, const int cell_width=10) { //--- Number of cells int total=(int)this.CellsTotal(); //--- If the output is in tabular form string res=""; if(as_table) { //--- create a table row from the values of all cells 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()); } //--- Display a row in the journal ::Print(res); return; } //--- Display the header as a row description ::Print(this.Description()+(detail ? ":" : "")); //--- If detailed description if(detail) { //--- The output is not in tabular form //--- In the loop through the list of cells in the row for(int i=0; i<total; i++) { //--- get the current cell and add its description to the final row CTableCell *cell=this.GetCell(i); if(cell!=NULL) res+=" "+cell.Description()+(i<total-1 ? "\n" : ""); } //--- Send the row created in the loop to the journal ::Print(res); } }
Pour l'affichage de données non tabulaires dans le journal, l'en-tête est d'abord affiché dans le journal en tant que description de ligne. Ensuite, si l'indicateur d'affichage détaillé est activé, les descriptions de chaque cellule sont affichées dans le journal, en boucle dans la liste des cellules.
Par conséquent, l'affichage détaillé d'une ligne de tableau dans le journal ressemble à ceci, par exemple (pour une vue non tabulaire) :
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
Dans le cas d'un affichage sous forme de tableau, le résultat sera, par exemple, le suivant :
| Row 0 | 0 | 1 | 2 | 3 |
Méthode permettant d'enregistrer une ligne de tableau dans un fichier :
//+------------------------------------------------------------------+ //| Save to file | //+------------------------------------------------------------------+ bool CTableRow::Save(const int file_handle) { //--- Check the handle if(file_handle==INVALID_HANDLE) return(false); //--- Save data start marker - 0xFFFFFFFFFFFFFFFF if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long)) return(false); //--- Save the object type if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE) return(false); //--- Save the index if(::FileWriteInteger(file_handle,this.m_index,INT_VALUE)!=INT_VALUE) return(false); //--- Save the list of cells if(!this.m_list_cells.Save(file_handle)) return(false); //--- Successful return true; }
Enregistrez les marqueurs de début de données, puis le type d'objet. Il s'agit de l'en-tête standard de chaque objet du fichier. Une entrée dans le fichier des propriétés de l'objet suit ensuite. Ici, l'index des lignes est enregistré dans le fichier, puis la liste des cellules.
Méthode de chargement de la ligne à partir d'un fichier :
//+------------------------------------------------------------------+ //| Load from file | //+------------------------------------------------------------------+ bool CTableRow::Load(const int file_handle) { //--- Check the handle if(file_handle==INVALID_HANDLE) return(false); //--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return(false); //--- Load the object type if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type()) return(false); //--- Load the index this.m_index=::FileReadInteger(file_handle,INT_VALUE); //--- Load the list of cells if(!this.m_list_cells.Load(file_handle)) return(false); //--- Successful return true; }
Ici, tout est dans le même ordre que lors de l'enregistrement. Tout d'abord, le marqueur de début de données et le type d'objet sont chargés et vérifiés. L'index de la ligne et la liste complète des cellules sont ensuite chargés.
4. Classe de modèles de tableaux
Dans sa forme la plus simple, le modèle de tableau est une liste chaînée de lignes, qui contiennent à leur tour des listes chaînées de cellules. Notre modèle, que nous créons aujourd'hui, recevra en entrée un tableau bidimensionnel de l'un des cinq types suivants (double, long, datetime, color, string) et construira un tableau virtuel à partir de ce tableau. Nous étendrons également cette classe afin d'accepter d'autres arguments pour créer des tableaux à partir d'autres données d'entrée. Le même modèle sera considéré comme le modèle par défaut.
Continuons à écrire le code dans le même fichier \MQL5\Scripts\TableModel\TableModelTest.mq5.
La classe de modèle de tableau est essentiellement une liste chaînée de lignes avec des méthodes pour gérer les lignes, les colonnes et les cellules. Ecrivez le corps de la classe avec toutes les variables et les méthodes, puis examinez les méthodes déclarées de la classe :
//+------------------------------------------------------------------+ //| Table model class | //+------------------------------------------------------------------+ class CTableModel : public CObject { protected: CTableRow m_row_tmp; // Row object to search in the list CListObj m_list_rows; // List of table rows //--- Create a table model from a two-dimensional array template<typename T> void CreateTableModel(T &array[][]); //--- Return the correct data type ENUM_DATATYPE GetCorrectDatatype(string type_name) { return ( //--- Integer value 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 : //--- Real value type_name=="float"|| type_name=="double" ? TYPE_DOUBLE : //--- Date/time value type_name=="datetime" ? TYPE_DATETIME : //--- Color value type_name=="color" ? TYPE_COLOR : /*--- String value */ TYPE_STRING ); } //--- Create and add a new empty string to the end of the list CTableRow *CreateNewEmptyRow(void); //--- Add a string to the end of the list bool AddNewRow(CTableRow *row); //--- Set the row and column positions to all table cells void CellsPositionUpdate(void); public: //--- Return (1) cell, (2) row by index, number (3) of rows, cells (4) in the specified row and (5) in the table 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); //--- Set (1) value, (2) precision, (3) time display flags and (4) color name display flag to the specified cell 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) Assign and (2) cancel the object in the cell void CellAssignObject(const uint row, const uint col,CObject *object); void CellUnassignObject(const uint row, const uint col); //--- (1) Delete and (2) move the cell bool CellDelete(const uint row, const uint col); bool CellMoveTo(const uint row, const uint cell_index, const uint index_to); //--- (1) Return and (2) display the cell description and (3) the object assigned to the cell string CellDescription(const uint row, const uint col); void CellPrint(const uint row, const uint col); CObject *CellGetObject(const uint row, const uint col); public: //--- Create a new string and (1) add it to the end of the list, (2) insert to the specified list position CTableRow *RowAddNew(void); CTableRow *RowInsertNewTo(const uint index_to); //--- (1) Remove or (2) relocate the row, (3) clear the row data bool RowDelete(const uint index); bool RowMoveTo(const uint row_index, const uint index_to); void RowResetData(const uint index); //--- (1) Return and (2) display the row description in the journal string RowDescription(const uint index); void RowPrint(const uint index,const bool detail); //--- (1) Remove or (2) relocate the column, (3) clear the column data bool ColumnDelete(const uint index); bool ColumnMoveTo(const uint row_index, const uint index_to); void ColumnResetData(const uint index); //--- (1) Return and (2) display the table description in the journal string Description(void); void Print(const bool detail); void PrintTable(const int cell_width=10); //--- (1) Clear the data, (2) destroy the model void ClearData(void); void Destroy(void); //--- Virtual methods of (1) comparing, (2) saving to file, (3) loading from file, (4) object type virtual int Compare(const CObject *node,const int mode=0) const; virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); virtual int Type(void) const { return(OBJECT_TYPE_TABLE_MODEL); } //--- Constructors/destructor 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){} };
En principe, il n'existe qu'un seul objet pour une liste chaînée de lignes de tableau et des méthodes pour gérer les lignes, les cellules et les colonnes. Et des constructeurs qui acceptent différents types de tableaux bidimensionnels.
Un tableau est transmis au constructeur de la classe et une méthode de création d'un modèle de table est appelée :
//+------------------------------------------------------------------+ //| Create the table model from a two-dimensional array | //+------------------------------------------------------------------+ template<typename T> void CTableModel::CreateTableModel(T &array[][]) { //--- Get the number of table rows and columns from the array properties int rows_total=::ArrayRange(array,0); int cols_total=::ArrayRange(array,1); //--- In a loop by row indices for(int r=0; r<rows_total; r++) { //--- create a new empty row and add it to the end of the list of rows CTableRow *row=this.CreateNewEmptyRow(); //--- If a row is created and added to the list, if(row!=NULL) { //--- In the loop by the number of cells in a row, //--- create all the cells, adding each new one to the end of the list of cells in the row for(int c=0; c<cols_total; c++) row.CreateNewCell(array[r][c]); } } }
La logique de la méthode est expliquée dans les commentaires. La première dimension du tableau correspond aux lignes du tableau, la seconde aux cellules de chaque ligne. Lors de la création de cellules, celles-ci utilisent le même type de données que le type de tableau transmis à la méthode.
Nous pouvons créer plusieurs modèles de tableaux, dont les cellules stockent initialement différents types de données (double, long, datetime, color , et string). Plus tard, après avoir créé le modèle de tableau, les types de données stockées dans les cellules peuvent être modifiés.
Méthode qui crée une nouvelle ligne vide et l'ajoute à la fin de la liste :
//+------------------------------------------------------------------+ //| Create a new empty string and add it to the end of the list | //+------------------------------------------------------------------+ CTableRow *CTableModel::CreateNewEmptyRow(void) { //--- Create a new row object 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; } //--- If failed to add the row to the list, remove the newly created object and return NULL if(!this.AddNewRow(row)) { delete row; return NULL; } //--- Success - return the pointer to the created object return row; }
La méthode crée un nouvel objet de la classe CTableRow et l'ajoute à la fin de la liste des lignes à l'aide de la méthode AddNewRow(). En cas d'erreur, le nouvel objet créé est supprimé et NULL est renvoyé. En cas de succès, la méthode renvoie un pointeur sur la ligne nouvellement ajoutée à la liste.
Méthode qui ajoute un objet ligne à la fin de la liste :
//+------------------------------------------------------------------+ //| Add a row to the end of the list | //+------------------------------------------------------------------+ bool CTableModel::AddNewRow(CTableRow *row) { //--- If an empty object is passed, report this and return 'false' if(row==NULL) { ::PrintFormat("%s: Error. Empty CTableRow object passed",__FUNCTION__); return false; } //--- Set the row index in the list and add it to the end of the list 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; } //--- Successful return true; }
Les deux méthodes mentionnées ci-dessus se trouvent dans la section protégée de la classe, fonctionnent par paires et sont utilisées en interne lors de l'ajout de nouvelles lignes au tableau.
Méthode permettant de créer une nouvelle ligne et de l'ajouter à la fin de la liste :
//+------------------------------------------------------------------+ //| Create a new string and add it to the end of the list | //+------------------------------------------------------------------+ CTableRow *CTableModel::RowAddNew(void) { //--- Create a new empty row and add it to the end of the list of rows CTableRow *row=this.CreateNewEmptyRow(); if(row==NULL) return NULL; //--- Create cells equal to the number of cells in the first row for(uint i=0;i<this.CellsInRow(0);i++) row.CreateNewCell(0.0); row.ClearData(); //--- Success - return the pointer to the created object return row; }
Il s'agit d'une méthode publique. Elle est utilisée pour ajouter une nouvelle ligne avec des cellules au tableau. Le nombre de cellules pour la ligne créée est tiré de la toute première ligne du tableau.
Méthode permettant de créer et d'ajouter une nouvelle ligne à la position spécifiée dans la liste :
//+------------------------------------------------------------------+ //| Create and add a new string to the specified position in the list| //+------------------------------------------------------------------+ CTableRow *CTableModel::RowInsertNewTo(const uint index_to) { //--- Create a new empty row and add it to the end of the list of rows CTableRow *row=this.CreateNewEmptyRow(); if(row==NULL) return NULL; //--- Create cells equal to the number of cells in the first row for(uint i=0;i<this.CellsInRow(0);i++) row.CreateNewCell(0.0); row.ClearData(); //--- Shift the row to index_to this.RowMoveTo(this.m_list_rows.IndexOf(row),index_to); //--- Success - return the pointer to the created object return row; }
Il est parfois nécessaire d'insérer une nouvelle ligne non pas à la fin de la liste des lignes, mais entre les lignes existantes. Cette méthode crée d'abord une nouvelle ligne à la fin de la liste, la remplit de cellules, les efface, puis déplace la ligne à la position souhaitée.
Méthode qui définit des valeurs dans la cellule spécifiée :
//+------------------------------------------------------------------+ //| Set the value to the specified cell | //+------------------------------------------------------------------+ template<typename T> void CTableModel::CellSetValue(const uint row,const uint col,const T value) { //--- Get a cell by row and column indices CTableCell *cell=this.GetCell(row,col); if(cell==NULL) return; //--- Get the correct type of the data being set (double, long, datetime, color, string) ENUM_DATATYPE type=this.GetCorrectDatatype(typename(T)); //--- Depending on the data type, we call the corresponding //--- cell method for setting the value, explicitly specifying the required type 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; } }
Il faut d'abord obtenir un pointeur sur la cellule souhaitée grâce aux coordonnées de sa ligne et de sa colonne, puis lui attribuer une valeur. Quelle que soit la valeur transmise à la méthode pour l'installer dans la cellule, la méthode ne sélectionnera que le type correct pour l'installation : double, long, datetime, color ou string.
Méthode qui définit la précision de l'affichage des données dans la cellule spécifiée :
//+------------------------------------------------------------------+ //| Set the precision of data display in the specified cell | //+------------------------------------------------------------------+ void CTableModel::CellSetDigits(const uint row,const uint col,const int digits) { //--- Get a cell by row and column indices and //--- call its corresponding method to set the value CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.SetDigits(digits); }
La méthode n'est pertinente que pour les cellules qui stockent le type de valeur réelle. Elle permet de spécifier le nombre de décimales de la valeur affichée par la cellule.
Méthode qui définit les drapeaux d'affichage de l'heure dans la cellule spécifiée :
//+------------------------------------------------------------------+ //| Set the time display flags to the specified cell | //+------------------------------------------------------------------+ void CTableModel::CellSetTimeFlags(const uint row,const uint col,const uint flags) { //--- Get a cell by row and column indices and //--- call its corresponding method to set the value CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.SetDatetimeFlags(flags); }
Concerne les cellules affichant des valeurs datetime. Définissez le format d'affichage de l'heure par cellule (l'un des formats TIME_DATE|TIME_MINUTES|TIME_SECONDS, ou une combinaison de ceux-ci).
TIME_DATE donne le résultat " yyyy.mm.dd "
TIME_MINUTES donne le résultat " hh:mi "
TIME_SECONDS donne le résultat " hh:mi:ss "
Méthode qui définit les drapeaux d'affichage du nom de la couleur à la cellule spécifiée :
//+------------------------------------------------------------------+ //| Set the flag for displaying color names in the specified cell | //+------------------------------------------------------------------+ void CTableModel::CellSetColorNamesFlag(const uint row,const uint col,const bool flag) { //--- Get a cell by row and column indices and //--- call its corresponding method to set the value CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.SetColorNameFlag(flag); }
Ne concerne que les cellules affichant des valeurs de type color. Elle indique la nécessité d'afficher les noms des couleurs si la couleur stockée dans la cellule est présente dans la table des couleurs.
Méthode permettant d'affecter un objet à une cellule :
//+------------------------------------------------------------------+ //| Assign an object to a cell | //+------------------------------------------------------------------+ void CTableModel::CellAssignObject(const uint row,const uint col,CObject *object) { //--- Get a cell by row and column indices and //--- call its corresponding method to set the value CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.AssignObject(object); }
Méthode qui annule l'affectation d'un objet dans une cellule :
//+------------------------------------------------------------------+ //| Unassign an object from a cell | //+------------------------------------------------------------------+ void CTableModel::CellUnassignObject(const uint row,const uint col) { //--- Get a cell by row and column indices and //--- call its corresponding method to set the value CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.UnassignObject(); }
Les deux méthodes présentées ci-dessus permettent d'affecter un objet à une cellule ou de supprimer son affectation. L'objet doit être un descendant de la classe CObject. Dans le contexte des articles sur les tables, un objet peut être, par exemple, l'un des objets connus de l'énumération ENUM_OBJECT_TYPE. Pour l'instant, la liste ne contient que des cellules, des lignes et des modèles de tableaux. Les affecter à une cellule n'a pas de sens. Mais l'énumération s'étendra au fur et à mesure que nous écrirons des articles sur le composant View, où les contrôles seront créés. Il est alors opportun d'attribuer à une cellule, par exemple, le contrôle "liste déroulante".
Méthode qui supprime la cellule spécifiée :
//+------------------------------------------------------------------+ //| Delete a cell | //+------------------------------------------------------------------+ bool CTableModel::CellDelete(const uint row,const uint col) { //--- Get the row by index and return the result of deleting the cell from the list CTableRow *row_obj=this.GetRow(row); return(row_obj!=NULL ? row_obj.CellDelete(col) : false); }
La méthode récupère l'objet ligne par son index et appelle sa méthode pour supprimer la cellule spécifiée. Pour une seule cellule dans une seule ligne, la méthode n'a pas encore de sens, car elle réduira le nombre de cellules dans une seule ligne du tableau. Cela entraîne une désynchronisation des cellules par rapport aux lignes voisines. Jusqu'à présent, il n'existe pas de traitement d'une telle suppression, où il est nécessaire de "développer" la cellule voisine de la cellule supprimée jusqu'à la taille de deux cellules afin de ne pas perturber la structure du tableau. Toutefois, cette méthode est utilisée dans le cadre de la méthode de suppression des colonnes d'un tableau, où les cellules de toutes les lignes sont supprimées en une seule fois sans violer l'intégrité de l'ensemble du tableau.
Méthode de déplacement d'une cellule de tableau :
//+------------------------------------------------------------------+ //| Move the cell | //+------------------------------------------------------------------+ bool CTableModel::CellMoveTo(const uint row,const uint cell_index,const uint index_to) { //--- Get the row by index and return the result of moving the cell to a new position CTableRow *row_obj=this.GetRow(row); return(row_obj!=NULL ? row_obj.CellMoveTo(cell_index,index_to) : false); }
L'objet ligne est récupéré par son index et sa méthode est appelée pour supprimer la cellule spécifiée.
Méthode qui renvoie le nombre de cellules dans la ligne spécifiée :
//+------------------------------------------------------------------+ //| Return the number of cells in the specified row | //+------------------------------------------------------------------+ uint CTableModel::CellsInRow(const uint index) { CTableRow *row=this.GetRow(index); return(row!=NULL ? row.CellsTotal() : 0); }
L’objet ligne est récupéré par son index et le nombre de cellules qu'elle contient est renvoyé en appelant la méthode CellsTotal() sur la ligne.
Méthode qui renvoie le nombre de cellules dans le tableau :
//+------------------------------------------------------------------+ //| Return the number of cells in the table | //+------------------------------------------------------------------+ uint CTableModel::CellsTotal(void) { //--- count cells in a loop by rows (slow with a large number of rows) 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; }
La méthode parcourt toutes les lignes du tableau et ajoute le nombre de cellules de chaque ligne au résultat total, qui est renvoyé. Avec un grand nombre de lignes dans le tableau, ce comptage peut être lent. Une fois que toutes les méthodes affectant le nombre de cellules du tableau ont été créées, elles ne sont prises en compte que lorsque leur nombre change.
Méthode qui renvoie la cellule de tableau spécifiée :
//+------------------------------------------------------------------+ //| Return the specified table cell | //+------------------------------------------------------------------+ CTableCell *CTableModel::GetCell(const uint row,const uint col) { //--- get the row by index row and return the row cell by 'row' index by 'col' index CTableRow *row_obj=this.GetRow(row); return(row_obj!=NULL ? row_obj.GetCell(col) : NULL); }
La ligne est récupérée par son index et le pointeur sur la cellule par son index col est renvoyé en utilisant la méthode GetCell() de l'objet ligne.
Méthode qui renvoie la description de la cellule :
//+------------------------------------------------------------------+ //| Return the cell description | //+------------------------------------------------------------------+ string CTableModel::CellDescription(const uint row,const uint col) { CTableCell *cell=this.GetCell(row,col); return(cell!=NULL ? cell.Description() : ""); }
La ligne est récupérée par son index, puis la cellule est récupérée et sa description est renvoyée.
Méthode permettant d'afficher la description de la cellule dans le journal :
//+------------------------------------------------------------------+ //| Display a cell description in the journal | //+------------------------------------------------------------------+ void CTableModel::CellPrint(const uint row,const uint col) { //--- Get a cell by row and column index and return its description CTableCell *cell=this.GetCell(row,col); if(cell!=NULL) cell.Print(); }
Un pointeur sur la cellule est récupéré par les index de ligne et de colonne et, à l'aide de la méthode Print() de l'objet cellule, sa description est écrite dans le journal.
Méthode qui supprime la ligne spécifiée :
//+------------------------------------------------------------------+ //| Delete a row | //+------------------------------------------------------------------+ bool CTableModel::RowDelete(const uint index) { //--- Remove a string from the list by index if(!this.m_list_rows.Delete(index)) return false; //--- After deleting a row, be sure to update all indices of all table cells this.CellsPositionUpdate(); return true; }
La méthode Delete() de la classe CList permet de supprimer de la liste l'objet ligne par index. Après la suppression de la ligne, les index des lignes restantes et des cellules qu'elles contiennent ne correspondent pas à la réalité et doivent être ajustés à l'aide de la méthode CellsPositionUpdate().
Méthode qui déplace une ligne à la position spécifiée :
//+------------------------------------------------------------------+ //| Move a string to a specified position | //+------------------------------------------------------------------+ bool CTableModel::RowMoveTo(const uint row_index,const uint index_to) { //--- Get the row by index, turning it into the current one CTableRow *row=this.GetRow(row_index); //--- Move the current string to the specified position in the list if(row==NULL || !this.m_list_rows.MoveToIndex(index_to)) return false; //--- After moving a row, it is necessary to update all indices of all table cells this.CellsPositionUpdate(); return true; }
Dans la classe CList, de nombreuses méthodes travaillent avec l'objet liste en cours. Obtention d’un pointeur sur la ligne requise, en fait la ligne courante, et le déplace à la position requise à l'aide de la méthode MoveToIndex() de la classe CList. Après avoir déplacé la ligne vers une nouvelle position, il est nécessaire de mettre à jour les index des lignes restantes, ce que nous faisons à l'aide de la méthode CellsPositionUpdate().
Méthode qui définit la position des lignes et des colonnes pour toutes les cellules :
//+------------------------------------------------------------------+ //| Set the row and column positions to all cells | //+------------------------------------------------------------------+ void CTableModel::CellsPositionUpdate(void) { //--- In the loop by the list of rows for(int i=0;i<this.m_list_rows.Total();i++) { //--- get the next row CTableRow *row=this.GetRow(i); if(row==NULL) continue; //--- set the index, found by the IndexOf() method of the list, to the row row.SetIndex(this.m_list_rows.IndexOf(row)); //--- Update the row cell position indices row.CellsPositionUpdate(); } }
Parcours de la liste de toutes les lignes du tableau, sélection de chaque ligne suivante et définition correcte de son index, trouvé à l'aide de la méthode IndexOf() de la classe CList. Appel ensuite de la méthode CellsPositionUpdate() de la ligne, qui définit l'index correct pour chaque cellule de la ligne.
Méthode qui efface les données de toutes les cellules d'une ligne :
//+------------------------------------------------------------------+ //| Clear the row (only the data in the cells) | //+------------------------------------------------------------------+ void CTableModel::RowResetData(const uint index) { //--- Get a row from the list and clear the data of the row cells using the ClearData() method CTableRow *row=this.GetRow(index); if(row!=NULL) row.ClearData(); }
Chaque cellule de la ligne est réinitialisée à une valeur "vide". Pour l'instant, à des fins de simplification, la valeur vide pour les cellules numériques est zéro, mais cela sera modifié ultérieurement, car zéro est également une valeur qui doit être affichée dans la cellule. Et la réinitialisation d'une valeur implique l'affichage d'un champ cellulaire vide.
Méthode qui efface les données de toutes les cellules du tableau :
//+------------------------------------------------------------------+ //| Clear the table (data of all cells) | //+------------------------------------------------------------------+ void CTableModel::ClearData(void) { //--- Clear the data of each row in the loop through all the table rows for(uint i=0;i<this.RowsTotal();i++) this.RowResetData(i); }
Parcours de toutes les lignes du tableau et, pour chacune d'elles, appel de la méthode RowResetData() décrite ci-dessus.
Méthode qui renvoie la description de la ligne :
//+------------------------------------------------------------------+ //| Return the row description | //+------------------------------------------------------------------+ string CTableModel::RowDescription(const uint index) { //--- Get a row by index and return its description CTableRow *row=this.GetRow(index); return(row!=NULL ? row.Description() : ""); }
Un pointeur sur la ligne est récupéré par son index et sa description est retournée.
Méthode permettant d'afficher la description des lignes dans le journal :
//+------------------------------------------------------------------+ //| Display the row description in the journal | //+------------------------------------------------------------------+ void CTableModel::RowPrint(const uint index,const bool detail) { CTableRow *row=this.GetRow(index); if(row!=NULL) row.Print(detail); }
Un pointeur sur la ligne est récupéré et la méthode Print() de l'objet reçu est appelée.
Méthode qui supprime une colonne de tableau :
//+------------------------------------------------------------------+ //| Remove the column | //+------------------------------------------------------------------+ 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; }
Dans une boucle parcourant toutes les lignes du tableau, chaque ligne est récupérée et la cellule demandée est supprimée en fonction de l'indice de la colonne. Cette opération supprime toutes les cellules du tableau qui ont les mêmes index de colonne.
Méthode permettant de déplacer une colonne de tableau :
//+------------------------------------------------------------------+ //| Move the column | //+------------------------------------------------------------------+ 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; }
Dans une boucle parcourant toutes les lignes du tableau, chaque ligne est récupérée et la cellule demandée est déplacée vers une nouvelle position. Cette opération permet de déplacer toutes les cellules du tableau qui ont les mêmes index de colonne.
Méthode qui efface les données des cellules de la colonne :
//+------------------------------------------------------------------+ //| Clear the column data | //+------------------------------------------------------------------+ void CTableModel::ColumnResetData(const uint index) { //--- In a loop through all table rows for(uint i=0;i<this.RowsTotal();i++) { //--- get the cell with the column index from each row and clear it CTableCell *cell=this.GetCell(i, index); if(cell!=NULL) cell.ClearData(); } }
Dans une boucle parcourant toutes les lignes du tableau, chaque ligne est récupérée et les données dans la cellule voulue sont effacées. Cette opération permet d'effacer toutes les cellules du tableau qui ont les mêmes index de colonne.
Méthode qui renvoie la description de l'objet :
//+------------------------------------------------------------------+ //| Return the object description | //+------------------------------------------------------------------+ 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())); }
Une ligne est créée et renvoyée à partir de certains paramètres du modèle de tableau dans ce format :
Table Model: Rows 4, Cells in row 4, Cells Total 16:
Méthode qui produit une description de l'objet dans le journal :
//+------------------------------------------------------------------+ //| Display the object description in the journal | //+------------------------------------------------------------------+ void CTableModel::Print(const bool detail) { //--- Display the header in the journal ::Print(this.Description()+(detail ? ":" : "")); //--- If detailed description, if(detail) { //--- In a loop through all table rows for(uint i=0; i<this.RowsTotal(); i++) { //--- get the next row and display its detailed description to the journal CTableRow *row=this.GetRow(i); if(row!=NULL) row.Print(true,false); } } }
Tout d'abord, l'en-tête est imprimé en tant que description du modèle, puis, si l'indicateur de sortie détaillée est activé, les descriptions détaillées de toutes les lignes du modèle de tableau sont imprimées dans la boucle.
Méthode permettant d'afficher la description de l'objet dans le journal sous forme de tableau :
//+------------------------------------------------------------------+ //| Display the object description as a table in the journal | //+------------------------------------------------------------------+ void CTableModel::PrintTable(const int cell_width=10) { //--- Get the pointer to the first row (index 0) CTableRow *row=this.GetRow(0); if(row==NULL) return; //--- Create a table header row based on the number of cells in the first row of the table 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); } //--- Display the header row in the journal ::Print(res); //--- Iterate through all table rows and display them in tabular form for(uint i=0;i<this.RowsTotal();i++) { CTableRow *row=this.GetRow(i); if(row!=NULL) row.Print(true,true,cell_width); } }
Tout d'abord, en fonction du nombre de cellules dans la toute première ligne du tableau, l'en-tête du tableau est créé avec les noms des colonnes du tableau et imprimé dans le journal. Ensuite, toutes les lignes du tableau sont parcourues dans une boucle et chacune d'entre elles est imprimée sous forme de tableau.
Méthode qui détruit le modèle de tableau :
//+------------------------------------------------------------------+ //| Destroy the model | //+------------------------------------------------------------------+ void CTableModel::Destroy(void) { //--- Clear cell list m_list_rows.Clear(); }
La liste des lignes du tableau est simplement effacée et tous les objets sont détruits à l'aide de la méthode Clear() de la classe CList.
Méthode d'enregistrement du modèle de tableau dans un fichier :
//+------------------------------------------------------------------+ //| Save to file | //+------------------------------------------------------------------+ bool CTableModel::Save(const int file_handle) { //--- Check the handle if(file_handle==INVALID_HANDLE) return(false); //--- Save data start marker - 0xFFFFFFFFFFFFFFFF if(::FileWriteLong(file_handle,MARKER_START_DATA)!=sizeof(long)) return(false); //--- Save the object type if(::FileWriteInteger(file_handle,this.Type(),INT_VALUE)!=INT_VALUE) return(false); //--- Save the list of rows if(!this.m_list_rows.Save(file_handle)) return(false); //--- Successful return true; }
Après avoir enregistré le marqueur de début de données et le type de liste, la liste des lignes est enregistrée dans le fichier à l'aide de la méthode Save() de la classe CList.
Méthode de chargement d'un modèle de tableau à partir d'un fichier :
//+------------------------------------------------------------------+ //| Load from file | //+------------------------------------------------------------------+ bool CTableModel::Load(const int file_handle) { //--- Check the handle if(file_handle==INVALID_HANDLE) return(false); //--- Load and check the data start marker - 0xFFFFFFFFFFFFFFFF if(::FileReadLong(file_handle)!=MARKER_START_DATA) return(false); //--- Load the object type if(::FileReadInteger(file_handle,INT_VALUE)!=this.Type()) return(false); //--- Load the list of rows if(!this.m_list_rows.Load(file_handle)) return(false); //--- Successful return true; }
Après avoir chargé et vérifié le marqueur de début de données et le type de liste, la liste des lignes est chargé du fichier à l'aide de la méthode Load() de la classe CListObj, décrite au début de l'article.
Toutes les classes permettant de créer un modèle de tableau sont prêtes. Écrivons maintenant un script pour tester le fonctionnement du modèle.
Test du résultat
Continuez à écrire le code dans le même fichier. Ecrivez un script dans lequel nous créerons un tableau 4x4 bidimensionnel (4 lignes de 4 cellules chacune) avec le type long par exemple. Ensuite, ouvrez un fichier pour y écrire les données du tableau et chargez les données dans le tableau à partir du fichier. Créez un modèle de tableau et vérifiez le fonctionnement de certaines de ses méthodes.
Chaque fois que le tableau est modifié, nous enregistrons le résultat reçu à l'aide de la fonction TableModelPrint(), qui sélectionne la manière d'imprimer le modèle de tableau. Si la valeur de la macro PRINT_AS_TABLE est true, l'enregistrement est effectué à l'aide de la méthode PrintTable() de la classe CTableModel, si la valeur est false — à l'aide de la méthode Print() de la même classe.
Dans un script, créez un modèle de tableau, imprimez-le sous forme de tableau et enregistrez le modèle dans un fichier. Ensuite, ajoutez des lignes, supprimez des colonnes et modifiez les droits d'édition...
Téléchargez ensuite à nouveau la version initiale du tableau à partir du fichier et imprimez le résultat.
#define PRINT_AS_TABLE true // Display the model as a table //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Declare and fill the 4x4 array //--- Acceptable array types: 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}}; //--- Create a table model from the 4x4 long array created above CTableModel *tm=new CTableModel(array); //--- Leave if the model is not created if(tm==NULL) return; //--- Print the model in tabular form Print("The table model has been successfully created:"); tm.PrintTable(); //--- Delete the table model object delete tm; }
Nous obtenons ainsi le résultat suivant du script dans le journal :
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 |
Pour tester l'utilisation du modèle de tableau, l'ajout, la suppression et le déplacement de lignes et de colonnes, ainsi que l'utilisation d'un fichier, complétez le script :
#define PRINT_AS_TABLE true // Display the model as a table //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Declare and fill the 4x4 array //--- Acceptable array types: 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}}; //--- Create a table model from the 4x4 long array created above CTableModel *tm=new CTableModel(array); //--- Leave if the model is not created if(tm==NULL) return; //--- Print the model in tabular form Print("The table model has been successfully created:"); tm.PrintTable(); //--- Check handling files and the functionality of the table model //--- Open a file to write table model data into it int handle=FileOpen(MQLInfoString(MQL_PROGRAM_NAME)+".bin",FILE_READ|FILE_WRITE|FILE_BIN|FILE_COMMON); if(handle==INVALID_HANDLE) return; //--- Save the original created table to the file if(tm.Save(handle)) Print("\nThe table model has been successfully saved to file."); //--- Now insert a new row into the table at position 2 //--- Get the last cell of the created row and make it non-editable //--- Print the modified table model in the journal 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); } //--- Now delete the table column with index 1 and //--- print the resulting table model in the journal if(tm.ColumnDelete(1)) { Print("\nRemove column from position 1"); TableModelPrint(tm); } //--- When saving table data, the file pointer was shifted to the last set data //--- Place the pointer at the beginning of the file, load the previously saved original table and print it if(FileSeek(handle,0,SEEK_SET) && tm.Load(handle)) { Print("\nLoad the original table view from the file:"); TableModelPrint(tm); } //--- Close the open file and delete the table model object FileClose(handle); delete tm; } //+------------------------------------------------------------------+ //| Display the table model | //+------------------------------------------------------------------+ void TableModelPrint(CTableModel *tm) { if(PRINT_AS_TABLE) tm.PrintTable(); // Print the model as a table else tm.Print(true); // Print detailed table data }
Ce résultat apparaît dans le journal :
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 |
Si vous souhaitez éditer dans le journal les données qui ne sont pas sous forme de tableau à partir du modèle de tableau, utilisez false dans la macro PRINT_AS_TABLE :
#define PRINT_AS_TABLE false // Display the model as a table
Dans ce cas, le texte suivant s'affiche dans le journal :
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
Cette sortie fournit davantage d'informations de débogage. Par exemple, nous voyons ici que l'activation du drapeau d'interdiction de modification d'une cellule est affichée dans les journaux du programme.
Toutes les fonctionnalités mentionnées fonctionnent comme prévu, les lignes sont ajoutées, déplacées, les colonnes sont supprimées, nous pouvons modifier les propriétés des cellules et travailler avec des fichiers.
Conclusion
Il s'agit de la première version d'un modèle de tableau simple qui permet de créer un tableau à partir d'un tableau de données à 2 dimensions. Nous créerons par la suite d'autres modèles de tableaux spécialisés, nous créerons le composant View et, enfin, nous travaillerons pleinement avec des données tabulaires comme l'un des contrôles de l'interface graphique de l'utilisateur.
Le fichier du script créé aujourd'hui avec les classes incluses est joint à l'article. Vous pouvez le télécharger pour l'étudier vous-même.
Traduit du russe par MetaQuotes Ltd.
Article original : https://www.mql5.com/ru/articles/17653
Avertissement: Tous les droits sur ces documents sont réservés par MetaQuotes Ltd. La copie ou la réimpression de ces documents, en tout ou en partie, est interdite.
Cet article a été rédigé par un utilisateur du site et reflète ses opinions personnelles. MetaQuotes Ltd n'est pas responsable de l'exactitude des informations présentées, ni des conséquences découlant de l'utilisation des solutions, stratégies ou recommandations décrites.
Classes de table et d'en-tête basées sur un modèle de tableau dans MQL5 : Application du concept MVC
Passer à MQL5 Algo Forge (Partie 4) : Travailler avec les versions et les mises à jour
Les méthodes de William Gann (première partie) : Création de l'indicateur Angles de Gann
Passer à MQL5 Algo Forge (Partie 3) : Utiliser des dépôts externes dans vos propres projets
- Applications de trading gratuites
- Plus de 8 000 signaux à copier
- Actualités économiques pour explorer les marchés financiers
Vous acceptez la politique du site Web et les conditions d'utilisation
Lorsqu'une classe de SomeObject est chargée à partir d'un fichier en appelant la méthode Load() de ce même SomeObject, elle vérifie "est-ce que je me suis vraiment lu moi-même dans le fichier ? Si ce n'est pas le cas, cela signifie que quelque chose s'est mal passé et qu'il est inutile de poursuivre le chargement.
Ce que j'ai ici est une LISTE (CListObj) qui lit un type d'objet à partir d'un fichier. La liste ne sait pas ce qu'il y a (quel objet) dans le fichier. Mais elle doit connaître ce type d'objet pour le créer dans sa méthode CreateElement(). C'est pourquoi elle ne vérifie pas le type de l'objet chargé à partir du fichier. Après tout, il y aura une comparaison avec Type(), qui dans cette méthode renvoie le type d'une liste, pas d'un objet.
Merci, j'ai compris.
Je l'ai lu, puis relu.
c'est tout sauf un "modèle" en MVC. Certains ListStorage par exemple
Je m'interroge. Est-il possible d'obtenir un analogue des dataframes de python et de R de cette manière ? Il s'agit de tableaux où les différentes colonnes peuvent contenir des données de différents types (à partir d'un ensemble limité de types, mais incluant les chaînes de caractères).
C'est possible. Si nous parlons de différentes colonnes d'un tableau, alors dans l'implémentation décrite, chaque cellule du tableau peut avoir un type de données différent.