Grafisches Interface X: Sortieren, Neuerstellen der Tabelle und Steuerelemente der Zellen (build 11)
Inhalt
- Einführung
- Das Sortieren von Tabellen
- Hinzufügen und entfernen von Spalten und Zeilen
- Doppelklick der linken Maustaste
- Steuerelemente in einer Tabellenzelle
- Schlussfolgerung
Einführung
Der erste Artikel Grafische Interfaces I: Vorbereitung der Bibliotheksstruktur (Kapitel 1) beschreibt im Detail den Zweck der Bibliothek. Sie finden eine Liste von Artikeln mit Verweisen am Ende jeden Kapitels. Dort können Sie auch die komplette, aktuelle Version der Bibliothek zum derzeitigen Entwicklungsstand herunterladen. Die Dateien müssen im gleichen Verzeichnis wie das Archiv platziert werden.
Die Tabellendarstellung wird weiterentwickelt. Zählen wir ihre neuen Eigenschaften auf.
- Sortieren der Daten der Tabelle.
- Verwalten der Anzahl der Spalten und Zeilen: Hinzufügen und entfernen von Spalten und Zeilen zum angegebenen Index, vollständiges Löschen der Tabelle (es bleibt nur eine Spalte und eine Zeile); Neuerstellung der Tabelle (löscht die Tabelle und setzt neue Dimensionen).
- Erweiterung der Möglichkeiten des Nutzers des grafischen Interfaces: Der Doppelklick mit der linken Maustaste wurde hinzugefügt.
- Wir beginnen Steuerelementen, die Tabellenzellen zugewiesen werden können: Kontrollkästchen und Tasten.
Wenn Sie bereits ein grafisches Interface mit der Hilfe dieser Bibliothek erstellt haben und eine Tabelle des Typs CTable zur Anzeige von Daten verwenden, wird Ihnen empfohlen im Weiteren Tabellen des Typs CCanvasTable zu verwenden. Beginnend mit diesem Artikel sind sie ganz auf Augenhöhe mit Tabellen anderen Typs dieser Bibliothek und ja übertreffen sie in mancher Hinsicht.
Das Sortieren von Tabellen
Die meisten Methoden für die Sortierung der Tabellendaten sind die gleichen wie in CTable. Der Artikel Grafische Interfaces I: Vorbereitung der Bibliotheksstruktur (Kapitel 1) beschreibt, wie diese im Detail funktionieren. Hier werden die Änderungen und Ergänzungen bezüglich der Tabelle des Typs CCanvasTable nur kurz erwähnt.
Das Zeichnen der Icons zur Kennzeichnung sortierter Tabelle verlangt einen static Array vom Typ CTImage mit zwei Elementen. Es enthält die Pfade zu den Icons für die Anzeige der Sortierrichtung. Wenn der Nutzer keine eigene Icons benannt hat, werden standardmäßige Icons verwendet. Die Initialisierung mit Standardwerten und das Ausfüllen des Array mit den Icons übernimmt die Methode CCanvasTable::CreateHeaders() für das Erstellen der Kopfzeilen.
//+------------------------------------------------------------------+ //| Kasse des Erzeugens eine Tabellendarstellung | //+------------------------------------------------------------------+ class CCanvasTable : public CElement { private: //--- Icons als Zeichen der sortierten Daten CTImage m_sort_arrows[2]; //--- public: //--- Eintragen der Icons als Zeichen der sortierten Daten void SortArrowFileAscend(const string path) { m_sort_arrows[0].m_bmp_path=path; } void SortArrowFileDescend(const string path) { m_sort_arrows[1].m_bmp_path=path; } }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CCanvasTable::CCanvasTable(void) { ... //--- Initialisierung der Struktur der Sortiericons m_sort_arrows[0].m_bmp_path=""; m_sort_arrows[1].m_bmp_path=""; } //+------------------------------------------------------------------+ //| Erstellen der Spaltenköpfe | //+------------------------------------------------------------------+ bool CCanvasTable::CreateHeaders(void) { //--- Verlassen, wenn die Spaltenköpfe deaktiviert sind if(!m_show_headers) return(true); //--- Bilden des Objektnamens string name=CElementBase::ProgramName()+"_table_headers_"+(string)CElementBase::Id(); //--- Koordinaten int x =m_x+1; int y =m_y+1; //--- Definieren der Icons als Zeichen der Sortierfähigkeit der Tabelle if(m_sort_arrows[0].m_bmp_path=="") m_sort_arrows[0].m_bmp_path="::Images\\EasyAndFastGUI\\Controls\\SpinInc.bmp"; if(m_sort_arrows[1].m_bmp_path=="") m_sort_arrows[1].m_bmp_path="::Images\\EasyAndFastGUI\\Controls\\SpinDec.bmp"; //--- for(int i=0; i<2; i++) { ::ResetLastError(); if(!::ResourceReadImage(m_sort_arrows[i].m_bmp_path,m_sort_arrows[i].m_image_data, m_sort_arrows[i].m_image_width,m_sort_arrows[i].m_image_height)) { ::Print(__FUNCTION__," > error: ",::GetLastError()); } } //--- Erstellen des Objektes ::ResetLastError(); if(!m_headers.CreateBitmapLabel(m_chart_id,m_subwin,name,x,y,m_table_x_size,m_header_y_size,COLOR_FORMAT_ARGB_NORMALIZE)) { ::Print(__FUNCTION__," > Failed to create a canvas for drawing the table headers: ",::GetLastError()); return(false); } //--- Starten auf dem Chart //--- Festlegen der Eigenschaften //--- Koordinaten //--- Sichern der Größe //--- Abstände vom Rand des Panels //--- Sichern des Objektpointers //--- Setzen der Größe des sichtbaren Teils //--- Setzen des Rahmenabstandes im Bild für die X- und die Y-Achse ... return(true); }
Die Methode CCanvasTable::DrawSignSortedData() wird für das Zeichnen der Icons sortierter Arrays verwendet. Dies Element wird nur gezeichnet, wenn (1) Sortiermodus aktiviert ist und (2) das Sortieren der Tabelle wurde bereits durchgeführt. Die Abstände können mit den Methoden CCanvasTable::SortArrowXGap() und CCanvasTable::SortArrowYGap() gesteuert werden. Das Icon für eine aufsteigende Sortierung erhält den Index 0 0, eine absteigende – 1. Dann folgt die Doppelschleife zum Zeichnen des Icons. Dieses Thema wurde bereits detailliert besprochen.
class CCanvasTable : public CElement { private: //--- Abstand des Icons der sortierten Daten int m_sort_arrow_x_gap; int m_sort_arrow_y_gap; //--- public: //--- Abstand des Icons der sortierten Tabelle void SortArrowXGap(const int x_gap) { m_sort_arrow_x_gap=x_gap; } void SortArrowYGap(const int y_gap) { m_sort_arrow_y_gap=y_gap; } //--- private: //--- Zeichen des Icons für die Möglichkeit der Sortierung der Tabelle void DrawSignSortedData(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CCanvasTable::CCanvasTable(void) : m_sort_arrow_x_gap(20), m_sort_arrow_y_gap(6) { ... } //+------------------------------------------------------------------+ //| Zeichen des Icons für die Möglichkeit der Sortierung der Tabelle | //+------------------------------------------------------------------+ void CCanvasTable::DrawSignSortedData(void) { //--- Verlassen, wenn (1) Sortieren ist deaktiviert oder (2) noch nicht sortiert if(!m_is_sort_mode || m_is_sorted_column_index==WRONG_VALUE) return; //--- Berechnen der Koordinaten int x =m_columns[m_is_sorted_column_index].m_x2-m_sort_arrow_x_gap; int y =m_sort_arrow_y_gap; //--- Das ausgewählte Icon für die Sortierrichtung int image_index=(m_last_sort_direction==SORT_ASCEND)? 0 : 1; //--- Zeichnen for(uint ly=0,i=0; ly<m_sort_arrows[image_index].m_image_height; ly++) { for(uint lx=0; lx<m_sort_arrows[image_index].m_image_width; lx++,i++) { //--- Gibt es keine Farbe, gehe zum nächsten Pixel if(m_sort_arrows[image_index].m_image_data[i]<1) continue; //--- Abrufen der Farbe des Hintergrundes (des Spaltenkopfes) und der Farbe des jeweiligen Pixels des Icons uint background =m_headers.PixelGet(x+lx,y+ly); uint pixel_color =m_sort_arrows[image_index].m_image_data[i]; //--- Mischen der Farben uint foreground=::ColorToARGB(m_clr.BlendColors(background,pixel_color)); //--- Zeichnen der Pixel des darüber gelegten Icons m_headers.PixelSet(x+lx,y+ly,foreground); } } }
Die Methode CCanvasTable::Swap() tauscht die Werte in der Tabelle während des Sortierens. Der Unterschied ist nur, dass nicht nur der Text und/oder das Bild der Zelle bewegt werden muss, sondern auch eine große Anzahl von Zelleigenschaften. Der Einfachheit halber und um Duplikate zu entfernen, benötigen wir eine Hilfsmethode CCanvasTable::ImageCopy(), um die Bilder umzukopieren. Ihr werden zwei Arrays des Typs CTImage übergeben — der Empfänger und die Quelle der Daten. Der Index des kopierten Bildes wird als drittes Argument übergeben, da eine Zelle auch mehrere Bilder enthalten kann.
class CCanvasTable : public CElement { private: //--- Kopieren der Bilddaten von einem Array zum anderen void ImageCopy(CTImage &destination[],CTImage &source[],const int index); }; //+------------------------------------------------------------------+ //| Kopieren der Bilddaten von einem Array zum anderen | //+------------------------------------------------------------------+ void CCanvasTable::ImageCopy(CTImage &destination[],CTImage &source[],const int index) { //--- Kopieren der Pixel des Bildes ::ArrayCopy(destination[index].m_image_data,source[index].m_image_data); //--- Kopieren der Eigenschaften des Bildes destination[index].m_image_width =source[index].m_image_width; destination[index].m_image_height =source[index].m_image_height; destination[index].m_bmp_path =source[index].m_bmp_path; }
Unten ist der Code von CCanvasTable::Swap() aufgeführt. Vor dem Kopieren der Bilder muss zuerst überprüft werden, ob sie existieren, um, falls nicht, mit der nächsten Spalte fortzusetzen. Wenn eine der Zellen ein Bild enthält, dann werden sie mit der Methode CCanvasTable::ImageCopy() kopiert. Dieser Vorgang ist der gleiche wie beim Kopieren der anderen Eigenschaften der Zelle. Das bedeutet, wir sichern als erstes die Eigenschaften der ersten Zelle. Dann werden die Eigenschaften der zweiten Zelle in die erste Zelle kopiert. Schließlich werden die zuerst gesicherten Werte der ersten Zelle in die zweite Zelle kopiert.
class CCanvasTable : public CElement { private: //--- Tauschen der Werte der angegebenen Zellen void Swap(uint r1,uint r2); }; //+------------------------------------------------------------------+ //| Tauschen der Elemente | //+------------------------------------------------------------------+ void CCanvasTable::Swap(uint r1,uint r2) { //--- Iterieren über alle Spalten in einer Schleife for(uint c=0; c<m_columns_total; c++) { //--- Tauschen des Volltextes string temp_text =m_columns[c].m_rows[r1].m_full_text; m_columns[c].m_rows[r1].m_full_text =m_columns[c].m_rows[r2].m_full_text; m_columns[c].m_rows[r2].m_full_text =temp_text; //--- Tauschen des verkürzten Textes temp_text =m_columns[c].m_rows[r1].m_short_text; m_columns[c].m_rows[r1].m_short_text =m_columns[c].m_rows[r2].m_short_text; m_columns[c].m_rows[r2].m_short_text =temp_text; //--- Tauschen der Dezimalstellen uint temp_digits =m_columns[c].m_rows[r1].m_digits; m_columns[c].m_rows[r1].m_digits =m_columns[c].m_rows[r2].m_digits; m_columns[c].m_rows[r2].m_digits =temp_digits; //--- Tauschen der Textfarbe color temp_text_color =m_columns[c].m_rows[r1].m_text_color; m_columns[c].m_rows[r1].m_text_color =m_columns[c].m_rows[r2].m_text_color; m_columns[c].m_rows[r2].m_text_color =temp_text_color; //--- Tauschen des Index des gewählten Icons int temp_selected_image =m_columns[c].m_rows[r1].m_selected_image; m_columns[c].m_rows[r1].m_selected_image =m_columns[c].m_rows[r2].m_selected_image; m_columns[c].m_rows[r2].m_selected_image =temp_selected_image; //--- Prüfen, ob die Zelle eine Bild enthält int r1_images_total=::ArraySize(m_columns[c].m_rows[r1].m_images); int r2_images_total=::ArraySize(m_columns[c].m_rows[r2].m_images); //--- Gehen zur nächsten Spalte, wenn beide Zellen keine Bilder enthalten if(r1_images_total<1 && r2_images_total<1) continue; //--- Tauschen der Bilder CTImage r1_temp_images[]; //--- ::ArrayResize(r1_temp_images,r1_images_total); for(int i=0; i<r1_images_total; i++) ImageCopy(r1_temp_images,m_columns[c].m_rows[r1].m_images,i); //--- ::ArrayResize(m_columns[c].m_rows[r1].m_images,r2_images_total); for(int i=0; i<r2_images_total; i++) ImageCopy(m_columns[c].m_rows[r1].m_images,m_columns[c].m_rows[r2].m_images,i); //--- ::ArrayResize(m_columns[c].m_rows[r2].m_images,r1_images_total); for(int i=0; i<r1_images_total; i++) ImageCopy(m_columns[c].m_rows[r2].m_images,r1_temp_images,i); } }
Der Klick auf einer der Spaltenköpfe ruft die Methode CCanvasTable::OnClickHeaders() auf, die den Sortiervorgang startet. In dieser Klasse ist alles eindeutig einfacher als in der Tabelle des Typs CTable: Vergleichen und sehen Sie selbst.
class CCanvasTable : public CElement { private: //--- Abfangen des Klicks auf einen Spaltenkopf bool OnClickHeaders(const string clicked_object); }; //+------------------------------------------------------------------+ //| Abfangen des Klicks auf einen Spaltenkopf | //+------------------------------------------------------------------+ bool CCanvasTable::OnClickHeaders(const string clicked_object) { //--- Verlassen, wenn (1) der Sortiermodus deaktiviert ist oder (2) die Spaltenbreite gerade geändert wird if(!m_is_sort_mode || m_column_resize_control!=WRONG_VALUE) return(false); //--- Verlassen, wenn die Bildlaufleiste aktiv ist if(m_scrollv.ScrollState() || m_scrollh.ScrollState()) return(false); //--- Verlassen, wenn es ein anderer Objektname ist if(m_headers.Name()!=clicked_object) return(false); //--- Bestimmung des Spaltenindex uint column_index=0; //--- Abruf der realtive X-Koordinate unter dem Mauskursor int x=m_mouse.RelativeX(m_headers); //--- Bestimmen des geklickten Spaltenkopfes for(uint c=0; c<m_columns_total; c++) { //--- Ist der Spaltenkopf gefunden, sichern dessen Index if(x>=m_columns[c].m_x && x<=m_columns[c].m_x2) { column_index=c; break; } } //--- Sortieren der Daten gemäß der angegebenen Spalte SortData(column_index); //--- Senden einer Nachricht darüber ::EventChartCustom(m_chart_id,ON_SORT_DATA,CElementBase::Id(),m_is_sorted_column_index,::EnumToString(DataType(column_index))); return(true); }
Die anderen Methoden zum Sortieren der Daten bleiben unverändert und unterscheiden sich nicht von den früher besprochenen. Daher betrachten wir nur eine Liste dieser Variablen und Methoden der Klasse CCanvasTable:
class CCanvasTable : public CElement { private: //--- Sortierrichtung der Daten in der Spalte bool m_is_sort_mode; //--- Index der sortierten Spalte (WRONG_VALUE – Tabelle ist nicht sortiert) int m_is_sorted_column_index; //--- Letzt Sortierrichtung ENUM_SORT_MODE m_last_sort_direction; //--- public: //--- Sortiermodus der Daten void IsSortMode(const bool flag) { m_is_sort_mode=flag; } //--- Abfrage/Sichern des Datentyps ENUM_DATATYPE DataType(const uint column_index); void DataType(const uint column_index,const ENUM_DATATYPE type); //--- Sortieren der Daten gemäß der angegebenen Spalte void SortData(const uint column_index=0); //--- private: //--- Abfangen des Klicks auf einen Spaltenkopf bool OnClickHeaders(const string clicked_object); //--- Quicksort Methode void QuickSort(uint beg,uint end,uint column,const ENUM_SORT_MODE mode=SORT_ASCEND); //--- Prüfen der Sortierbedingungen bool CheckSortCondition(uint column_index,uint row_index,const string check_value,const bool direction); };
Das Sortieren einer Tabelle diesen Typs ist unten gezeigt:
Fig. 1. Demonstration des Sortierens einer Tabelle vom Typ CCanvasTable.
Hinzufügen und entfernen von Spalten und Zeilen
In einen früheren Artikel wurden die Methoden entwickelt, um Spalten und Zeilen einer Tabelle des Typs CTable hinzuzufügen und zu löschen. Diese Version erlaubte das Hinzufügen immer nur am Ende der Tabelle. Diese Nicklichkeit wird jetzt aufgelöst: Wir fügen eine Spalte oder Zeile an dem Platz des angegebenen Index hinzu.
Beispiel: Hinzufügen einer Spalte am Anfang einer Tabelle, d.h. diese Spalte muss den ersten Index (Index 0) erhalten. Der Array der Spalten muss um eine Spalte erhöht werden, während die Eigenschaften und Werte aller Tabellenzellen um eine Zelle nach rechts verschoben werden müssen, so dass eine leere erste Spalte entsteht. Das selbe Prinzip wird beim Hinzufügen einer neuen Zeile angewendet.
Das umgekehrte Vorgehen wird für das Entfernen einer Spalte oder Zeile verwendet. Versuchen wir die erste Spalte des vorherigen Beispiels wieder entfernen: zuerst werden alle Daten und Eigenschaften um eine Spalte nach links verschoben und danach wird der Array der Spalten um eins erniedrigt.
Dafür wurde die Hilfsmethode entwickelt, um das Erstellen der Methode zum Hinzufügen oder Löschen von Spalten und Zeilen zu erleichtern. Die Methoden CCanvasTable::ColumnInitialize() und CCanvasTable::CellInitialize() werden zur Initialisierung der Zellen der ergänzten Spalten und Zeilen mit Standardwerten verwendet.
class CCanvasTable : public CElement { private: //--- Initialisierung der angegebenen Spalte mit Standardwerten void ColumnInitialize(const uint column_index); //--- Initialisierung der angegebenen Zelle mit Standardwerten void CellInitialize(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| Initialisierung der angegebenen Spalte mit Standardwerten | //+------------------------------------------------------------------+ void CCanvasTable::ColumnInitialize(const uint column_index) { //--- Initialisierung der Eigenschaften der Spalte mit Standardwerten m_columns[column_index].m_x =0; m_columns[column_index].m_x2 =0; m_columns[column_index].m_width =100; m_columns[column_index].m_type =TYPE_STRING; m_columns[column_index].m_text_align =ALIGN_CENTER; m_columns[column_index].m_text_x_offset =m_text_x_offset; m_columns[column_index].m_image_x_offset =m_image_x_offset; m_columns[column_index].m_image_y_offset =m_image_y_offset; m_columns[column_index].m_header_text =""; } //+------------------------------------------------------------------+ //| Initialisierung der angegebenen Zelle mit Standardwerten | //+------------------------------------------------------------------+ void CCanvasTable::CellInitialize(const uint column_index,const uint row_index) { m_columns[column_index].m_rows[row_index].m_full_text =""; m_columns[column_index].m_rows[row_index].m_short_text =""; m_columns[column_index].m_rows[row_index].m_selected_image =0; m_columns[column_index].m_rows[row_index].m_text_color =m_cell_text_color; m_columns[column_index].m_rows[row_index].m_digits =0; m_columns[column_index].m_rows[row_index].m_type =CELL_SIMPLE; //--- Standardmäßig enthält die Zelle keine Bilder ::ArrayFree(m_columns[column_index].m_rows[row_index].m_images); }
Das Kopieren der Eigenschaften einer Spalte erfolgt mit der Methode CCanvasTable::ColumnCopy(). Ihr Code ist unten aufgeführt:
class CCanvasTable : public CElement { private: //--- Kopieren der angegebenen Spalte (Quelle) an den neuen Ort (Ziel) void ColumnCopy(const uint destination,const uint source); }; //+------------------------------------------------------------------+ //| Kopieren der angegebenen Spalte (Quelle) an den neuen Ort (Ziel) | //+------------------------------------------------------------------+ void CCanvasTable::ColumnCopy(const uint destination,const uint source) { m_columns[destination].m_header_text =m_columns[source].m_header_text; m_columns[destination].m_width =m_columns[source].m_width; m_columns[destination].m_type =m_columns[source].m_type; m_columns[destination].m_text_align =m_columns[source].m_text_align; m_columns[destination].m_text_x_offset =m_columns[source].m_text_x_offset; m_columns[destination].m_image_x_offset =m_columns[source].m_image_x_offset; m_columns[destination].m_image_y_offset =m_columns[source].m_image_y_offset; }
Die Methode CCanvasTable::CellCopy() kopiert die angegebene Zelle in eine andere. Dafür müssen die Indizes der Spalte und der Zeile der Ziel-Zelle und die Indizes der Spalte und Zeile der Quell-Zelle übergeben werden.
class CCanvasTable : public CElement { private: //--- Kopieren der angegebene Zelle (Quelle) an den neuen Ort (Ziel) void CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source); }; //+------------------------------------------------------------------+ //| Kopieren der angegebene Zelle (Quelle) an den neuen Ort (Ziel) | //+------------------------------------------------------------------+ void CCanvasTable::CellCopy(const uint column_dest,const uint row_dest,const uint column_source,const uint row_source) { m_columns[column_dest].m_rows[row_dest].m_type =m_columns[column_source].m_rows[row_source].m_type; m_columns[column_dest].m_rows[row_dest].m_digits =m_columns[column_source].m_rows[row_source].m_digits; m_columns[column_dest].m_rows[row_dest].m_full_text =m_columns[column_source].m_rows[row_source].m_full_text; m_columns[column_dest].m_rows[row_dest].m_short_text =m_columns[column_source].m_rows[row_source].m_short_text; m_columns[column_dest].m_rows[row_dest].m_text_color =m_columns[column_source].m_rows[row_source].m_text_color; m_columns[column_dest].m_rows[row_dest].m_selected_image =m_columns[column_source].m_rows[row_source].m_selected_image; //--- Kopieren der Größe des Arrays von der Quelle zum Ziel int images_total=::ArraySize(m_columns[column_source].m_rows[row_source].m_images); ::ArrayResize(m_columns[column_dest].m_rows[row_dest].m_images,images_total); //--- for(int i=0; i<images_total; i++) { //--- Kopieren, wenn es Bilder gibt if(::ArraySize(m_columns[column_source].m_rows[row_source].m_images[i].m_image_data)<1) continue; //--- Erstelle eine Kopie des Bildes ImageCopy(m_columns[column_dest].m_rows[row_dest].m_images,m_columns[column_source].m_rows[row_source].m_images,i); } }
Um wiederkehrende Teile des Codes in der Methode zu eliminieren, wurde eine weitere Methode erstellt, die nach den Veränderungen der neu erstellten Tabelle aufgerufen wird. Jedes mal, wenn Spalten oder Zeilen hinzugefügt oder gelöscht wurden, muss die Tabelle neu berechnet und angepasst werden, um sie danach neu zu zeichnen, damit diese Änderungen sichtbar werden. Die Methode CCanvasTable::RecalculateAndResizeTable() dient diesem Zweck. Hier bestimmt der einzige Parameter, ob die Tabelle ganz (true) neuzuzeichnen ist oder (false) nur zu aktualisieren.
class CCanvasTable : public CElement { private: //--- Neuberechnen unter Berücksichtigung der letzten Änderungen und Anpassen der Tabelle void RecalculateAndResizeTable(const bool redraw=false); }; //+------------------------------------------------------------------+ //| Neuberechnen nach den letzten Änderungen, Anpassen der Tabelle | //+------------------------------------------------------------------+ void CCanvasTable::RecalculateAndResizeTable(const bool redraw=false) { //--- Berechnen der Tabellengröße CalculateTableSize(); //--- Größenanpassung der Tabelle ChangeTableSize(); //--- Aktualisieren der Tabelle UpdateTable(redraw); }
Die Methoden zum Hinzufügen und Löschen von Spalten und Zeilen sind viel einfacher und leichter zu verstehen, als als die der Tabelle vom Typ CTable. Sie können das leicht selber vergleichen. Hier wird jetzt nur der Code zum Hinzufügen oder Löschen einer Spalte als Beispiel gezeigt. Der Code zum Arbeiten mit Zeilen folgt der gleichen Reihenfolge von Vorgängen wie sie zu Beginn dieses Kapitels beschrieben wurden. Bitte beachten Sie: das Icon einer sortierten Tabelle bewegt sich zusammen mit der sortierten Spalte sowohl beim Hinzufügen wie beim Löschen. Wenn eine sortierte Spalte entfernt wird, wird die Variable des Index der sortierten Spalte auf Null gesetzt.
class CCanvasTable : public CElement { public: //--- Hinzufügen einer Spalte zur Tabelle am angegebenen Index void AddColumn(const int column_index,const bool redraw=false); //--- Löschen der Spalte in der Tabelle mit dem angegebenen Index void DeleteColumn(const int column_index,const bool redraw=false); }; //+------------------------------------------------------------------+ //| Hinzufügen einer Spalte zur Tabelle am angegebenen Index | //+------------------------------------------------------------------+ void CCanvasTable::AddColumn(const int column_index,const bool redraw=false) { //--- Erhöhen der Arraygröße um eine Spalte int array_size=(int)ColumnsTotal(); m_columns_total=array_size+1; ::ArrayResize(m_columns,m_columns_total); //--- Setzen der Anzahl der Zeilen der Tabelle ::ArrayResize(m_columns[array_size].m_rows,m_rows_total); //--- Anpassen des Index, falls die Größe überschritten wurde int checked_column_index=(column_index>=(int)m_columns_total)? (int)m_columns_total-1 : column_index; //--- Verschieben der anderen Spalten (beginnend vom Ende des Arrays bis zum Index der neuen Spalte) for(int c=array_size; c>=checked_column_index; c--) { //--- Verschieben des Icons der sortierten Tabelle if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE) m_is_sorted_column_index++; //--- Index der alten Spalte int prev_c=c-1; //--- Initialisierung der neuen Spalte mit den Standardwerten if(c==checked_column_index) ColumnInitialize(c); //--- Verschieben der Daten von der alten Spalte in die aktuelle else ColumnCopy(c,prev_c); //--- for(uint r=0; r<m_rows_total; r++) { //--- Initialisierung der neuen Spalte mit Standardwerten if(c==checked_column_index) { CellInitialize(c,r); continue; } //--- Verschieben der Daten der alten Spaltenzelle zur aktuellen Spaltenzelle CellCopy(c,r,prev_c,r); } } //--- Berechnen und Anpassen der Tabelle RecalculateAndResizeTable(redraw); } //+------------------------------------------------------------------+ //| Löschen der Spalte einer Tabelle mit angegebenen Index | //+------------------------------------------------------------------+ void CCanvasTable::DeleteColumn(const int column_index,const bool redraw=false) { //--- Abruf der Arraygröße der Spalten int array_size=(int)ColumnsTotal(); //--- Anpassen des Index, falls die Größe überschritten wurde int checked_column_index=(column_index>=array_size)? array_size-1 : column_index; //--- Verschieben der anderen Spalten (beginnend beim angegebenen Index bis zur letzten Spalte) for(int c=checked_column_index; c<array_size-1; c++) { //--- Verschieben des Icons der sortierten Tabelle if(c!=checked_column_index) { if(c==m_is_sorted_column_index && m_is_sorted_column_index!=WRONG_VALUE) m_is_sorted_column_index--; } //--- Null setzten, wenn die Spalte entfernt wurde else m_is_sorted_column_index=WRONG_VALUE; //--- Index der nächsten Spalte int next_c=c+1; //--- Verschieben der Daten aus der nächsten Spalte in die aktuelle ColumnCopy(c,next_c); //--- Verschieben der Daten der nächsten Spaltenzelle in die aktuelle for(uint r=0; r<m_rows_total; r++) CellCopy(c,r,next_c,r); } //--- Erniedrigen des Spaltenarrays um eine Spalte m_columns_total=array_size-1; ::ArrayResize(m_columns,m_columns_total); //--- Berechnen und Anpassen der Tabelle RecalculateAndResizeTable(redraw); }
Die Methoden, um die Tabelle komplett neuzuerstellen und zu löschen sind auch ganz einfach, anders als die Methoden für die Tabelle des Typs CTable. Jetzt kann die neue Größe einfach mit der Methode CCanvasTable::TableSize() festgelegt werden. Die kleinste Größe wird durch das Löschen der ganzen Tabelle gesetzt, sie beträgt eine Spalte und eine Zeile. Die Variablen mit Werten der gewählten Zeile, Sortierrichtung und dem Index der sortierenden Spalte sind auch auf Null gesetzt, wenn sie gelöscht werden. Die neue Tabellengröße wird berechnet und am Ende beider Methoden gesetzt.
class CCanvasTable : public CElement { public: //--- Neuerstellen der Tabelle void Rebuilding(const int columns_total,const int rows_total,const bool redraw=false); //--- Löschen der Tabelle. Es bleiben nur eine Spalte und eine Zeile. void Clear(const bool redraw=false); }; //+------------------------------------------------------------------+ //| Neuerstellen der Tabelle | //+------------------------------------------------------------------+ void CCanvasTable::Rebuilding(const int columns_total,const int rows_total,const bool redraw=false) { //--- Festlegen der neuen Größe TableSize(columns_total,rows_total); //--- Berechnen und Anpassen der Tabelle RecalculateAndResizeTable(redraw); } //+------------------------------------------------------------------+ //| Löschen der Tabelle. Es bleiben nur eine Spalte und eine Zeile. | //+------------------------------------------------------------------+ void CCanvasTable::Clear(const bool redraw=false) { //--- Setzen der Mindestgröße von 1x1 TableSize(1,1); //--- Setzen der Standardwerte m_selected_item_text =""; m_selected_item =WRONG_VALUE; m_last_sort_direction =SORT_ASCEND; m_is_sorted_column_index =WRONG_VALUE; //--- Berechnen und Anpassen der Tabelle RecalculateAndResizeTable(redraw); }
Und so schaut alles aus:
Fig. 2. Demonstration der Verwaltung der Tabellengröße.
Die Test-Anwendung der obigen Animation kann am Ende des Artikels heruntergeladen werden.
Doppelklick der linken Maustaste
Manchmal kann notwendig sein, ein bestimmtes Ereignis mit einem Doppelklick auszulösen. Implementieren wir diesen Auslöser in der Bibliothek. Dazu fügen wir den Identifikator ON_DOUBLE_CLICK eines Doppelklicks der linken Maustaste der Datei Defines.mqh hinzu:
//+------------------------------------------------------------------+ //| Defines.mqh | //| Copyright 2015, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ ... #define ON_DOUBLE_CLICK (34) // Doppelklick der linken Maustaste ...
Es wird ein Ereignis des Identifikator ON_DOUBLE_CLICK in der Klasse CMouse erzeugt. In Betriebssystemen wird dieses Ereignis in der Regel durch "Drücken-Loslassen-Drücken" der linken Maustaste erzeugt. Aber Tests mit der Terminalumgebung haben gezeigt, dass es nicht immer gelingt, dies Ereignis gleichzeitig mit dem Ereignis CHARTEVENT_MOUSE_MOVE (Parameter sparam) zu erkennen. Daher wurde dieses Ereignis mit dem Auslöser “Drücken-Loslassen-Drücken-Loslassen” implementiert. Diese Aktionen können korrekt zusammen mit dem Ereignis CHARTEVENT_CLICK erkannt werden.
Standardmäßig dürfen nicht mehr als 300 Millisekunden zwischen den beiden Klicks liegen. Für das Erkennen des Ereignisses CHARTEVENT_CLICK wird die Methode CMouse::CheckDoubleClick() in der Ereignisbehandlung der Maus verwendet. Zwei 'static' Variablen werden zu Beginn dieser Methode deklariert, um bei jedem Aufruf der Methode die Werte des aktuellen und des vorigen Ticks (die Anzahl der Millisekunden seit Systemstart) zu sichern. Ist die Anzahl der Millisekunden zwischen beiden Werten kleiner als in der Variablen m_pause_between_clicks festgelegt, dann wird ein Ereignis des Doppelklicks der linken Maustaste erzeugt.
//+------------------------------------------------------------------+ //| Klasse zur Abfrage der Mausparameter | //+------------------------------------------------------------------+ class CMouse { private: //--- Zeitspanne zwischen zwei Klicks der linken Maustaste (zur Bestimmung des Doppelklicks) uint m_pause_between_clicks; //--- private: //--- Prüfen auf Doppelklick der linken Maustaste bool CheckDoubleClick(void); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CMouse::CMouse(void) : m_pause_between_clicks(300) { ... } //+------------------------------------------------------------------+ //| Umgang mit dem Ereignis der Maus | //+------------------------------------------------------------------+ void CMouse::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- Umgang mit einem Klick auf dem Chart if(id==CHARTEVENT_CLICK) { //--- Prüfen auf Doppelklick der linken Maustaste CheckDoubleClick(); return; } } //+------------------------------------------------------------------+ //| Prüfen auf Doppelklick der linken Maustaste | //+------------------------------------------------------------------+ void CMouse::CheckDoubleClick(void) { static uint prev_depressed =0; static uint curr_depressed =::GetTickCount(); //--- Aktualisieren der Werte prev_depressed =curr_depressed; curr_depressed =::GetTickCount(); //--- Bestimmen der Zeitspanne zwischen den Klicks uint counter=curr_depressed-prev_depressed; //--- Ist die Zeitspanne kleiner als angegeben, sende eine Nachricht über einen Doppelklick if(counter<m_pause_between_clicks) ::EventChartCustom(m_chart.ChartId(),ON_DOUBLE_CLICK,counter,0.0,""); }
Jetzt vermag die Ereignisbehandlung der Klassen in der Bibliothek auf einen Doppelklick der linken Maustaste irgendwo auf dem Chart zu reagieren, egal ob sich ein Grafikobjekt unter der Maus befindet oder nicht.
Steuerelemente in einer Tabellenzelle
In diesem Artikel beginnen wir mit dem Thema der Steuerelemente in Tabellenzellen. Diese Funktion könnte zum Beispiel beim Erstellen von mehrparametrischen Expert Systeme notwendig werden. Es ist leichter ein solches System über ein grafisches Interface anzusprechen. Beginnen wir damit, solche Steuerelemente den Tabellenzellen hinzuzufügen, mit Kontrollkästchen und Tasten.
Als ersten benötigen wir die neue Enumeration ENUM_TYPE_CELL der Zelltypen:
//+------------------------------------------------------------------+ //| Enumeration der Typen der Tabellenzellen | //+------------------------------------------------------------------+ enum ENUM_TYPE_CELL { CELL_SIMPLE =0, CELL_BUTTON =1, CELL_CHECKBOX =2 };
Standardmäßig werden während der Initialisierung den Tabellenzellen der einfache Typ – CELL_SIMPLE zugewiesen, das heißt “Zelle ohne Steuerelement”. Um den Zelltyp anzufragen oder festzulegen, verwenden wir die Methode CCanvasTable::CellType(), ihr Code ist unten aufgeführt. Wurde einer Zelle der Typ CELL_CHECKBOX (Kontrollkästchen) zugewiesen, verlangt das auch die Bilder, die die Zustände der Kontrollkästchen zeigen. Die kleinst Zahl von Bildern dieses Zelltyps ist zwei. Es ist möglich mehr als zwei Icons zu verwenden, das erlaubt den Einsatz von Kontrollkästchen mit mehreren Parametern.
class CCanvasTable : public CElement { //--- Bestimmen/Abfragen des Zelltyps void CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type); ENUM_TYPE_CELL CellType(const uint column_index,const uint row_index); }; //+------------------------------------------------------------------+ //| Bestimmen des Zelltyps | //+------------------------------------------------------------------+ void CCanvasTable::CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type) { //--- Prüfen der Arraygrenze if(!CheckOutOfRange(column_index,row_index)) return; //--- Bestimmen des Zelltyps m_columns[column_index].m_rows[row_index].m_type=type; } //+------------------------------------------------------------------+ //| Abfragen des Zelltyps | //+------------------------------------------------------------------+ ENUM_TYPE_CELL CCanvasTable::CellType(const uint column_index,const uint row_index) { //--- Prüfen der Arraygrenze if(!CheckOutOfRange(column_index,row_index)) return(WRONG_VALUE); //--- Rückgabe des Typs der angegebenen Spalte return(m_columns[column_index].m_rows[row_index].m_type); }
Da Bilderreihen für Kontrollkästchen und Tasten in jeder Spalte unterschiedlich groß sein können, müssen wir in der Lage sein die X- und Y-Abstände für jede Spalte einzeln zu bestimmen. Dazu verwenden wir die Methoden CCanvasTable::ImageXOffset() und CCanvasTable::ImageYOffset():
class CCanvasTable : public CElement { public: //--- Abstand der Bilder entlang der X- und Y-Achse void ImageXOffset(const int &array[]); void ImageYOffset(const int &array[]); };
Eine weitere Ergänzung ist der Modus, der das Deselektieren einer Zeile bei einem erneuten Klick deaktiviert. Dieser Modus funktioniert nur, wenn die Auswahlmodus für Zeilen aktiviert ist.
class CCanvasTable : public CElement { private: //--- Kein Deselektieren bei einem erneuten Klick bool m_is_without_deselect; //--- public: //--- Der "Kein Deselektieren bei einem erneuten Klick" Modus void IsWithoutDeselect(const bool flag) { m_is_without_deselect=flag; } };
Die separaten Methoden CCanvasTable::PressedRowIndex() und CCanvasTable::PressedCellColumnIndex() werden jetzt zur Bestimmung des Index der geklickten Spalte und Zeile verwendet. Sie wurden bereits beschrieben als Teile der Methode CCanvasTable::OnClickTable(). Ihr ganzer Code mit den Ergänzungen findet sich in den Dateien, die dem Artikel beigefügt sind. Es sollte eigens vermerkt werden, dass die Verwendung der beiden Methoden bei der Bestimmung der durch die linke Maustaste geklickten Zelle. Als Nächstes schauen wir uns an, wo die erhaltenen Indices der Spalte und Zeile übergeben werden.
class CCanvasTable : public CElement { private: //--- Rückgabe des Index der geklickten Zeile int PressedRowIndex(void); //--- Rückgabe des Spaltenindex der geklickten Zelle int PressedCellColumnIndex(void); };
Wurde eine Tabellenzelle geklickt, muss ihr Typ ermittelt werden und, wenn es ein Steuerelement enthält, muss auch ermittelt werden, ob es aktiviert ist. Zu diesem Zweck werden mehrere 'private' Methoden erstellt, die wichtigste ist CCanvasTable::CheckCellElement(). Ihr werden die Indizes der Spalte und der Zeile übergeben, die die Methoden CCanvasTable::PressedRowIndex() und CCanvasTable::PressedCellColumnIndex() zurückgeliefert haben. Die Methode kennt zwei Modi. Je nach dem Ereignis, weswegen die Methode aufgerufen wird, wird der dritte Parameter verwendet, um anzugeben, ob das Ereignis ein Dopplklick ist.
Danach wird der Zelltyp abgefragt und die entsprechende Methode aufgerufen. Enthält die Zelle eine "Taste" , dann wird die Methode CCanvasTable::CheckPressedButton() aufgerufen. Die Methode CCanvasTable::CheckPressedCheckBox() ist für Zellen mit Kontrollkästchen.
class CCanvasTable : public CElement { private: //--- Prüfen, ob das Steuerelement der Zelle aktiv ist bool CheckCellElement(const int column_index,const int row_index,const bool double_click=false); }; //+------------------------------------------------------------------+ //| Prüfen, ob das Steuerelement beim Klick aktiv ist | //+------------------------------------------------------------------+ bool CCanvasTable::CheckCellElement(const int column_index,const int row_index,const bool double_click=false) { //--- Verlassen, wenn die Zelle gar kein Steuerelement hat if(m_columns[column_index].m_rows[row_index].m_type==CELL_SIMPLE) return(false); //--- switch(m_columns[column_index].m_rows[row_index].m_type) { //--- Ist es eine Taste case CELL_BUTTON : { if(!CheckPressedButton(column_index,row_index,double_click)) return(false); //--- break; } //--- Ist es ein Kontrollkästchen case CELL_CHECKBOX : { if(!CheckPressedCheckBox(column_index,row_index,double_click)) return(false); //--- break; } } //--- return(true); }
Schauen wir jetzt einmal auf die Struktur der Methoden CCanvasTable::CheckPressedButton() und CCanvasTable::CheckPressedCheckBox(). Die Anzahl der Bilder in der Zelle wird zu Beginn beider Methoden überprüft. Ein Zelle mit einer Taste beinhaltet mindestens ein Icon, Zellen mit einem Kontrollkästchen zwei. Dann muss ermittelt werden, ob das Icon geklickt wurde. Im Falle des Kontrollkästchens, muss eine binäre Umschaltung implementiert werden. Ein einzelner Klick funktioniert nur mit den Icons der Kontrollkästchen. Ein Doppelklick irgendwo in der Zelle kehrt das Kontrollkästchen um. Beide Methoden erzeugen ein Ereignis mit dem entsprechenden Identifikator und garantieren, dass alle Bedingungen erfüllt sind. Der Index des Icons wird als double Parameter übergeben, und der string Parameter bildet einen Zeichenkette aus den Indices von Spalte und Zeile.
class CCanvasTable : public CElement { private: //--- Prüfen ob die Taste geklickt wurde bool CheckPressedButton(const int column_index,const int row_index,const bool double_click=false); //--- Prüfen, ob das Kontrollkästchen in der Zelle geklickt wurde bool CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false); }; //+------------------------------------------------------------------+ //| Prüfen ob die Taste geklickt wurde | //+------------------------------------------------------------------+ bool CCanvasTable::CheckPressedButton(const int column_index,const int row_index,const bool double_click=false) { //--- Verlassen, wenn es kein Bild in der Zelle gibt if(ImagesTotal(column_index,row_index)<1) { ::Print(__FUNCTION__," > Assign at least one image to the button cell!"); return(false); } //--- Ermitteln der relativen Koordinaten unter dem Mauskursor int x=m_mouse.RelativeX(m_table); // --- Ermitteln des rechten Randes des Bildes int image_x =int(m_columns[column_index].m_x+m_columns[column_index].m_image_x_offset); int image_x2 =int(image_x+m_columns[column_index].m_rows[row_index].m_images[0].m_image_width); //--- Verlassen, wenn nicht das Bild geklickt wurde if(x>image_x2) return(false); else { //--- War es kein Doppelklick, sende eine Nachricht if(!double_click) { int image_index=m_columns[column_index].m_rows[row_index].m_selected_image; ::EventChartCustom(m_chart_id,ON_CLICK_BUTTON,CElementBase::Id(),image_index,string(column_index)+"_"+string(row_index)); } } //--- return(true); } //+------------------------------------------------------------------+ //| Prüfen, ob das Kontrollkästchen in der Zelle geklickt wurde | //+------------------------------------------------------------------+ bool CCanvasTable::CheckPressedCheckBox(const int column_index,const int row_index,const bool double_click=false) { //--- Verlassen, wenn es kein Bild in der Zelle gibt if(ImagesTotal(column_index,row_index)<2) { ::Print(__FUNCTION__," > Assign at least two images to the checkbox cell!"); return(false); } //--- Ermitteln der relativen Koordinaten unter dem Mauskursor int x=m_mouse.RelativeX(m_table); // --- Ermitteln des rechten Randes des Bildes int image_x =int(m_columns[column_index].m_x+m_image_x_offset); int image_x2 =int(image_x+m_columns[column_index].m_rows[row_index].m_images[0].m_image_width); //--- Verlassen, wenn (1) nicht das Bild geklickt wurde und (2) es kein Doppelklick war if(x>image_x2 && !double_click) return(false); else { //--- Aktueller Index des gewählten Bildes int image_i=m_columns[column_index].m_rows[row_index].m_selected_image; //--- Bestimmen des nächsten Index des Bildes int next_i=(image_i<ImagesTotal(column_index,row_index)-1)? ++image_i : 0; //--- Wählen des nächsten Bildes und Aktualisieren der Tabelle ChangeImage(column_index,row_index,next_i,true); m_table.Update(false); //--- Senden einer Nachricht darüber ::EventChartCustom(m_chart_id,ON_CLICK_CHECKBOX,CElementBase::Id(),next_i,string(column_index)+"_"+string(row_index)); } //--- return(true); }
Im Ergebnis ist der Code der Methoden CCanvasTable::OnClickTable() und CCanvasTable::OnDoubleClickTable() für den Umgang mit Mausklicks in der Tabelle jetzt sehr viel leichter zu verstehen und zu lesen (siehe den Code unten). Es wird ein entsprechendes Ereignis erzeugt in Abhängigkeit des aktivierten Modus und des Zelltyps, der geklickt wurde.
class CCanvasTable : public CElement { private: //--- Behandlung eines Tabellenklicks bool OnClickTable(const string clicked_object); //--- Behandlung eines Doppelklicks in der Tabelle bool OnDoubleClickTable(const string clicked_object); }; //+------------------------------------------------------------------+ //| Behandlung eines Klicks in der Tabelle | //+------------------------------------------------------------------+ bool CCanvasTable::OnClickTable(const string clicked_object) { //--- Verlassen, wenn (1) der Modus der Zeilenauswahl deaktiviert ist oder (2) gerade die Spaltenbreite geändert wird if(m_column_resize_control!=WRONG_VALUE) return(false); //--- Verlassen, wenn die Bildlaufleiste aktiv ist if(m_scrollv.ScrollState() || m_scrollh.ScrollState()) return(false); //--- Verlassen, wenn es ein anderer Objektname ist if(m_table.Name()!=clicked_object) return(false); //--- Bestimmen der geklickten Zeile int r=PressedRowIndex(); //--- Bestimmen der geklickten Zelle int c=PressedCellColumnIndex(); //--- Prüfen, wenn das Steuerelement der Zelle aktiv ist bool is_cell_element=CheckCellElement(c,r); //--- Wenn (1) Auswahlmodus für Zeilen aktiv ist und (2) das Steuerelement der Zelle nicht aktiviert ist if(m_selectable_row && !is_cell_element) { //--- Ändern der Farbe RedrawRow(true); m_table.Update(); //--- Senden einer Nachricht darüber ::EventChartCustom(m_chart_id,ON_CLICK_LIST_ITEM,CElementBase::Id(),m_selected_item,string(c)+"_"+string(r)); } //--- return(true); } //+------------------------------------------------------------------+ //| Behandlung eines Doppelklicks in der Tabelle | //+------------------------------------------------------------------+ bool CCanvasTable::OnDoubleClickTable(const string clicked_object) { if(!m_table.MouseFocus()) return(false); //--- Bestimmen der geklickten Zeile int r=PressedRowIndex(); //--- Bestimmen der geklickten Zelle int c=PressedCellColumnIndex(); //--- Prüfen, ob das Steuerelement in der Zelle aktiviert wurde und die Rückgabe des Ergebnisses return(CheckCellElement(c,r,true)); }
Ereignisse der linken Maustaste in einer Zelle werden von der Ereignisbehandlung des Steuerelementes entsprechend des Ereignisses ON_DOUBLE_CLICK bearbeitet:
//+------------------------------------------------------------------+ //| Ereignisbehandlung | //+------------------------------------------------------------------+ void CCanvasTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- Handhabung des Doppelklicks der linken Maustaste if(id==CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK) { //--- Klicks in der Tabelle if(OnDoubleClickTable(sparam)) return; //--- return; } }
Am Ende funktioniert alles wie dargestellt:
Fig. 3. Demonstration des Einwirkens auf die Steuerelemente von Tabellenzellen.
Eine Anwendung aus diesem Artikel kann über den Link am Ende heruntergeladen werden.
Schlussfolgerung
Zur Zeit schaut das Schema der Bibliothek zum Erstellen einer grafische Benutzeroberfläche wie folgt aus:
Fig. 4. Struktur der Bibliothek im augenblicklichen Entwicklungszustand.
Unten können Sie die letzten Versionen der Bibliothek und der Dateien zum Testen herunterladen.
Bei Fragen zur Verwendung des bereitgestellten Materials, können Sie auf die detaillierten Beschreibungen im Lauf der Entwicklung der Bibliothek in den vorangegangenen Artikeln dieser Serie zurückgreifen oder Sie stellen Ihre Fragen im Kommentarteil diese Artikels.
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/3104
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.