
Grafiken in der DoEasy-Bibliothek (Teil 99): Verschieben eines erweiterten grafischen Objekts mit einem einzigen Steuerpunkt
Inhalt
Konzept
Im letzten Artikel, habe ich die Möglichkeit implementiert, Angelpunkte eines erweiterten grafischen Objekts mit Hilfe von Steuerformularen zu verschieben. Ich verfüge jedoch noch nicht über die Funktionsweise, ein solches grafisches Objekt als Ganzes zu verschieben. Jedes grafische Standardobjekt kann vollständig verschoben werden, wenn man seinen zentralen Punkt verschiebt. In ähnlicher Weise werde ich hier einen einzigen zentralen Punkt für die Verwaltung eines grafischen Objekts festlegen, um das gesamte grafische Objekt (und nicht nur seine Angelpunkte) durch Verschieben des Punkts bewegen zu können. Für den Test habe ich ein zusammengesetztes grafisches Objekt gewählt, das aus einer Trendlinie besteht, an deren Enden sich Preisetiketten befinden. In diesem Sinne wird die gesamte Arbeit für grafische Objekte durchgeführt, die zwei Angelpunkte für die Verschiebung ihrer Enden und einen einzigen zentralen Punkt für die Verschiebung des gesamten grafischen Objekts haben (zwei Punkte für die Änderung der Objektenden und ein zentraler Punkt für die Verschiebung). Später werde ich die gleichen Formulare mit Kontrollpunkten für grafische Objekte mit mehr als drei Kontrollpunkten erstellen.
Außerdem werde ich den Code zur Berechnung der Bildschirmkoordinaten der Angelpunkte des grafischen Objekts leicht optimieren, indem ich ihn in separate Methoden aufteile, um das Verständnis der grundlegenden Logik zu vereinfachen. Schließlich ist es viel einfacher, den Code mit dem Aufruf der Methode zu lesen, die einen bestimmten Wert zurückgibt (während es wiederum eine andere Methode darin gibt, die ebenfalls etwas berechnet), als den gesamten Code solcher Methoden in den Hauptberechnungsblock zu setzen, was ihn unhandlich und schwer verständlich macht.
Nicht alles, was hier implementiert ist, wird immer wie beabsichtigt funktionieren. Aber das Ziel des Artikels ist es, den Prozess der Entwicklung und Erstellung des Codes zu beschreiben, um das notwendige Ergebnis zu erhalten. Ich glaube, es ist viel interessanter, fast den ganzen Weg von der Planung der Funktionalität bis zu ihrer Implementierung zu gehen, als nur eine trockene Präsentation darüber zu lesen, "wie alles am Ende ausgegangen ist".
Da die Funktion zum Abrufen der Bildschirmkoordinaten, ChartTimePriceToXY(), nur die Koordinaten des sichtbaren Teils des Charts zurückgibt, können wir die Bildschirmkoordinaten des Linienpunkts außerhalb der Chartgrenzen nicht berechnen. Die Funktion gibt immer 0 zurück, wenn die X-Koordinate in Bildschirmzeitpixeln außerhalb des linken Teils des sichtbaren Charts abgefragt wird. Wenn Sie also ein zusammengesetztes grafisches Objekt auf dem Bildschirm verschieben und sein linker Teil über den linken Rand des Bildschirms hinausgeht, bleibt der linke Angelpunkt des Objekts an der Koordinate 0 in Chart-Pixeln. Dies führt zu einer Verzerrung des grafischen Objekts. Dasselbe gilt für die rechte Seite des grafischen Objekts und die rechte Seite des Chart-Bildschirms (sowie für die obere und untere Seite). Daher werde ich eine Begrenzung für ein zusammengesetztes grafisches Objekt einführen, die seine Bewegung über den sichtbaren Teil des Charts hinaus einschränkt. Dadurch wird verhindert, dass das grafische Objekt verzerrt wird, wenn eine seiner Seiten beim Verschieben gegen den Bildschirmrand stößt.
Verbesserung der Klassenbibliothek
Da das Objektformular, das zur Anzeige eines Kontrollpunkts für die Verwaltung von Angelpunkten eines erweiterten grafischen Objekts verwendet wird, ein wichtiges Objekt unter den Bibliotheksobjekten ist, die Formulare aber nicht in die Sammlung der grafischen Objekte aufgenommen werden, müssen wir einen neuen Typ für solche Formulare definieren. Alle wichtigen Bibliotheksobjekte haben einen eigenen Namen für den Typ des Bibliotheksobjekts, der es uns ermöglicht, zu definieren, welches Objekt gerade aktiv ist. Definieren wir den Typ für die Formularobjekte, die für die Verwaltung von Angelpunkten innerhalb der erweiterten grafischen Objekte der Bibliothek gedacht sind.
In \MQL5\Include\DoEasy\Defines.mqh fügen wir den neuen Typ zur Enumeration der Bibliotheksobjekttypen hinzu:
//+------------------------------------------------------------------+ //| List of library object types | //+------------------------------------------------------------------+ enum ENUM_OBJECT_DE_TYPE { //--- Graphics OBJECT_DE_TYPE_GBASE = COLLECTION_ID_LIST_END+1, // "Base object of all library graphical objects" object type OBJECT_DE_TYPE_GELEMENT, // "Graphical element" object type OBJECT_DE_TYPE_GFORM, // Form object type OBJECT_DE_TYPE_GFORM_CONTROL, // "Form for managing pivot points of graphical object" object type OBJECT_DE_TYPE_GSHADOW, // Shadow object type //--- Animation OBJECT_DE_TYPE_GFRAME, // "Single animation frame" object type OBJECT_DE_TYPE_GFRAME_TEXT, // "Single text animation frame" object type OBJECT_DE_TYPE_GFRAME_QUAD, // "Single rectangular animation frame" object type OBJECT_DE_TYPE_GFRAME_GEOMETRY, // "Single geometric animation frame" object type OBJECT_DE_TYPE_GANIMATIONS, // "Animations" object type //--- Managing graphical objects ... ... ... }
In \MQL5\Include\DoEasy\Data.mqh ergänzen wir die Indices der neuen Nachrichten:
MSG_LIB_SYS_REQUEST_OUTSIDE_LONG_ARRAY, // Request outside long array MSG_LIB_SYS_REQUEST_OUTSIDE_DOUBLE_ARRAY, // Request outside double array MSG_LIB_SYS_REQUEST_OUTSIDE_STRING_ARRAY, // Request outside string array MSG_LIB_SYS_REQUEST_OUTSIDE_ARRAY, // Request outside the array MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY, // Failed to convert graphical object coordinates to screen ones MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY, // Failed to convert time/price coordinates to screen ones
und die Textmeldungen zu den neu hinzugefügten Indizes:
{"Запрос за пределами long-массива","Data requested outside the long-array"}, {"Запрос за пределами double-массива","Data requested outside the double-array"}, {"Запрос за пределами string-массива","Data requested outside the string-array"}, {"Запрос за пределами массива","Data requested outside the array"}, {"Не удалось преобразовать координаты графического объекта в экранные","Failed to convert graphics object coordinates to screen coordinates"}, {"Не удалось преобразовать координаты время/цена в экранные","Failed to convert time/price coordinates to screen coordinates"},
Wenn bei der Entwicklung der Funktionen zum Verschieben von grafischen Objekten ein Fehler bei der Konvertierung von Zeit-/Preiskoordinaten in Bildschirmkoordinaten festgestellt wird, wird dieser Fehler gemeldet, um die Kette von der logischen Fehlersuche auszuschließen.
Die Funktion ChartTimePriceToXY(), die möglicherweise den Koordinatenumwandlungsfehler verursacht, wird auch in der Objektklasse des Chartfensters in \MQL5\Include\DoEasy\Objects\Chart\ChartWnd.mqh verwendet. Fügen wir der Methode TimePriceToXY() die Anzeige der Fehlermeldung der Koordinatenumrechnung im Journal hinzu:
//+------------------------------------------------------------------+ //| Convert chart coordinates from the time/price representation | //| to X and Y coordinates | //+------------------------------------------------------------------+ bool CChartWnd::TimePriceToXY(const datetime time,const double price) { ::ResetLastError(); if(!::ChartTimePriceToXY(this.m_chart_id,this.WindowNum(),time,price,this.m_wnd_coord_x,this.m_wnd_coord_y)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_CONV_TIMEPRICE_COORDS_TO_XY); CMessage::ToLog(DFUN,::GetLastError(),true); return false; } return true; } //+------------------------------------------------------------------+
Zuerst wird die Meldung "Failed to convert time/price coordinates to screen ones" (Konvertieren der Zeit-/Preiskoordinaten in Bildschirmkoordinaten ist fehlgeschlagen) angezeigt, gefolgt von der Fehlerbeschreibung zusammen mit dem Fehlercode.
Da ich nun den neuen Bibliotheksobjekttyp für Kontrollpunkte zur Verwaltung von Angelpunkten eines erweiterten grafischen Standardobjekts deklariert habe, muss ich die Klasse eines solchen Objekts erstellen, die von der Klasse der Formularobjekte geerbt wird. Innerhalb der Klasse werde ich einige Variablen und Methoden hinzufügen, um den Umgang mit solchen Objekten zu vereinfachen.
Wir platzieren sie in das Toolkit eines erweiterten grafischen Standardobjekts in \MQL5\Include\DoEasy\Objects\Graph\Extend\CGStdGraphObjExtToolkit.mqh:
//+------------------------------------------------------------------+ //| CGStdGraphObjExtToolkit.mqh | //| Copyright 2022, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2022, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "..\..\Graph\Form.mqh" //+------------------------------------------------------------------+ //| Class of the form for managing pivot points of a graphical object| //+------------------------------------------------------------------+ class CFormControl : public CForm { private: bool m_drawn; // Flag indicating that the pivot point is drawn on the form int m_pivot_point; // Pivot point managed by the form public: //--- (1) Return and (2) set the drawn point flag bool IsControlAlreadyDrawn(void) const { return this.m_drawn; } void SetControlPointDrawnFlag(const bool flag) { this.m_drawn=flag; } //--- (1) Return and (2) set the pivot point managed by the form int GraphObjPivotPoint(void) const { return this.m_pivot_point; } void SetGraphObjPivotPoint(const int index) { this.m_pivot_point=index; } //--- Constructor CFormControl(void) { this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL; } CFormControl(const long chart_id,const int subwindow,const string name,const int pivot_point,const int x,const int y,const int w,const int h) : CForm(chart_id,subwindow,name,x,y,w,h) { this.m_type=OBJECT_DE_TYPE_GFORM_CONTROL; this.m_pivot_point=pivot_point; } }; //+------------------------------------------------------------------+ //| Extended standard graphical | //| object toolkit class | //+------------------------------------------------------------------+ class CGStdGraphObjExtToolkit : public CObject
Die private Klassenvariable m_drawn dient dazu, das Flag zu speichern, das angibt, dass der Punkt bereits auf dem Formular gezeichnet wurde. Warum brauchen wir eine solche Variable? Wenn der Mauszeiger aus der aktiven Zone des Formulars für die Verwaltung der Kontrollpunkte eines grafischen Objekts entfernt wird, muss der auf dem Formular gezeichnete Punkt gelöscht werden. Gegenwärtig wird der gezeichnete Punkt auf allen derartigen Formularen ständig entfernt, wenn der Mauszeiger nicht über dem aktiven Bereich des Formulars schwebt. Warum sollten wir das System mit dem ständigen Neuzeichnen all dieser Formulare belasten, wenn wir vorher das Flag für das Neuzeichnen des Formulars ansehen können? Das Flag informiert uns darüber, ob der Punkt gezeichnet oder entfernt wird. In der Zukunft werde ich einige visuelle Effekte für das Zeichnen solcher Punkte entwickeln (zusätzlich zu einigen anderen Dingen), daher ist es besser, das Flag zu haben, das sofort nach dem Ausführen des Handlers für visuelle Effekte gesetzt wird, als zu versuchen, zu definieren, dass das Zeichnen bereits abgeschlossen ist.
Die private Klassenvariable m_pivot_point speichert den vom Formular verwalteten Index des Angelpunkts. Ein grafisches Objekt hat mehrere Kontrollpunkte. Zum Beispiel hat eine Trendlinie drei Punkte — zwei an den Enden der Linien für die unabhängige Änderung der Position der Linienenden und einen zentralen Punkt für die Bewegung des gesamten Objekts. Die in den Formularobjekten gespeicherten Indizes entsprechen den Indizes der Angelpunkte der Linie: 0 und 1 — für die Punkte entlang der Linienränder und 2 — für den zentralen Andere grafische Objekte können völlig andere Kontrollpunkte haben, aber alle Indizes müssen den Angelpunkten des Objekts entsprechen und einen zusätzlichen (wenn auch nicht in allen Fällen, dies wird in späteren Artikeln besprochen) für die Bewegung des gesamten Objekts.
Die öffentlichen Methoden der Klasse werden verwendet, um die Werte der oben beschriebenen Variablen zu setzen/zurückzugeben. Die Klasse verfügt außerdem über zwei Konstruktoren. Im Standardkonstruktor wird der im aktuellen Artikel neu hinzugefügte OBJECT_DE_TYPE_GFORM_CONTROL-Typ im Objekttyp gesetzt.
Der parametrische Konstruktor übergibt alle Werte, die an den Konstruktor der übergeordneten Klasse übergeben wurden, plus eine zusätzliche Variable — den Index des grafischen Objekts Angelpunkt, das von dem erstellten Formular verwaltet wird.
Nun werden alle Angelpunkt-Steuerungsformulare in der Klasse CGStdGraphObjExtToolkit vom Typ CFormControl sein, daher müssen wir den Typ des CForm-Formularobjekts durch CFormControl ersetzen und die neuen Methoden zur Handhabung der Steuerungsformulare für die Verwaltung grafischer Objekt-Angelpunkte hinzufügen:
//+------------------------------------------------------------------+ //| Extended standard graphical | //| object toolkit class | //+------------------------------------------------------------------+ class CGStdGraphObjExtToolkit : public CObject { private: long m_base_chart_id; // Base graphical object chart ID int m_base_subwindow; // Base graphical object chart subwindow ENUM_OBJECT m_base_type; // Base object type string m_base_name; // Base object name int m_base_pivots; // Number of base object reference points datetime m_base_time[]; // Time array of base object reference points double m_base_price[]; // Price array of base object reference points int m_base_x; // Base object X coordinate int m_base_y; // Base object Y coordinate int m_ctrl_form_size; // Size of forms for managing reference points int m_shift; // Shift coordinates for adjusting the form location CArrayObj m_list_forms; // List of form objects for managing reference points //--- Create a form object on a base object reference point CFormControl *CreateNewControlPointForm(const int index); //--- Return X and Y (1) screen coordinates of the specified reference point of the graphical object bool GetControlPointCoordXY(const int index,int &x,int &y); //--- Set the parameters of a form object for managing pivot points void SetControlFormParams(CFormControl *form,const int index); public: //--- Set the parameters of the base object of a composite graphical object void SetBaseObj(const ENUM_OBJECT base_type,const string base_name, const long base_chart_id,const int base_subwindow, const int base_pivots,const int ctrl_form_size, const int base_x,const int base_y, const datetime &base_time[],const double &base_price[]); //--- Set the base object (1) time, (2) price, (3) time and price coordinates void SetBaseObjTime(const datetime time,const int index); void SetBaseObjPrice(const double price,const int index); void SetBaseObjTimePrice(const datetime time,const double price,const int index); //--- Set the base object (1) X, (2) Y, (3) X and Y screen coordinates void SetBaseObjCoordX(const int value) { this.m_base_x=value; } void SetBaseObjCoordY(const int value) { this.m_base_y=value; } void SetBaseObjCoordXY(const int value_x,const int value_y) { this.m_base_x=value_x; this.m_base_y=value_y; } //--- (1) Set and (2) return the size of the form of pivot point management control points void SetControlFormSize(const int size); int GetControlFormSize(void) const { return this.m_ctrl_form_size; } //--- Return the pointer to the pivot point form by (1) index and (2) name CFormControl *GetControlPointForm(const int index) { return this.m_list_forms.At(index); } CFormControl *GetControlPointForm(const string name,int &index); //--- Return the number of (1) base object pivot points and (2) newly created form objects for managing control points int GetNumPivotsBaseObj(void) const { return this.m_base_pivots; } int GetNumControlPointForms(void) const { return this.m_list_forms.Total(); } //--- Create form objects on the base object pivot points bool CreateAllControlPointForm(void); //--- (1) Draw a control point on the form, (2) draw a control point on the form and delete it on all other forms void DrawControlPoint(CFormControl *form,const uchar opacity,const color clr); void DrawOneControlPoint(CFormControl *form,const uchar opacity=255,const color clr=CTRL_POINT_COLOR); //--- (1) Draw using a default color, (remove) a control point on the form void DrawControlPoint(CFormControl *form) { this.DrawControlPoint(form,255,CTRL_POINT_COLOR);} void ClearControlPoint(CFormControl *form) { this.DrawControlPoint(form,0,CTRL_POINT_COLOR); } //--- Remove all form objects from the list void DeleteAllControlPointForm(void); //--- Event handler void OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam); //--- Constructor/destructor CGStdGraphObjExtToolkit(const ENUM_OBJECT base_type,const string base_name, const long base_chart_id,const int base_subwindow, const int base_pivots,const int ctrl_form_size, const int base_x,const int base_y, const datetime &base_time[],const double &base_price[]) { this.m_list_forms.Clear(); this.SetBaseObj(base_type,base_name,base_chart_id,base_subwindow,base_pivots,ctrl_form_size,base_x,base_y,base_time,base_price); this.CreateAllControlPointForm(); } CGStdGraphObjExtToolkit(){;} ~CGStdGraphObjExtToolkit(){;} }; //+------------------------------------------------------------------+
Verbesserung der Methode GetControlPointCoordXY(), die die X- und Y-Koordinaten des angegebenen Angelpunkts des grafischen Objekts in Bildschirmkoordinaten zurückgibt.
Bisher gab die Methode einfach die berechneten Koordinaten des angegebenen Angelpunkts des grafischen Objekts zurück. Jetzt müssen wir berücksichtigen, dass grafische Objekte eine unterschiedliche Anzahl von Angelpunkten und eine andere Lage des zentralen Angelpunkts haben können. Daher sollten wir die Berechnung für verschiedene Arten von Objekten in switch() vornehmen. Außerdem sollten wir die Koordinaten des gewünschten Angelpunkts berücksichtigen - einen der an den Rändern des Objekts liegenden oder einen zentralen. Wenn der Index des an die Methode übergebenen Angelpunkts kleiner ist als die Gesamtzahl der Angelpunkte des grafischen Objekts, werden die Koordinaten des Angelpunkts angefordert. Andernfalls werden die Koordinaten des zentralen Angelpunkts angefordert.
Im Moment werde ich den Empfang von X- und Y-Koordinaten nur für die grafischen Objekte mit zwei Angelpunkten an den Rändern und einem einzigen zentralen Punkt implementieren:
//+------------------------------------------------------------------+ //| Return the X and Y coordinates of the specified pivot point | //| of the graphical object in screen coordinates | //+------------------------------------------------------------------+ bool CGStdGraphObjExtToolkit::GetControlPointCoordXY(const int index,int &x,int &y) { //--- Declare form objects, from which we are to receive their screen coordinates CFormControl *form0=NULL, *form1=NULL; //--- Set X and Y to zero - these values will be received in case of a failure x=0; y=0; //--- Depending on the graphical object type switch(this.m_base_type) { //--- Objects drawn using screen coordinates case OBJ_LABEL : case OBJ_BUTTON : case OBJ_BITMAP_LABEL : case OBJ_EDIT : case OBJ_RECTANGLE_LABEL : case OBJ_CHART : case OBJ_EVENT : //--- Write object screen coordinates and return 'true' x=this.m_base_x; y=this.m_base_y; return true; //--- Lines (vertical and horizontal) case OBJ_VLINE : break; case OBJ_HLINE : break; //--- Lines case OBJ_TREND : case OBJ_TRENDBYANGLE : case OBJ_CYCLES : case OBJ_ARROWED_LINE : //--- Channels case OBJ_CHANNEL : case OBJ_STDDEVCHANNEL : case OBJ_REGRESSION : //--- Gann case OBJ_GANNLINE : case OBJ_GANNGRID : //--- Fibo case OBJ_FIBO : case OBJ_FIBOTIMES : case OBJ_FIBOFAN : case OBJ_FIBOARC : case OBJ_FIBOCHANNEL : case OBJ_EXPANSION : //--- Calculate coordinates for forms on the line pivot points if(index<this.m_base_pivots) return(::ChartTimePriceToXY(this.m_base_chart_id,this.m_base_subwindow,this.m_base_time[index],this.m_base_price[index],x,y) ? true : false); //--- Calculate the coordinates for the central form located between the line pivot points else { form0=this.GetControlPointForm(0); form1=this.GetControlPointForm(1); if(form0==NULL || form1==NULL) return false; x=(form0.CoordX()+this.m_shift+form1.CoordX()+this.m_shift)/2; y=(form0.CoordY()+this.m_shift+form1.CoordY()+this.m_shift)/2; return true; } //--- Channels case OBJ_PITCHFORK : break; //--- Gann case OBJ_GANNFAN : break; //--- Elliott case OBJ_ELLIOTWAVE5 : break; case OBJ_ELLIOTWAVE3 : break; //--- Shapes case OBJ_RECTANGLE : break; case OBJ_TRIANGLE : break; case OBJ_ELLIPSE : break; //--- Arrows case OBJ_ARROW_THUMB_UP : break; case OBJ_ARROW_THUMB_DOWN : break; case OBJ_ARROW_UP : break; case OBJ_ARROW_DOWN : break; case OBJ_ARROW_STOP : break; case OBJ_ARROW_CHECK : break; case OBJ_ARROW_LEFT_PRICE : break; case OBJ_ARROW_RIGHT_PRICE : break; case OBJ_ARROW_BUY : break; case OBJ_ARROW_SELL : break; case OBJ_ARROW : break; //--- Graphical objects with time/price coordinates case OBJ_TEXT : break; case OBJ_BITMAP : break; //--- default : break; } return false; } //+------------------------------------------------------------------+
Die Berechnung des Angelpunkts erfolgt anhand der Werte, die in den Arrays m_base_time und m_base_price der Koordinaten des Angelpunkts der Linie festgelegt sind. Zur Berechnung der Zentralpunktkoordinaten werden die Koordinaten von Formularobjekten verwendet, die an Angelpunkten an den Linienkanten angebracht sind. Im Falle einer erfolgreichen Koordinatenberechnung gibt die Methode sofort true zurück. In allen anderen Fällen gibt sie entweder false zurück oder wendet break an, um die Codeausführung im Fall von switch anzuhalten und zum Ende der Methode zu gelangen, wo false zurückgegeben wird.
In der Methode, die den Zeiger auf das Angelpunkt-Formular namentlich zurückgibt, ersetzen wir CForm durch CFormControl:
//+------------------------------------------------------------------+ //| Return the pointer to the pivot point form by name | //+------------------------------------------------------------------+ CFormControl *CGStdGraphObjExtToolkit::GetControlPointForm(const string name,int &index) { index=WRONG_VALUE; for(int i=0;i<this.m_list_forms.Total();i++) { CFormControl *form=this.m_list_forms.At(i); if(form==NULL) continue; if(form.Name()==name) { index=i; return form; } } return NULL; } //+------------------------------------------------------------------+
In der Methode zum Erstellen eines Formularobjekts auf einem Basisobjekt Angelpunkt, ersetzen wir CForm durch CFormControl und setzen die Parameter für ein erfolgreich erstelltes Formularobjekt:
//+------------------------------------------------------------------+ //| Create a form object on a base object reference point | //+------------------------------------------------------------------+ CFormControl *CGStdGraphObjExtToolkit::CreateNewControlPointForm(const int index) { string name=this.m_base_name+"_CP_"+(index<this.m_base_pivots ? (string)index : "X"); CFormControl *form=this.GetControlPointForm(index); if(form!=NULL) return NULL; int x=0, y=0; if(!this.GetControlPointCoordXY(index,x,y)) return NULL; form=new CFormControl(this.m_base_chart_id,this.m_base_subwindow,name,index,x-this.m_shift,y-this.m_shift,this.GetControlFormSize(),this.GetControlFormSize()); //--- Set all the necessary properties for the created form object if(form!=NULL) this.SetControlFormParams(form,index); return form; } //+------------------------------------------------------------------+
In der Methode zum Erzeugen von Formularobjekten auf dem Basisobjekt Angelpunkte, ersetzen wir CForm durch CFormControl und entfernen die Zeichenketten zum Setzen der Parameter eines erzeugten Formularobjekts, da die Parameter jetzt sofort beim Erzeugen des Objekts in der oben betrachteten Methode gesetzt werden:
//+------------------------------------------------------------------+ //| Create form objects on the base object pivot points | //+------------------------------------------------------------------+ bool CGStdGraphObjExtToolkit::CreateAllControlPointForm(void) { bool res=true; //--- In the loop by the number of base object pivot points for(int i=0;i<=this.m_base_pivots;i++) { //--- Create a new form object on the current pivot point corresponding to the loop index CFormControl *form=this.CreateNewControlPointForm(i); //--- If failed to create the form, inform of that and add 'false' to the final result if(form==NULL) { CMessage::ToLog(DFUN,MSG_GRAPH_OBJ_EXT_FAILED_CREATE_CTRL_POINT_FORM); res &=false; } //--- If failed to add the form to the list, inform of that, remove the created form and add 'false' to the final result if(!this.m_list_forms.Add(form)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST); delete form; res &=false; } } //--- Redraw the chart for displaying changes (if successful) and return the final result if(res) ::ChartRedraw(this.m_base_chart_id); return res; } //+------------------------------------------------------------------+
Jetzt läuft die Schleife um die Anzahl der Angelpunkte des Basisobjekts plus einen weiteren. Mit anderen Worten: Die Anzahl der erzeugten Formulare übersteigt die Anzahl der Angelpunkte des grafischen Objekts um eins. Das letzte Formular ist ein zentrales Formular, das für die Bewegung des gesamten grafischen Objekts bestimmt ist.
Die Methode setzt die Parameter eines Formularobjekts zur Verwaltung von Angelpunkten:
//+------------------------------------------------------------------+ //| Set the parameters of a form object for managing pivot points | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::SetControlFormParams(CFormControl *form,const int index) { form.SetBelong(GRAPH_OBJ_BELONG_PROGRAM); // Object is created programmatically form.SetActive(true); // Form object is active form.SetMovable(true); // Movable object int x=(int)::floor((form.Width()-CTRL_POINT_RADIUS*2)/2);// Shift the active area from the form edge form.SetActiveAreaShift(x,x,x,x); // Object active area is located at the center of the form, its size is equal to two CTRL_POINT_RADIUS values form.SetFlagSelected(false,false); // Object is not selected form.SetFlagSelectable(false,false); // Object cannot be selected by mouse form.Erase(CLR_CANV_NULL,0); // Fill in the form with transparent color and set the full transparency //form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,clrSilver); // Draw an outlining rectangle for visual display of the form location //form.DrawRectangle(x,x,form.Width()-x-1,form.Height()-x-1,clrSilver);// Draw an outlining rectangle for visual display of the form active area location form.SetID(index+1); // Set the form ID form.SetControlPointDrawnFlag(false); // Set the flag that the pivot point is not drawn on the form form.Done(); // Save the initial form object state (its appearance) } //+------------------------------------------------------------------+
Hier haben wir die Zeichenketten des Codes aus der oben betrachteten Methode übernommen. Außerdem gibt es ein Flag für einen auf dem Formular gezeichneten Punkt und die Formular-ID.
In der Methode, die den Kontrollpunkt auf dem Formular zeichnet, verlagert man die Berechnung der Formularmitte in einen separaten String, um doppelte Berechnungen zu vermeiden. Nach Beendigung der Methode setze das Flag eines gezeichneten Punktes auf dem Formular:
//+------------------------------------------------------------------+ //| Draw a control point on the form | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::DrawControlPoint(CFormControl *form,const uchar opacity,const color clr) { if(form==NULL) return; int c=int(::floor(form.Width()/2)); // Form center (coordinates) form.DrawCircle(c,c,CTRL_POINT_RADIUS,clr,opacity); // Draw a circle in the form center form.DrawCircleFill(c,c,2,clr,opacity); // Draw a circle in the form center form.SetControlPointDrawnFlag(opacity>0 ? true : false); // Set the flag that the pivot point is drawn on the form } //+------------------------------------------------------------------+
Zurzeit erscheint ein Punkt, wenn der Mauszeiger über das Formular zur Verwaltung des Angelpunkts eines grafischen Objekts bewegt wird. Der Punkt wird erst entfernt, wenn der Mauszeiger das Formular verlässt. Bringt man jedoch alle Kontrollpunkte des Objekts näher zusammen, so dass sich die Formulare an den Enden des grafischen Objekts und das zentrale Formular zu überlappen beginnen, dann führt das Wegbewegen des Cursors von einem Formular dazu, dass sich der Cursor zu einem anderen Formular in der Nähe bewegt. So können wir erreichen, dass alle Punkte auf allen Objektformen angezeigt werden:
Wenn wir das Formular anfassen und bewegen, bewegt sich auch der Angelpunkt des Objekts. Formulare, die aufgrund eines Fehlers sichtbar sind, bleiben dort, wo sie vor der Verschiebung waren. Dieses Verhalten ist nicht korrekt. Deshalb brauchen wir die Methode, die einen Punkt auf einem Formularobjekt eines grafischen Objekts zeichnet und gleichzeitig Punkte auf anderen Formularobjekten desselben Objekts löscht.
Die Methode, die einen Kontrollpunkt auf dem Formular zeichnet und sie auf allen anderen Formularen entfernt:
//+------------------------------------------------------------------+ //| Draw a control point on the form, | //| remove it on all other forms | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::DrawOneControlPoint(CFormControl *form,const uchar opacity=255,const color clr=CTRL_POINT_COLOR) { this.DrawControlPoint(form,opacity,clr); for(int i=0;i<this.GetNumControlPointForms();i++) { CFormControl *ctrl=this.GetControlPointForm(i); if(ctrl==NULL || ctrl.ID()==form.ID()) continue; this.ClearControlPoint(ctrl); } } //+------------------------------------------------------------------+
Hier erhält die Methode den Zeiger auf das Formular, über dem der Cursor schwebt. Zeichne den Punkt auf dem Formular. Dann, in der Schleife durch alle Objektformulare, wählt man das Formular aus und, wenn das Formular nicht an die Methode übergeben wurde, löscht man den Punkt auf ihm.
In der Ereignisbehandlung ersetzen wir den Formulartyp CForm durch CFormControl:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGStdGraphObjExtToolkit::OnChartEvent(const int id,const long& lparam,const double& dparam,const string& sparam) { if(id==CHARTEVENT_CHART_CHANGE) { for(int i=0;i<this.m_list_forms.Total();i++) { CFormControl *form=this.m_list_forms.At(i); if(form==NULL) continue; int x=0, y=0; if(!this.GetControlPointCoordXY(i,x,y)) continue; form.SetCoordX(x-this.m_shift); form.SetCoordY(y-this.m_shift); form.Update(); } ::ChartRedraw(this.m_base_chart_id); } } //+------------------------------------------------------------------+
Machen wir noch einige Verbesserungen bei der Optimierung des Methodencodes in der Klasse des abstrakten grafischen Standardobjekts in \MQL5\Include\DoEasy\Objects\Graph\Standard\GStdGraphObj.mqh. Da wir ähnliche Codefragmente in verschiedenen Methoden haben, ist es sinnvoll, solche Codeblöcke als separate Methoden zu implementieren und sie dort aufzurufen, wo sie benötigt werden, damit der Code leichter zu lesen ist.
In dem öffentlichen und und dem privaten Klassenabschnitt deklarieren wir neue Methoden, die die sich wiederholenden Codefragmente enthalten sollen:
//--- Return (1) the list of dependent objects, (2) dependent graphical object by index and (3) the number of dependent objects CArrayObj *GetListDependentObj(void) { return &this.m_list; } CGStdGraphObj *GetDependentObj(const int index) { return this.m_list.At(index); } int GetNumDependentObj(void) { return this.m_list.Total(); } //--- Return the name of the dependent object by index string NameDependent(const int index); //--- Add the dependent graphical object to the list bool AddDependentObj(CGStdGraphObj *obj); //--- Change X and Y coordinates of the current and all dependent objects bool ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false); //--- Set X and Y coordinates into the appropriate pivot points of a specified subordinate object bool SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj); //--- Return the pivot point data object CLinkedPivotPoint*GetLinkedPivotPoint(void) { return &this.m_linked_pivots; }
...
private: //--- Set the X coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object void SetCoordXToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to); void SetCoordXFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to); //--- Set the Y coordinate (1) from the specified property of the base object to the specified subordinate object, (2) from the base object void SetCoordYToDependentObj(CGStdGraphObj *obj,const int prop_from,const int modifier_from,const int modifier_to); void SetCoordYFromBaseObj(const int prop_from,const int modifier_from,const int modifier_to); //--- Set X and Y coordinates into the appropriate pivot point of a specified subordinate object void SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj,CLinkedPivotPoint *pivot_point,const int index); //--- Set the (1) integer, (2) real and (3) string property to the specified subordinate property void SetDependentINT(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_INTEGER prop,const long value,const int modifier); void SetDependentDBL(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_DOUBLE prop,const double value,const int modifier); void SetDependentSTR(CGStdGraphObj *obj,const ENUM_GRAPH_OBJ_PROP_STRING prop,const string value,const int modifier); public: //--- Event handler
In der Methode, die die Änderungen in den Objekteigenschaften überprüft, entfernen wir den angegebenen Codeblock (der Code wird in eine separate Methode verschoben):
//--- If subordinate objects are attached to the base one (in a composite graphical object) if(this.m_list.Total()>0) { //--- In the loop by the number of added graphical objects, for(int i=0;i<this.m_list.Total();i++) { //--- get the next graphical object, CGStdGraphObj *dep=m_list.At(i); if(dep==NULL) continue; //--- get the data object of its pivot points, CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint(); if(pp==NULL) continue; //--- get the number of coordinate points the object is attached to int num=pp.GetNumLinkedCoords(); //--- In the loop by the object coordinate points, for(int j=0;j<num;j++) { //--- get the number of coordinate points of the base object for setting the X coordinate int numx=pp.GetBasePivotsNumX(j); //--- In the loop by each coordinate point for setting the X coordinate, for(int nx=0;nx<numx;nx++) { //--- get the property for setting the X coordinate, its modifier //--- and set it in the object selected as the current one in the main loop int prop_from=pp.GetPropertyX(j,nx); int modifier_from=pp.GetPropertyModifierX(j,nx); this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx); } //--- Get the number of coordinate points of the base object for setting the Y coordinate int numy=pp.GetBasePivotsNumY(j); //--- In the loop by each coordinate point for setting the Y coordinate, for(int ny=0;ny<numy;ny++) { //--- get the property for setting the Y coordinate, its modifier //--- and set it in the object selected as the current one in the main loop int prop_from=pp.GetPropertyY(j,ny); int modifier_from=pp.GetPropertyModifierY(j,ny); this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny); } } dep.PropertiesCopyToPrevData(); } //--- Move reference control points to new coordinates if(ExtToolkit!=NULL) { for(int i=0;i<this.Pivots();i++) { ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i); } ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); long lparam=0; double dparam=0; string sparam=""; ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } //--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes ::ChartRedraw(m_chart_id); }
Wir fügen den Aufruf einer neuen Methode anstelle eines entfernten Blocks hinzu:
//--- If subordinate objects are attached to the base one (in a composite graphical object) if(this.m_list.Total()>0) { //--- In the loop by the number of added graphical objects, for(int i=0;i<this.m_list.Total();i++) { //--- get the next graphical object, CGStdGraphObj *dep=m_list.At(i); if(dep==NULL) continue; //--- Set X and Y coordinates to all pivot points of a subordinate object and //--- save the current properties of a subordinate graphical object as the previous ones if(this.SetCoordsXYtoDependentObj(dep)) dep.PropertiesCopyToPrevData(); } //--- Move reference control points to new coordinates if(this.ExtToolkit!=NULL) { for(int i=0;i<this.Pivots();i++) { this.ExtToolkit.SetBaseObjTimePrice(this.Time(i),this.Price(i),i); } this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); long lparam=0; double dparam=0; string sparam=""; this.ExtToolkit.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } //--- Upon completion of the loop of handling all bound objects, redraw the chart to display all the changes ::ChartRedraw(m_chart_id); }
Da die derzeitige Logik der Punktlöschung impliziert, dass, wenn der Cursor auf keinem der Formulare gesetzt ist, jedes dieser Formulare ständig neu gezeichnet wird (was suboptimal und ressourcenintensiv ist), implementieren wir die Prüfung, die sicherstellt, dass das Formular zur Punktlöschung nur dann neu gezeichnet wird, wenn der Punkt tatsächlich gelöscht werden soll und noch vorhanden ist in der Methode, die das Formular zur Verwaltung eines Kontrollpunkts eines erweiterten grafischen Standardobjekts neu zeichnet. Außerdem werde ich den Objekttyp des Formulars durch den neuen Typ ersetzen:
//+------------------------------------------------------------------+ //| Redraw the form for managing a control point | //| of an extended standard graphical object | //+------------------------------------------------------------------+ void CGStdGraphObj::RedrawControlPointForms(const uchar opacity,const color clr) { //--- Leave if the object has no toolkit of an extended standard graphical object if(this.ExtToolkit==NULL) return; //--- Get the number of pivot point management forms int total_form=this.GetNumControlPointForms(); //--- In the loop by the number of pivot point management forms for(int i=0;i<total_form;i++) { //--- get the next form object CFormControl *form=this.ExtToolkit.GetControlPointForm(i); if(form==NULL) continue; //--- Draw a point and a circle with a specified non-transparency and color //--- If a point should be completely transparent (deleted) //--- and the form still has the point, delete the point, if(opacity==0 && form.IsControlAlreadyDrawn()) this.ExtToolkit.DrawControlPoint(form,0,clr); //--- otherwise, draw the point with a specified non-transparency and color else this.ExtToolkit.DrawControlPoint(form,opacity,clr); } //--- Get the total number of bound graphical objects int total_dep=this.GetNumDependentObj(); //--- In the loop by all bound graphical objects, for(int i=0;i<total_dep;i++) { //--- get the next graphical object from the list CGStdGraphObj *dep=this.GetDependentObj(i); if(dep==NULL) continue; //--- call the method for it dep.RedrawControlPointForms(opacity,clr); } } //+------------------------------------------------------------------+
Der Punkt wird jetzt nur noch gelöscht, wenn er tatsächlich gelöscht werden soll (die Nicht-Transparenz des Punktes ist auf Null gesetzt) und wenn der Punkt noch vorhanden ist (gezeichneter Punkt-Flag ist gesetzt).
Außerdem überarbeiten wir die Methode, die die X- und Y-Koordinaten des aktuellen und aller abhängigen Objekte ändert, indem wir die Codesegmente löschen, die durch den Aufruf der neuen Methode ersetzt werden:
//+----------------------------------------------------------------------+ //| Change X and Y coordinates of the current and all dependent objects | //+----------------------------------------------------------------------+ bool CGStdGraphObj::ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false) { //--- Set new coordinates for the pivot point specified in 'modifier' if(!this.SetTimePrice(x,y,modifier)) return false; //--- If the object is not a composite graphical object //--- or if subordinate graphical objects are not attached to the object, //--- there is nothing else to do here, return 'true' if(this.ExtToolkit==NULL || this.m_list.Total()==0) return true; //--- Get the graphical object bound to the 'modifier' point CGStdGraphObj *dep=this.GetDependentObj(modifier); if(dep==NULL) return false; //--- Get the object of pivot point data of the bound graphical object CLinkedPivotPoint *pp=dep.GetLinkedPivotPoint(); if(pp==NULL) return false; //--- get the number of coordinate points the object is attached to int num=pp.GetNumLinkedCoords(); //--- In the loop by the object coordinate points, for(int j=0;j<num;j++) { //--- get the number of coordinate points of the base object for setting the X coordinate int numx=pp.GetBasePivotsNumX(j); //--- In the loop by each coordinate point for setting the X coordinate, for(int nx=0;nx<numx;nx++) { //--- get the property for setting the X coordinate, its modifier //--- and set it to the subordinate graphical object attached to the 'modifier' point int prop_from=pp.GetPropertyX(j,nx); int modifier_from=pp.GetPropertyModifierX(j,nx); this.SetCoordXToDependentObj(dep,prop_from,modifier_from,nx); } //--- Get the number of coordinate points of the base object for setting the Y coordinate int numy=pp.GetBasePivotsNumY(j); //--- In the loop by each coordinate point for setting the Y coordinate, for(int ny=0;ny<numy;ny++) { //--- get the property for setting the Y coordinate, its modifier //--- and set it to the subordinate graphical object attached to the 'modifier' point int prop_from=pp.GetPropertyY(j,ny); int modifier_from=pp.GetPropertyModifierY(j,ny); this.SetCoordYToDependentObj(dep,prop_from,modifier_from,ny); } } //--- Save the current properties of a subordinate graphical object as the previous ones dep.PropertiesCopyToPrevData(); //--- Move a reference control point to new coordinates this.ExtToolkit.SetBaseObjTimePrice(this.Time(modifier),this.Price(modifier),modifier); this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); //--- If the flag is active, redraw the chart if(redraw) ::ChartRedraw(m_chart_id); //--- All is successful - return 'true' return true; } //+------------------------------------------------------------------+
Jetzt ist die Methode viel einfacher:
//+----------------------------------------------------------------------+ //| Change X and Y coordinates of the current and all dependent objects | //+----------------------------------------------------------------------+ bool CGStdGraphObj::ChangeCoordsExtendedObj(const int x,const int y,const int modifier,bool redraw=false) { //--- Set new coordinates for the pivot point specified in 'modifier' if(!this.SetTimePrice(x,y,modifier)) return false; //--- If the object is a composite graphical object, //--- and subordinate graphical objects are attached to the object if(this.ExtToolkit!=NULL && this.m_list.Total()>0) { //--- Get the graphical object bound to the 'modifier' point CGStdGraphObj *dep=this.GetDependentObj(modifier); if(dep==NULL) return false; //--- Set X and Y coordinates to all pivot points of a subordinate object and //--- save the current properties of a subordinate graphical object as the previous ones if(this.SetCoordsXYtoDependentObj(dep)) dep.PropertiesCopyToPrevData(); } //--- Move a reference control point to new coordinates this.ExtToolkit.SetBaseObjTimePrice(this.Time(modifier),this.Price(modifier),modifier); this.ExtToolkit.SetBaseObjCoordXY(this.XDistance(),this.YDistance()); //--- If the flag is active, redraw the chart if(redraw) ::ChartRedraw(m_chart_id); //--- All is successful - return 'true' return true; } //+------------------------------------------------------------------+
Implementieren wir die Methode, die X- und Y-Koordinaten in den entsprechenden Angelpunkt eines angegebenen untergeordneten Objekts setzt:
//+------------------------------------------------------------------+ //| Set X and Y coordinates to the associated pivot point | //| of a specified subordinate object by index | //+------------------------------------------------------------------+ void CGStdGraphObj::SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj,CLinkedPivotPoint *pivot_point,const int index) { //--- get the number of coordinate points of the base object for setting the X coordinate int numx=pivot_point.GetBasePivotsNumX(index); //--- In the loop by each coordinate point for setting the X coordinate, for(int nx=0;nx<numx;nx++) { //--- get the property for setting the X coordinate, its modifier //--- and set it to the subordinate graphical object attached to the 'index' point int prop_from=pivot_point.GetPropertyX(index,nx); int modifier_from=pivot_point.GetPropertyModifierX(index,nx); this.SetCoordXToDependentObj(dependent_obj,prop_from,modifier_from,nx); } //--- Get the number of coordinate points of the base object for setting the Y coordinate int numy=pivot_point.GetBasePivotsNumY(index); //--- In the loop by each coordinate point for setting the Y coordinate, for(int ny=0;ny<numy;ny++) { //--- get the property for setting the Y coordinate, its modifier //--- and set it to the subordinate graphical object attached to the 'index' point int prop_from=pivot_point.GetPropertyY(index,ny); int modifier_from=pivot_point.GetPropertyModifierY(index,ny); this.SetCoordYToDependentObj(dependent_obj,prop_from,modifier_from,ny); } } //+------------------------------------------------------------------+
Tatsächlich sind dies genau die Codeblöcke, die aus den Klassenmethoden entfernt wurden. Ich habe die Codelogik in den vorherigen Artikeln berücksichtigt. Außerdem ist sie in den Kommentaren zum Code beschrieben. Ich glaube also, dass es keiner Erklärungen bedarf.
Implementierung der Methode, die X- und Y-Koordinaten in zugehörige Angelpunkte eines bestimmten untergeordneten Objekts setzt:
//+------------------------------------------------------------------+ //| Set X and Y coordinates to associated pivot points | //| of the specified subordinate object | //+------------------------------------------------------------------+ bool CGStdGraphObj::SetCoordsXYtoDependentObj(CGStdGraphObj *dependent_obj) { //--- Get the object of pivot point data of the bound graphical object CLinkedPivotPoint *pp=dependent_obj.GetLinkedPivotPoint(); if(pp==NULL) return false; //--- get the number of coordinate points the object is attached to int num=pp.GetNumLinkedCoords(); //--- In the loop by the object coordinate points, //--- set X and Y to all pivot points of a subordinate object for(int j=0;j<num;j++) this.SetCoordsXYtoDependentObj(dependent_obj,pp,j); return true; } //+------------------------------------------------------------------+
Die Methode ermöglicht das Setzen von Koordinaten für alle Angelpunkte eines untergeordneten Objekts. Wenn dem zusammengesetzten grafischen Objekt weitere grafische Objekte hinzugefügt werden, setzt die Methode die angegebenen Koordinaten in ihnen.
Fügen wir Verbesserungen in die Sammelklasse der grafischen Elemente \MQL5\Include\DoEasy\Collections\GraphElementsCollection.mqh ein.
Da die Standardfunktion ChartTimePriceToXY() gleich zwei Koordinaten — X und Y — zurückgibt, werde ich die Struktur im privaten Bereich deklarieren, um sie zu speichern. Zusätzlich zu den Koordinaten werden in der Struktur auch die Koordinatenverschiebungen relativ zum zentralen Punkt gespeichert. Da das grafische Objekt mehrere Angelpunkte haben kann, deklarieren wir das Array mit dem erstellten Strukturtyp, um die Koordinaten jedes Angelpunktes für das grafische Objekt zu speichern. In diesem Fall enthält jede Array-Zelle die X- und Y-Koordinaten des Bildschirms, die aus den "Zeit/Preis"-Koordinaten umgewandelt wurden, sowie die Verschiebungen der Koordinaten der Angelpunkte relativ zum zentralen Punkt des grafischen Objekts.
Im privaten Teil der Klasse erstellen wir die Struktur und deklarieren das benötigte Array:
//+------------------------------------------------------------------+ //| Collection of graphical objects | //+------------------------------------------------------------------+ #resource "\\"+PATH_TO_EVENT_CTRL_IND; // Indicator for controlling graphical object events packed into the program resources class CGraphElementsCollection : public CBaseObj { private: //--- Pivot point data structure struct SDataPivotPoint { public: int X; // Pivot point X coordinate int Y; // Pivot point Y coordinate int ShiftX; // Pivot point X coordinate shift from the central one int ShiftY; // Pivot point Y coordinate shift from the central one }; SDataPivotPoint m_data_pivot_point[]; // Pivot point data structure array CArrayObj m_list_charts_control; // List of chart management objects CListObj m_list_all_canv_elm_obj; // List of all graphical elements on canvas CListObj m_list_all_graph_obj; // List of all graphical objects CArrayObj m_list_deleted_obj; // List of removed graphical objects CMouseState m_mouse; // "Mouse status" class object bool m_is_graph_obj_event; // Event flag in the list of graphical objects int m_total_objects; // Number of graphical objects int m_delta_graph_obj; // Difference in the number of graphical objects compared to the previous check
Im privaten Abschnitt der Klasse deklarieren wir die Methode, die die Bildschirmkoordinaten jedes Angelpunkts eines grafischen Objekts zurückgibt
private: //--- Find an object present in the collection but not on a chart CGStdGraphObj *FindMissingObj(const long chart_id); CGStdGraphObj *FindMissingObj(const long chart_id,int &index); //--- Find the graphical object present on a chart but not in the collection string FindExtraObj(const long chart_id); //--- Remove the graphical object class object from the graphical object collection list: (1) specified object, (2) by chart ID bool DeleteGraphObjFromList(CGStdGraphObj *obj); void DeleteGraphObjectsFromList(const long chart_id); //--- Move the graphical object class object to the list of removed graphical objects: (1) specified object, (2) by index bool MoveGraphObjToDeletedObjList(CGStdGraphObj *obj); bool MoveGraphObjToDeletedObjList(const int index); //--- Move all objects by chart ID to the list of removed graphical objects void MoveGraphObjectsToDeletedObjList(const long chart_id); //--- Remove the object of managing charts from the list bool DeleteGraphObjCtrlObjFromList(CChartObjectsControl *obj); //--- Set the flags of scrolling the chart with the mouse, context menu and crosshairs tool for the specified chart void SetChartTools(const long chart_id,const bool flag); //--- Return the screen coordinates of each pivot point of the graphical object bool GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[]); public:
Implementieren der Methode außerhalb des Klassenkörpers:
//+------------------------------------------------------------------+ //| Return screen coordinates | //| of each graphical object pivot point | //+------------------------------------------------------------------+ bool CGraphElementsCollection::GetPivotPointCoordsAll(CGStdGraphObj *obj,SDataPivotPoint &array_pivots[]) { //--- If failed to increase the array of structures to match the number of pivot points, //--- inform of that in the journal and return 'false' if(::ArrayResize(array_pivots,obj.Pivots())!=obj.Pivots()) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); return false; } //--- In the loop by the number of graphical object pivot points for(int i=0;i<obj.Pivots();i++) { //--- Convert the object coordinates into screen ones. If failed, inform of that and return 'false' if(!::ChartTimePriceToXY(obj.ChartID(),obj.SubWindow(),obj.Time(i),obj.Price(i),array_pivots[i].X,array_pivots[i].Y)) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_CONV_GRAPH_OBJ_COORDS_TO_XY); return false; } } //--- Depending on the graphical object type switch(obj.TypeGraphObject()) { //--- One pivot point in screen coordinates case OBJ_LABEL : case OBJ_BUTTON : case OBJ_BITMAP_LABEL : case OBJ_EDIT : case OBJ_RECTANGLE_LABEL : case OBJ_CHART : break; //--- One pivot point (price only) case OBJ_HLINE : break; //--- One pivot point (time only) case OBJ_VLINE : break; case OBJ_EVENT : break; //--- Two pivot points and a central one //--- Lines case OBJ_TREND : case OBJ_TRENDBYANGLE : case OBJ_CYCLES : case OBJ_ARROWED_LINE : //--- Channels case OBJ_CHANNEL : case OBJ_STDDEVCHANNEL : case OBJ_REGRESSION : //--- Gann case OBJ_GANNLINE : case OBJ_GANNGRID : //--- Fibo case OBJ_FIBO : case OBJ_FIBOTIMES : case OBJ_FIBOFAN : case OBJ_FIBOARC : case OBJ_FIBOCHANNEL : case OBJ_EXPANSION : //--- Calculate the shifts of all pivot points from the central one and write them to the structure array array_pivots[0].ShiftX=(array_pivots[1].X-array_pivots[0].X)/2; array_pivots[0].ShiftY=(array_pivots[1].Y-array_pivots[0].Y)/2; array_pivots[1].ShiftX=(array_pivots[0].X-array_pivots[1].X)/2; array_pivots[1].ShiftY=(array_pivots[0].Y-array_pivots[1].Y)/2; return true; //--- Channels case OBJ_PITCHFORK : break; //--- Gann case OBJ_GANNFAN : break; //--- Elliott case OBJ_ELLIOTWAVE5 : break; case OBJ_ELLIOTWAVE3 : break; //--- Shapes case OBJ_RECTANGLE : break; case OBJ_TRIANGLE : break; case OBJ_ELLIPSE : break; //--- Arrows case OBJ_ARROW_THUMB_UP : break; case OBJ_ARROW_THUMB_DOWN : break; case OBJ_ARROW_UP : break; case OBJ_ARROW_DOWN : break; case OBJ_ARROW_STOP : break; case OBJ_ARROW_CHECK : break; case OBJ_ARROW_LEFT_PRICE : break; case OBJ_ARROW_RIGHT_PRICE : break; case OBJ_ARROW_BUY : break; case OBJ_ARROW_SELL : break; case OBJ_ARROW : break; //--- Graphical objects with time/price coordinates case OBJ_TEXT : break; case OBJ_BITMAP : break; //--- default: break; } return false; } //+------------------------------------------------------------------+
Vorerst werden nur die Bildschirmkoordinaten der grafischen Objekte mit zwei Angelpunkten und einem zentralen in der Struktur gesetzt.
Die Methode erhält den Zeiger auf das grafische Objekt, dessen Angelpunkt-Koordinaten gesetzt werden sollen, in dem Struktur-Array, das ebenfalls per Link an die Methode übergeben wird. Bei erfolgreicher Koordinatenkonvertierung gibt die Methode true zusammen mit dem vollständig gefüllten Strukturarray mit Bildschirmkoordinaten für jeden Angelpunkt eines grafischen Objekts zurück. Bei einem Fehler gibt die Methode false zurück.
In der Ereignisbehandlung der Klasse müssen wir die Formverschiebung der Objektverwaltung so behandeln, dass das Objekt in seiner Gesamtheit verschoben wird, wenn es sich um einen zentralen Punkt handelt. Um dies zu erreichen, müssen wir die Verschiebungen der Randformen relativ zur zentralen Form (die zum Ziehen des Objekts verwendet wird) berechnen und beide Angelpunkte um einen in der Struktur berechneten und festgelegten Verschiebungswert verschieben. Auf diese Weise werden alle Angelpunkte um denselben Wert verschoben wie der zentrale, der mit der Maus gezogen wird.
Fügen wir eine solche Behandlung eines zentralen Kontrollpunktes (Formular) hinzufügen:
//+------------------------------------------------------------------+ //| Event handler | //+------------------------------------------------------------------+ void CGraphElementsCollection::OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { CGStdGraphObj *obj_std=NULL; // Pointer to the standard graphical object CGCnvElement *obj_cnv=NULL; // Pointer to the graphical element object on canvas ushort idx=ushort(id-CHARTEVENT_CUSTOM); if(id==CHARTEVENT_OBJECT_CHANGE || id==CHARTEVENT_OBJECT_DRAG || id==CHARTEVENT_OBJECT_CLICK || idx==CHARTEVENT_OBJECT_CHANGE || idx==CHARTEVENT_OBJECT_DRAG || idx==CHARTEVENT_OBJECT_CLICK) { //--- Calculate the chart ID //--- If the event ID corresponds to an event from the current chart, the chart ID is received from ChartID //--- If the event ID corresponds to a user event, the chart ID is received from lparam //--- Otherwise, the chart ID is assigned to -1 long param=(id==CHARTEVENT_OBJECT_CLICK ? ::ChartID() : idx==CHARTEVENT_OBJECT_CLICK ? lparam : WRONG_VALUE); long chart_id=(param==WRONG_VALUE ? (lparam==0 ? ::ChartID() : lparam) : param); //--- Get the object, whose properties were changed or which was relocated, //--- from the collection list by its name set in sparam obj_std=this.GetStdGraphObject(sparam,chart_id); //--- If failed to get the object by its name, it is not on the list, //--- which means its name has been changed if(obj_std==NULL) { //--- Let's search the list for the object that is not on the chart obj_std=this.FindMissingObj(chart_id); //--- If failed to find the object here as well, exit if(obj_std==NULL) return; //--- Get the name of the renamed graphical object on the chart, which is not in the collection list string name_new=this.FindExtraObj(chart_id); //--- set a new name for the collection list object, which does not correspond to any graphical object on the chart, //--- and send an event with the new name of the object to the control program chart if(obj_std.SetNamePrev(obj_std.Name()) && obj_std.SetName(name_new)) ::EventChartCustom(this.m_chart_id_main,GRAPH_OBJ_EVENT_RENAME,obj_std.ChartID(),obj_std.TimeCreate(),obj_std.Name()); } //--- Update the properties of the obtained object //--- and check their change obj_std.PropertiesRefresh(); obj_std.PropertiesCheckChanged(); } //--- Handle standard graphical object events in the collection list for(int i=0;i<this.m_list_all_graph_obj.Total();i++) { //--- Get the next graphical object and obj_std=this.m_list_all_graph_obj.At(i); if(obj_std==NULL) continue; //--- call its event handler obj_std.OnChartEvent((id<CHARTEVENT_CUSTOM ? id : idx),lparam,dparam,sparam); } //--- Handle chart changes for extended standard objects if(id==CHARTEVENT_CHART_CHANGE || idx==CHARTEVENT_CHART_CHANGE) { CArrayObj *list=this.GetListStdGraphObjectExt(); if(list!=NULL) { for(int i=0;i<list.Total();i++) { obj_std=list.At(i); if(obj_std==NULL) continue; obj_std.OnChartEvent(CHARTEVENT_CHART_CHANGE,lparam,dparam,sparam); } } } //--- Handling mouse events of graphical objects on canvas //--- If the event is not a chart change else { //--- Check whether the mouse button is pressed bool pressed=(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)==MOUSE_BUTT_KEY_STATE_LEFT ? true : false); ENUM_MOUSE_FORM_STATE mouse_state=MOUSE_FORM_STATE_NONE; //--- Declare static variables for the active form and status flags static CForm *form=NULL; static bool pressed_chart=false; static bool pressed_form=false; static bool move=false; //--- Declare static variables for the index of the form for managing an extended standard graphical object and its ID static int form_index=WRONG_VALUE; static long graph_obj_id=WRONG_VALUE; //--- If the button is not pressed on the chart and the movement flag is not set, get the form, above which the cursor is located if(!pressed_chart && !move) form=this.GetFormUnderCursor(id,lparam,dparam,sparam,mouse_state,graph_obj_id,form_index); //--- If the button is not pressed, reset all flags and enable the chart tools if(!pressed) { pressed_chart=false; pressed_form=false; move=false; this.SetChartTools(::ChartID(),true); } //--- If this is a mouse movement event and the movement flag is active, move the form, above which the cursor is located (if the pointer to it is valid) if(id==CHARTEVENT_MOUSE_MOVE && move) { if(form!=NULL) { //--- calculate the cursor movement relative to the form coordinate origin int x=this.m_mouse.CoordX()-form.OffsetX(); int y=this.m_mouse.CoordY()-form.OffsetY(); //--- get the width and height of the chart the form is located at int chart_width=(int)::ChartGetInteger(form.ChartID(),CHART_WIDTH_IN_PIXELS,form.SubWindow()); int chart_height=(int)::ChartGetInteger(form.ChartID(),CHART_HEIGHT_IN_PIXELS,form.SubWindow()); //--- If the form is not within an extended standard graphical object if(form_index==WRONG_VALUE) { //--- Adjust the calculated form coordinates if the form is out of the chart range if(x<0) x=0; if(x>chart_width-form.Width()) x=chart_width-form.Width(); if(y<0) y=0; if(y>chart_height-form.Height()) y=chart_height-form.Height(); //--- If the one-click trading panel is not present on the chart, if(!::ChartGetInteger(form.ChartID(),CHART_SHOW_ONE_CLICK)) { //--- calculate the form coordinates so that the form does not overlap with the one-click trading panel button if(y<17 && x<41) y=17; } //--- If the one-click trading panel is on the chart, else { //--- calculate the form coordinates so that the form does not overlap with the one-click trading panel if(y<80 && x<192) y=80; } } //--- If the form is included into the extended standard graphical object else { if(graph_obj_id>WRONG_VALUE) { //--- Get the list of objects by object ID (there should be one object) CArrayObj *list_ext=CSelect::ByGraphicStdObjectProperty(GetListStdGraphObjectExt(),GRAPH_OBJ_PROP_ID,0,graph_obj_id,EQUAL); //--- If managed to obtain the list and it is not empty, if(list_ext!=NULL && list_ext.Total()>0) { //--- get the graphical object from the list CGStdGraphObj *ext=list_ext.At(0); //--- If the pointer to the object has been received, if(ext!=NULL) { //--- get the object type ENUM_OBJECT type=ext.GraphObjectType(); //--- If the object is built using screen coordinates, set the coordinates to the object if(type==OBJ_LABEL || type==OBJ_BUTTON || type==OBJ_BITMAP_LABEL || type==OBJ_EDIT || type==OBJ_RECTANGLE_LABEL) { ext.SetXDistance(x); ext.SetYDistance(y); } //--- otherwise, if the object is built based on time/price coordinates, else { //--- calculate the coordinate shift int shift=(int)::ceil(form.Width()/2)+1; //--- If the form is located on one of the graphical object pivot points, if(form_index<ext.Pivots()) { //--- limit the form coordinates so that they do not move beyond the chart borders if(x+shift<0) x=-shift; if(x+shift>chart_width) x=chart_width-shift; if(y+shift<0) y=-shift; if(y+shift>chart_height) y=chart_height-shift; //--- set the calculated coordinates to the object ext.ChangeCoordsExtendedObj(x+shift,y+shift,form_index); } //--- If the form is central for managing all pivot points of a graphical object else { //--- Get screen coordinates of all object pivot points and write them to the m_data_pivot_point structure if(this.GetPivotPointCoordsAll(ext,m_data_pivot_point)) { //--- In the loop by the number of object pivot points, for(int i=0;i<(int)this.m_data_pivot_point.Size();i++) { //--- limit the screen coordinates of the current pivot point so that they do not move beyond the chart borders if(x+shift-this.m_data_pivot_point[i].ShiftX<0) x=-shift+m_data_pivot_point[i].ShiftX; if(x+shift+this.m_data_pivot_point[i].ShiftX>chart_width) x=chart_width-shift-this.m_data_pivot_point[i].ShiftX; if(y+shift+this.m_data_pivot_point[i].ShiftY<0) y=-shift-this.m_data_pivot_point[i].ShiftY; if(y+shift-this.m_data_pivot_point[i].ShiftY>chart_height) y=chart_height-shift+this.m_data_pivot_point[i].ShiftY; //--- set the calculated coordinates to the current object pivot point ext.ChangeCoordsExtendedObj(x+shift-this.m_data_pivot_point[i].ShiftX,y+shift-this.m_data_pivot_point[i].ShiftY,i); } } } } } } } } //--- Move the form by the obtained coordinates form.Move(x,y,true); } } //--- Display debugging comments on the chart Comment ( (form!=NULL ? form.Name()+":" : ""),"\n", EnumToString((ENUM_CHART_EVENT)id),"\n", EnumToString(this.m_mouse.ButtonKeyState(id,lparam,dparam,sparam)), "\n",EnumToString(mouse_state), "\npressed=",pressed,", move=",move,(form!=NULL ? ", Interaction="+(string)form.Interaction() : ""), "\npressed_chart=",pressed_chart,", pressed_form=",pressed_form, "\nform_index=",form_index,", graph_obj_id=",graph_obj_id ); //--- If the cursor is not above the form if(form==NULL) { //--- If the mouse button is pressed if(pressed) { //--- If the button is still pressed and held on the form, exit if(pressed_form) { return; } //--- If the button hold flag is not enabled yet, set the flags and enable chart tools if(!pressed_chart) { pressed_chart=true; // Button is held on the chart pressed_form=false; // Cursor is not above the form move=false; // movement disabled this.SetChartTools(::ChartID(),true); } } //--- If the mouse button is not pressed else { //--- Get the list of extended standard graphical objects CArrayObj *list_ext=GetListStdGraphObjectExt(); //--- In the loop by all extended graphical objects, int total=list_ext.Total(); for(int i=0;i<total;i++) { //--- get the next graphical object CGStdGraphObj *obj=list_ext.At(i); if(obj==NULL) continue; //--- and redraw it without a point and a circle obj.RedrawControlPointForms(0,CTRL_POINT_COLOR); } } } //--- If the cursor is above the form else { //--- If the button is still pressed and held on the chart, exit if(pressed_chart) { return; } //--- If the flag of holding the button on the form is not set yet if(!pressed_form) { pressed_chart=false; // The button is not pressed on the chart this.SetChartTools(::ChartID(),false); //--- 'The cursor is inside the form, no mouse buttons are clicked' event handler if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_NOT_PRESSED) { //--- If the cursor is above the form for managing the pivot point of an extended graphical object, if(graph_obj_id>WRONG_VALUE) { //--- get the object by its ID and by the chart ID CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID()); if(graph_obj!=NULL) { //--- Get the toolkit of an extended standard graphical object CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit(); if(toolkit!=NULL) { //--- Draw a point with a circle on the form and delete it on all other forms toolkit.DrawOneControlPoint(form); } } } } //--- 'The cursor is inside the form, a mouse button is clicked (any)' event handler if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_PRESSED) { this.SetChartTools(::ChartID(),false); //--- If the flag of holding the form is not set yet if(!pressed_form) { pressed_form=true; // set the flag of pressing on the form pressed_chart=false; // disable the flag of pressing on the form } } //--- 'The cursor is inside the form, the mouse wheel is being scrolled' event handler workpiece if(mouse_state==MOUSE_FORM_STATE_INSIDE_FORM_WHEEL) { } //--- 'The cursor is inside the active area, the mouse buttons are not clicked' event handler if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_NOT_PRESSED) { //--- Set the cursor shift relative to the form initial coordinates form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX()); form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY()); //--- If the cursor is above the active area of the form for managing the pivot point of an extended graphical object, if(graph_obj_id>WRONG_VALUE) { //--- get the object by its ID and by the chart ID CGStdGraphObj *graph_obj=this.GetStdGraphObjectExt(graph_obj_id,form.ChartID()); if(graph_obj!=NULL) { //--- Get the toolkit of an extended standard graphical object CGStdGraphObjExtToolkit *toolkit=graph_obj.GetExtToolkit(); if(toolkit!=NULL) { //--- Draw a point with a circle on the form and delete it on all other forms toolkit.DrawOneControlPoint(form); } } } } //--- 'The cursor is inside the active area, any mouse button is clicked' event handler if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_PRESSED && !move) { pressed_form=true; // the flag of holding the mouse button on the form //--- If the left mouse button is pressed if(this.m_mouse.IsPressedButtonLeft()) { //--- Set flags and form parameters move=true; // movement flag form.SetInteraction(true); // flag of the form interaction with the environment form.BringToTop(); // form on the background - above all others this.ResetAllInteractionExeptOne(form); // Reset interaction flags for all forms except the current one form.SetOffsetX(this.m_mouse.CoordX()-form.CoordX()); // Cursor shift relative to the X coordinate form.SetOffsetY(this.m_mouse.CoordY()-form.CoordY()); // Cursor shift relative to the Y coordinate } } //--- 'The cursor is inside the active area, the mouse wheel is being scrolled' event handler workpiece if(mouse_state==MOUSE_FORM_STATE_INSIDE_ACTIVE_AREA_WHEEL) { } //--- 'The cursor is inside the window scrolling area, no mouse buttons are clicked' event handler workpiece if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_NOT_PRESSED) { } //--- 'The cursor is inside the window scrolling area, a mouse button is clicked (any)' event handler workpiece if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_PRESSED) { } //--- 'The cursor is inside the window scrolling area, the mouse wheel is being scrolled' event handler workpiece if(mouse_state==MOUSE_FORM_STATE_INSIDE_SCROLL_AREA_WHEEL) { } } } } } //+------------------------------------------------------------------+
Neben der neuen Ereignisbehandlung für das Verschieben des zentralen Verwaltungsformulars habe ich auch den Aufruf der Methode hinzugefügt, die einen Punkt auf dem Objektformular unter dem Cursor zeichnet und diese Punkte auf anderen Formularen des grafischen Objekts löscht. Damit kann man vermeiden, dass gleichzeitig Punkte auf mehreren Formularobjekten gezeichnet werden, wenn diese nahe beieinander liegen und sich überlappen, wie oben gezeigt.
Derzeit ist alles bereit, um die neue Funktionsweise zu testen.
Test
Um den Test durchzuführen, verwenden wir den EA aus dem vorherigen Artikel und speichern ihn in \MQL5\Experts\TestDoEasy\Part99\ als TestDoEasyPart99.mq5.
Ich muss keine Änderungen in den EA einfügen — alle Änderungen werden erst einmal nur in den Bibliotheksklassen vorgenommen.
Kompilieren Sie den EA und starten Sie ihn auf dem Chart:
Wie wir sehen, funktionieren alle Beschränkungen in Bezug auf Angelpunkte, die die Grenzen des Charts überschreiten, korrekt, wenn wir das zusammengesetzte grafische Objekt in der Form verschieben, in der es erstellt wurde. Wenn wir jedoch die Position der Angelpunkte relativ zu ihrer ursprünglichen Position "umkehren", wird die "Konfiguration" des Objekts verzerrt, wenn der Angelpunkt die Grenzen des Charts überschreitet. Dies impliziert die falsche Berechnung der Einschränkungen und die Abhängigkeit davon, welcher Angelpunkt den rechten, linken, oberen oder unteren Rand des Charts überschreitet.
Dies ist nicht verwunderlich, da die Verschiebungen der Angelpunkte relativ zum zentralen Punkt berechnet werden, was bedeutet, dass ein Punkt eine positive Verschiebung hat, während der zweite Punkt eine negative Verschiebung hat. Der Fehler bei der Berechnung der Begrenzung tritt auf, wenn man die Lage der Angelpunkte relativ zum zentralen Punkt ändert. Ich werde dies im nächsten Artikel korrigieren.
Was kommt als Nächstes?
Im nächsten Artikel werde ich meine Arbeit an zusammengesetzten grafischen Objekten und ihrer Funktionsweisen fortsetzen.
*Frühere Artikel dieser Serie:
Grafiken in der Bibliothek DoEasy (Teil 93): Vorbereiten der Funktionen zur Erstellung zusammengesetzter grafischer Objekte
Grafiken in der Bibliothek DoEasy (Teil 94): Bewegen und Löschen zusammengesetzter grafischer Objekte
Grafiken in der DoEasy-Bibliothek (Teil 95): Steuerelemente für zusammengesetzte grafische Objekte
Grafiken in der DoEasy-Bibliothek (Teil 96): Grafiken in Formularobjekten und Behandlung von Mausereignissen
Grafiken in der DoEasy-Bibliothek (Teil 97): Unabhängige Handhabung der Bewegung von Formularobjekten
Grafiken in der Bibliothek DoEasy (Teil 98): Verschieben von Angelpunkten erweiterter grafischer Standardobjekte
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/10584





- 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.