
Entwicklung eines Replay-Systems (Teil 64): Abspielen des Dienstes (V)
Einführung
Im vorigen Artikel, Entwicklung eines Replay Systems (Teil 63): Abspielen des Dienstes (IV), haben wir einen Mechanismus entwickelt und implementiert, der es den Nutzern ermöglicht, die maximale Anzahl der Ticks einzustellen, die für die Konstruktion eines Balkens im Chart verwendet werden. Obwohl der Hauptzweck dieser Steuerung darin besteht, sicherzustellen, dass die Balkenkonstruktion andere kritische Bereiche der Wiedergabe/Simulation nicht beeinträchtigt, werden die tatsächlichen Volumenwerte, die angezeigt werden sollen, nicht beeinflusst oder verzerrt.
Wenn Sie sich jedoch das Video in diesem Artikel genau angesehen oder die Wiedergabe/Simulation selbst zusammengestellt und getestet haben, ist Ihnen vielleicht aufgefallen, dass das System von Zeit zu Zeit unerwartet in den Pausenmodus wechselt. Seltsamerweise geschah dies, ohne dass der Kontrollindikator eine Veränderung anzeigte. Es war nicht klar, warum wir irgendwie vom Abspielmodus in den Pausenmodus übergegangen sind. Ich gebe zu, das ist in der Tat recht merkwürdig. Sie fragen sich wahrscheinlich, wie so etwas passieren konnte. Ich habe mir die gleiche Frage gestellt und versucht zu verstehen, warum das System immer wieder an bestimmten Stellen pausiert. Im nächsten Abschnitt werde ich die von mir entwickelte Lösung erläutern und erklären, wie das Problem zu verstehen ist.
Verstehen und Beheben des automatischen Pausenmodus
Das eigentliche Problem besteht darin, nicht nur zu verstehen, warum die Wiedergabe/Simulation automatisch in den Pausenmodus schaltet. Die wahre Frustration liegt in der Erkenntnis, dass die Entwickler von MetaTrader 5 sich wahrscheinlich bald darum kümmern werden, was aber zum Zeitpunkt des Verfassens dieses Artikels bestimmte Testszenarien und Anwendungsfälle mit grafischen Objekten zu ziemlichen Kopfschmerzen macht. Dies liegt daran, dass bestimmte Objektzustände ohne ersichtlichen Grund oder Erklärung geändert werden können.
Vielleicht bin ich etwas zu hart. Erinnern wir uns aber daran, wie wichtig diese Objekte für unsere Wiedergabe/Simulation sind und wie wir sie verwenden, um die Kontrolle zu behalten. Dies ist vor allem für diejenigen von Bedeutung, die diese Artikelserie über die Wiedergabe/Simulation noch nicht verfolgt haben.Wenn der Dienst der Wiedergabe/Simulation initialisiert wird, öffnet er einen Chart, lädt Ticks aus einer Datei, erstellt ein nutzerdefiniertes Symbol und platziert schließlich einen Indikator auf dem Chart. Dieser Indikator ist unser Kontrollindikator, der für die Steuerung des Verhaltens des Replays/Simulators verantwortlich ist.
In diesem Stadium verwenden wir keine globalen Terminalvariablen mehr für den Zugriff auf oder die Übertragung von Informationen zwischen dem Kontrollindikator und dem Wiedergabe/Simulationsdienst. Stattdessen haben wir eine andere Technik gewählt, bei der die Informationen zwischen den beiden Komponenten so fließen, dass der Nutzer keinen Einfluss auf die ausgetauschten Daten nehmen kann.
Im Wesentlichen verwendet der Dienst nutzerdefinierte Ereignisse, um Informationen an den Kontrollindikator zu senden. Der Kontrollindikator wiederum sendet einen Teil dieser Informationen über einen Puffer an den Dienst zurück. Ich sage „teilweise“, weil eine Information anders übertragen wird, damit der Dienst nicht ständig aus dem Puffer lesen muss. Das Auslesen des Puffers würde bedeuten, dass mehr Daten übertragen werden, als wirklich notwendig sind. Aus diesem Grund kann der Dienst auf ein vom Kontrollindikator gepflegtes Objekt zugreifen, ohne es zu verändern. Bei diesem Objekt handelt es sich um die Schaltfläche, die den aktuellen Ausführungsstatus repräsentiert, d. h. die Schaltfläche, mit der der Nutzer angeben kann, ob sich das System im Abspiel- oder im Pausenmodus befinden soll.
Sie können dieses Verhalten im Quellcode der Datei C_Replay.mqh beobachten. Um dies zu verdeutlichen, sehen Sie sich den folgenden Codeschnipsel an, der Teil der Klasse C_Replay.mqh ist:35. //+------------------------------------------------------------------+ 36. inline void UpdateIndicatorControl(void) 37. { 38. static bool bTest = false; 39. double Buff[]; 40. 41. if (m_IndControl.Handle == INVALID_HANDLE) return; 42. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 43. { 44. if (bTest) 45. m_IndControl.Mode = (ObjectGetInteger(m_Infos.IdReplay, def_ObjectCtrlName((C_Controls::eObjectControl)C_Controls::ePlay), OBJPROP_STATE) == 1 ? C_Controls::ePause : C_Controls::ePlay); 46. else 47. { 48. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 49. m_IndControl.Memory.dValue = Buff[0]; 50. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 51. if (bTest = ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)) 52. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 53. } 54. }else 55. { 56. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 57. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 58. m_IndControl.Memory._8b[7] = 'D'; 59. m_IndControl.Memory._8b[6] = 'M'; 60. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 61. bTest = false; 62. } 63. } 64. //+------------------------------------------------------------------+
Das Originalfragment des Quellcodes aus der Datei C_Replay.mqh
Beachten Sie, dass wir in Zeile 38 eine statische Variable haben, die der Prozedur mitteilen soll, ob wir das Objekt direkt lesen können oder ob wir stattdessen aus dem Puffer des Kontrollindikators lesen sollen. Wenn wir in Zeile 60 ein nutzerdefiniertes Ereignis senden, erzwingen wir in Zeile 61 ausdrücklich, dass der nächste Aufruf aus dem Puffer des Kontrollindikators lesen muss. Auf diese Weise wird der Puffer nicht ständig gelesen, sondern nur von Zeit zu Zeit und in gut abgestuften Intervallen, was eine zu starke Auswirkungen auf die Leistung der Wiedergabe/Simulation verhindert. Der eigentliche Trick liegt jedoch in Zeile 51, wo wir das System anweisen, bei den nächsten Lesevorgängen nicht den Puffer zu verwenden, sondern stattdessen direkt auf das grafische Objekt zuzugreifen. Dies geschieht jedoch nur, wenn wir uns im Abspielmodus befinden. Wenn wir uns im Pausenmodus befinden, ist die Reaktionszeit nicht so kritisch oder besorgniserregend.
Wenn wir also im Abspielmodus sind, führen wir ab dem dritten Aufruf die Zeile 45 aus. Dies gilt so lange, bis der Nutzer das System ausdrücklich in den Pausenmodus versetzt. Wenn dies geschieht, wird die Funktion LoopEventOnTime in der Klasse C_Replay beendet. Da der Nutzer jedoch nur angegeben hat, dass der Dienst in den Pausemodus gehen soll, wird die Funktion LoopEventOnTime erneut aufgerufen, und die obige Logik setzt die Beobachtung des Kontrollindikators fort. Zu diesem Zeitpunkt überwachen wir den Status der Steuerung noch über das grafische Objekt. Hier stoßen wir auf eine Einschränkung, die uns daran hindert, etwas anderes zu tun. Dies ist jedoch nicht die Ursache für den automatischen Wechsel in den Pausenmodus. Der Pausenmodus wird nicht durch den Dienst selbst ausgelöst. Sie wird innerhalb des Kontrollindikators ausgelöst. Und die Ursache ist genau das nutzerdefinierte Ereignis, das in Zeile 60 des obigen Codeschnipsels gesendet wird. Jetzt werden die Dinge noch komplizierter. Wie kann etwas, das wir zum Auslösen eines nutzerdefinierten Ereignisses verwenden, dazu führen, dass der Dienst eine falsche Anzeige vom Kontrollindikator erhält, dass der Nutzer in den Pausenmodus gewechselt hat? Das ist absolut verrückt. Es ist wirklich bizarr. Aber irgendwie bewirkt das nutzerdefinierte Ereignis, dass der Indikator die OBJPROP_STATE-Eigenschaft des Objekts, das der Dienst überwacht, ändert. Wenn sich diese Eigenschaft ändert, veranlasst Zeile 45 im vorherigen Ausschnitt den Dienst, fälschlicherweise anzunehmen, dass der Indikator in den Pausenmodus gewechselt hat. Dadurch wird die Funktion LoopEventOnTime beendet und muss neu initialisiert werden. Wenn LoopEventOnTime jedoch erneut ausgeführt wird und den Wert der Eigenschaft OBJPROP_STATE überprüft, wird ein falscher Wert angezeigt. Dies führt dazu, dass der Dienst in den Pausemodus wechselt, obwohl die Anzeige immer noch anzeigt, dass das System normal im Abspielmodus läuft.
Wenn Sie diesen Fehler wirklich verstanden haben, denken Sie wahrscheinlich, dass das gesamte Problem auf die Tatsache zurückzuführen ist, dass wir ein Objekt im Chart überwachen, anstatt den Inhalt des Puffers des Indikators zu lesen. Und ja, ich stimme zu. Die Ursache des Problems ist, dass der Dienst die Eigenschaft OBJPROP_STATE aus einem Chartobjekt liest, auf das er eigentlich nicht zugreifen sollte. Noch einmal, ich stimme zu. Dies rechtfertigt jedoch immer noch nicht die Tatsache, dass die Eigenschaft OBJPROP_STATE geändert wird, nur weil ein nutzerdefiniertes Ereignis ausgelöst wurde. Ein Fehler rechtfertigt nicht den anderen. In jedem Fall gibt es zwei direkte Lösungen für dieses Problem. Eine Möglichkeit wäre die Verwendung einer anderen Eigenschaft, die es dem Dienst ermöglicht zu beobachten, was ein bestimmtes Chartobjekt tut. Obwohl diese Lösung das Problem lösen würde, werde ich sie nicht umsetzen. Der Grund dafür ist, dass wir uns mit einem anderen Thema befassen müssen, oder besser gesagt, etwas, das wir noch umsetzen müssen.
Obwohl das Lesen aus dem Indikatorpuffer etwas mehr Zeit in Anspruch nehmen kann als das Beobachten eines Chartobjekts, habe ich mich für das Lesen aus dem Indikatorpuffer entschieden. Der Grund dafür ist, dass ich eine Funktion implementieren werde, die derzeit nicht verfügbar ist, aber in früheren Versionen dieser Wiedergabe/Simulation vorhanden war. Diese Funktion ist der Schnellvorlaufmodus. Nachdem die notwendigen Änderungen vorgenommen und die statische Variable aus der Routine entfernt wurde, sieht die endgültige Implementierung nun wie folgt aus:
35. //+------------------------------------------------------------------+ 36. inline void UpdateIndicatorControl(void) 37. { 38. double Buff[]; 39. 40. if (m_IndControl.Handle == INVALID_HANDLE) return; 41. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 42. { 43. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 44. m_IndControl.Memory.dValue = Buff[0]; 45. if ((C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus] != C_Controls::eTriState) 46. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 47. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 48. }else 49. { 50. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 51. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 52. m_IndControl.Memory._8b[7] = 'D'; 53. m_IndControl.Memory._8b[6] = 'M'; 54. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 55. } 56. } 57. //+------------------------------------------------------------------+
Geändertes Quellcodefragment in der Datei C_Replay.mqh
Trotz aller Unannehmlichkeiten erweist sich dies letztlich als die beste Lösung für uns, auch wenn sie etwas Merkwürdiges aufzeigt: Bestimmte Eigenschaften bestimmter Objekte sind nicht ganz zuverlässig. Zumindest nicht zum Zeitpunkt der Abfassung dieses Artikels. Wenn Sie jedoch aus irgendeinem Grund einen Dienst benötigen, der Daten aus einem Objekt im Chart liest, empfehle ich die Verwendung der Eigenschaft OBJPROP_TOOLTIP. Obwohl er vom Typ String ist, hatte ich keine Probleme, ihn in Tests zur Übertragung von Informationen zu verwenden, um festzustellen, ob sich der Kontrollindikator im Pausen- oder Abspielmodus befindet. Obwohl dieser Ansatz für den Zugriff auf das Objekt ausreichend gewesen wäre, hätte er es uns nicht ermöglicht, die Schnellvorlauffunktion richtig zu implementieren. Dies würde zahlreiche weitere Änderungen am Code erfordern, und am Ende müssten wir noch die UpdateIndicadorControl-Prozedur ändern, damit sie wie gezeigt funktioniert.
Nachdem wir uns nun mit diesem Problem befasst haben, kommen wir zu einem anderen (ebenso frustrierenden), das in dem Video im Artikel Entwicklung eines Replay Systems (Teil 62): Abspielen des Dienstes (III) gezeigt wurde. Um zu erklären, wie dieses spezielle Problem gelöst wurde, wenden wir uns nun einem neuen Thema zu.
Behebung des Problems eines Speicherabzugs
Viele von Ihnen waren wahrscheinlich überrascht, dass die Anwendung plötzlich während der Ausführung fehlschlägt. Insbesondere dann, wenn das Chart gerade geschlossen wurde oder bereits geschlossen war. Ein weniger erfahrener Entwickler könnte sofort behaupten, dass der Fehler auf etwas in MetaTrader 5 oder MQL5 selbst zurückzuführen ist. Schließlich ist es einfacher, die Schuld abzuschieben, als die Verantwortung dafür zu übernehmen, dass sich Ihr Programm nicht richtig verhält oder zumindest Fehler enthält, für die Sie sich nicht die Zeit genommen haben. Wenn Sie sich darauf verlassen, dass die Plattform alles für Sie erledigt, sei es aufgrund mangelnder Kenntnisse oder weil Sie einer bestimmten Funktion Vorrang vor der Behebung von Fehlern einräumen, unterschreiben Sie im Grunde eine Erklärung der Unwissenheit. Oder man gibt zu, dass man zu naiv ist. In Wahrheit wird die Plattform nur das tun, wofür sie gedacht ist. Es liegt in Ihrer Verantwortung als Entwickler, den Rest zu erledigen, Fehler zu beheben und sicherzustellen, dass Ihre Anwendung ordnungsgemäß funktioniert.
Einige Zeit lang habe ich die Lösung bestimmter Probleme und die Verbesserung von Teilen des Codes vernachlässigt und aufgeschoben. Weil ich mich auf die Entwicklung anderer Funktionen konzentriert habe. Ich wusste nicht, ob ich überhaupt einen bestimmten Entwicklungsmeilenstein erreichen würde, also setzte ich keine Prioritäten bei der Behebung von Problemen oder der Verfeinerung des Codes. Alles, was ich wollte, war, dass der Code läuft oder eine Reihe von Funktionen bietet. Es ist entmutigend, Zeit mit der Behebung von Fehlern zu verbringen, nur um später den korrigierten Code zu verwerfen, weil sich etwas als nicht durchführbar oder unhaltbar herausgestellt hat. Daher werden Korrekturen und Verbesserungen erst dann vorgenommen, wenn sich der Code als wertvoll erwiesen hat, weiter beachtet zu werden.
Einige von Ihnen werden vielleicht denken, dass ich die Dinge zu sehr verkompliziere. Ich könnte Ihnen gleich eine erweiterte Version des Codes zeigen. Aber in Wahrheit würde das bei Anfängern den falschen Eindruck erwecken, dass der Code von Geburt an voll funktionsfähig ist oder dass er mit allen Funktionen vorinstalliert sein sollte. Erfahrene Entwickler wissen, dass es so einfach nicht funktioniert. Ich möchte Ihnen zeigen, wie sich der Code wirklich entwickelt und wie Sie mit den auftretenden Problemen umgehen können.
Auf jeden Fall ist es an der Zeit zu zeigen, wie das Problem gelöst werden kann, das im Code schon seit geraumer Zeit besteht, seit wir begonnen haben, das Verhalten des Systems zu ändern. Wenn Sie nicht versucht haben, das Problem durch Überprüfen des Codes zu verstehen, könnten Sie denken, dass es durch mehrere nicht miteinander verbundene Fehler verursacht wird. Aber das ist nicht wahr. Wenn das Ihre Einstellung ist, versuchen Sie nicht wirklich, diese Artikel zu studieren und zu verstehen. Sie sind nur auf der Suche nach fertigem Code und haben kein wirkliches Interesse daran, das Programmieren zu lernen. Das geht völlig am Zweck dieser Artikel vorbei: Ihnen, dem Leser, Techniken und Praktiken zu vermitteln, die von anderen Programmierern getestet oder verwendet werden, damit Sie neue Methoden lernen oder alternative Wege entdecken können, um bestimmte Ergebnisse zu erzielen.
Aber genug philosophiert. Schauen wir uns an, wie dieses Problem eines Speicherauszugs gelöst werden kann. Das erste, worauf Sie achten sollten, sind die Fehlermeldungen, die von MetaTrader 5 gemeldet werden. Als Anhaltspunkt sehen Sie sich das folgende Bild an:
Abbildung 01 - Anzeige der von MetaTrader 5 gemeldeten Fehler
Zwei Linien sind in diesem Bild besonders hervorgehoben. Die übrigen Zeilen sind mit diesen beiden verbunden. Wenn Sie genau hinschauen, können Sie die Ursache des Problems erkennen. Dieser Fehler tritt auf, weil bestimmte Objekte nicht aus dem Chart entfernt werden. Das werden Sie sich vielleicht fragen: Wie ist das möglich? Wie werden Objekte nicht entfernt? Habe ich vergessen, sie zu löschen? Die Antwort ist nein. Es gibt eine Objektentfernung, die im Destruktor der Klasse stattfindet. Sie können dies überprüfen, indem Sie den Code des Kontrollindikators untersuchen. Das Problem tritt genau dort auf, wie auf dem Bild zu sehen ist.
Wenn die Objekte also entfernt wurden, warum warnt uns MetaTrader 5, dass sie nicht entfernt wurden? Und schlimmer noch, diese Objekte sind Instanzen der Klasse C_DrawImage.
Wenn Sie das sehen, wissen Sie vielleicht nicht, was Sie tun sollen. Zumal auf die Klasse C_DrawImage nicht direkt, sondern nur indirekt zugegriffen wird. Zur besseren Erklärung sehen wir uns den Code der Klasse C_Controls an, die für die Verwaltung aller Objekte zuständig ist. Eine ihrer Aufgaben ist es, diese Klasse aufzurufen. Denken Sie daran: MetaTrader 5 macht uns auf ein Problem aufmerksam, bei dem Objekte vom Typ C_DrawImage nicht entfernt werden. Dies zeigt uns, dass das Problem nicht in der Klasse C_DrawImage selbst liegt, sondern in dem Code, der sie verwendet, d. h. in der Klasse C_Controls.
Da es nicht notwendig ist, den gesamten Code zu untersuchen, um das Problem und die Lösung zu verstehen, finden Sie unten die wichtigsten Teile. Nur ein Hinweis: Der unten gezeigte Code enthält bereits die Lösung für das Problem mit dem Speicherauszug.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\Auxiliar\C_DrawImage.mqh" 005. #include "..\Defines.mqh" 006. //+------------------------------------------------------------------+ 007. #define def_PathBMP "Images\\Market Replay\\Control\\" 008. #define def_ButtonPlay def_PathBMP + "Play.bmp" 009. #define def_ButtonPause def_PathBMP + "Pause.bmp" 010. #define def_ButtonLeft def_PathBMP + "Left.bmp" 011. #define def_ButtonLeftBlock def_PathBMP + "Left_Block.bmp" 012. #define def_ButtonRight def_PathBMP + "Right.bmp" 013. #define def_ButtonRightBlock def_PathBMP + "Right_Block.bmp" 014. #define def_ButtonPin def_PathBMP + "Pin.bmp" 015. #resource "\\" + def_ButtonPlay 016. #resource "\\" + def_ButtonPause 017. #resource "\\" + def_ButtonLeft 018. #resource "\\" + def_ButtonLeftBlock 019. #resource "\\" + def_ButtonRight 020. #resource "\\" + def_ButtonRightBlock 021. #resource "\\" + def_ButtonPin 022. //+------------------------------------------------------------------+ 023. #define def_ObjectCtrlName(A) "MarketReplayCTRL_" + (typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (string)(A)) 024. #define def_PosXObjects 120 025. //+------------------------------------------------------------------+ 026. #define def_SizeButtons 32 027. #define def_ColorFilter 0xFF00FF 028. //+------------------------------------------------------------------+ 029. #include "..\Auxiliar\C_Mouse.mqh" 030. //+------------------------------------------------------------------+ 031. class C_Controls : private C_Terminal 032. { 033. protected: 034. private : 035. //+------------------------------------------------------------------+ 036. enum eMatrixControl {eCtrlPosition, eCtrlStatus}; 037. enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + 1)}; 038. //+------------------------------------------------------------------+ 039. struct st_00 040. { 041. string szBarSlider, 042. szBarSliderBlock; 043. ushort Minimal; 044. }m_Slider; 045. struct st_01 046. { 047. C_DrawImage *Btn; 048. bool state; 049. short x, y, w, h; 050. }m_Section[eObjectControl::eNull]; 051. C_Mouse *m_MousePtr; 052. //+------------------------------------------------------------------+ ... 071. //+------------------------------------------------------------------+ 072. void SetPlay(bool state) 073. { 074. if (m_Section[ePlay].Btn == NULL) 075. m_Section[ePlay].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay); 076. m_Section[ePlay].Btn.Paint(m_Section[ePlay].x, m_Section[ePlay].y, m_Section[ePlay].w, m_Section[ePlay].h, 20, (m_Section[ePlay].state = state) ? 1 : 0); 077. if (!state) CreateCtrlSlider(); 078. } 079. //+------------------------------------------------------------------+ 080. void CreateCtrlSlider(void) 081. { 082. if (m_Section[ePin].Btn != NULL) return; 083. CreteBarSlider(77, 436); 084. m_Section[eLeft].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonLeft, "::" + def_ButtonLeftBlock); 085. m_Section[eRight].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonRight, "::" + def_ButtonRightBlock); 086. m_Section[ePin].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin); 087. PositionPinSlider(m_Slider.Minimal); 088. } 089. //+------------------------------------------------------------------+ 090. inline void RemoveCtrlSlider(void) 091. { 092. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 093. for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++) 094. { 095. delete m_Section[c0].Btn; 096. m_Section[c0].Btn = NULL; 097. } 098. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("B")); 099. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 100. } 101. //+------------------------------------------------------------------+ ... 132. //+------------------------------------------------------------------+ 133. public : 134. //+------------------------------------------------------------------+ 135. C_Controls(const long Arg0, const string szShortName, C_Mouse *MousePtr) 136. :C_Terminal(Arg0), 137. m_MousePtr(MousePtr) 138. { 139. if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown); 140. if (_LastError != ERR_SUCCESS) return; 141. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 142. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("")); 143. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 144. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) 145. { 146. m_Section[c0].h = m_Section[c0].w = def_SizeButtons; 147. m_Section[c0].y = 25; 148. m_Section[c0].Btn = NULL; 149. } 150. m_Section[ePlay].x = def_PosXObjects; 151. m_Section[eLeft].x = m_Section[ePlay].x + 47; 152. m_Section[eRight].x = m_Section[ePlay].x + 511; 153. m_Slider.Minimal = eTriState; 154. } 155. //+------------------------------------------------------------------+ 156. ~C_Controls() 157. { 158. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) delete m_Section[c0].Btn; 159. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("")); 160. delete m_MousePtr; 161. } 162. //+------------------------------------------------------------------+ ... 172. //+------------------------------------------------------------------+ 173. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 174. { 175. short x, y; 176. static ushort iPinPosX = 0; 177. static short six = -1, sps; 178. uCast_Double info; 179. 180. switch (id) 181. { 182. case (CHARTEVENT_CUSTOM + evCtrlReplayInit): 183. info.dValue = dparam; 184. if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break; 185. iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition])); 186. SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay); 187. break; 188. case CHARTEVENT_OBJECT_DELETE: 189. if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName("")) 190. { 191. if (sparam == def_ObjectCtrlName(ePlay)) 192. { 193. delete m_Section[ePlay].Btn; 194. m_Section[ePlay].Btn = NULL; 195. SetPlay(m_Section[ePlay].state); 196. }else 197. { 198. RemoveCtrlSlider(); 199. CreateCtrlSlider(); 200. } 201. } 202. break; 203. case CHARTEVENT_MOUSE_MOVE: 204. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick(x, y)) 205. { 206. case ePlay: 207. SetPlay(!m_Section[ePlay].state); 208. if (m_Section[ePlay].state) 209. { 210. RemoveCtrlSlider(); 211. m_Slider.Minimal = iPinPosX; 212. }else CreateCtrlSlider(); 213. break; ... 249. //+------------------------------------------------------------------+
Quellcode-Teile aus C_Controls.mqh
Wie ich bereits erwähnt habe, werden Sie, wenn Sie gerade erst mit dem Programmieren anfangen, wahrscheinlich nicht ganz verstehen, wie die Lösung zustande gekommen ist. Vor allem, weil es auf den ersten Blick keinen Unterschied im Code zu geben scheint. Das liegt daran, dass die Korrektur keinen langen Codeblock oder größere Änderungen an der Klasse erfordert. Das Problem wurde durch Hinzufügen einer einzigen Codezeile behoben. Das stimmt, nur eine Zeile. Um das Speicherauszugsproblem zu beheben, mussten wir eine Codezeile einführen, die zwar extrem einfach und scheinbar unbedeutend ist, aber für jemanden, der neu im Programmieren ist, erstaunlich komplex zu sein scheint. Es ist eine kleine Anleitung, die jeder verstehen kann, wenn er sie sieht. Wenn es jedoch nicht vorhanden ist, löst es das Speicherauszugsproblem aus und warnt, dass Objekte der Klasse C_DrawImage nicht ordnungsgemäß aus dem Speicher entfernt werden.
Beginnen wir also damit, zu verstehen, warum das Problem in der Klasse C_Controls und nicht in C_DrawImage liegt. Wie bereits erwähnt, erfolgt der Zugriff auf C_DrawImage nicht direkt, sondern indirekt. Und das liegt daran, dass der Zugriff auf die Klasse C_DrawImage über einen Zeiger erfolgt, der in Zeile 47 deklariert wird. Anmerkung: C_DrawImage wird durch einen Zeiger referenziert. Da MQL5 mit Zeigern etwas anders umgeht als C/C++, können Sie die Zeile 47 als einfache Variablendeklaration betrachten. Dennoch ist es ein Hinweis. Und das Problem liegt nicht darin, dass die Variable als Zeiger deklariert wird. Das Problem liegt darin, wie wir mit diesem Zeiger umgehen.
Das Hauptproblem mit Zeigern, und vielleicht der Grund, warum MQL5 sie anders als C/C++ behandelt, ist, dass Probleme im Zusammenhang mit Zeigern sehr schwer zu diagnostizieren sein können. Einige Fehler treten nur bei bestimmten Interaktionen auf, was ihre Behebung zu einem Albtraum macht. Sie können Ihr Programm Hunderte von Malen testen, ohne dass das Problem auftritt, und dann taucht es plötzlich auf, oft genau dann, wenn Sie beim Debuggen sind. Und das ist das Worst-Case-Szenario.
Wenn Sie also etwas als Zeiger deklarieren oder planen, es als Zeiger zu verwenden, sollten Sie es so früh wie möglich initialisieren. Das Gleiche gilt für Variablen im Allgemeinen. Da wir mit einer Klasse arbeiten, sollte die Initialisierung im Klassenkonstruktor erfolgen. Und in der Tat wurde dies bereits getan, wie Sie zwischen den Zeilen 144 und 149 sehen können (insbesondere wird der Zeiger in Zeile 148 initialisiert). Achten Sie darauf, dass er mit einem Wert von NULL initialisiert wird.
Das ist der springende Punkt. Ein Klassenkonstruktor kann auf zwei verschiedene Arten aufgerufen werden. Die erste ist, wenn die Klasse als Standardvariable referenziert wird. In diesem Fall gibt es in der Regel kein Problem, da der Compiler selbst Speicher für die Klasse zuweist, damit diese normal arbeiten kann. Der zweite Weg ist, wenn die Klasse über einen Zeiger referenziert wird. In diesem Fall muss der Programmierer den Konstruktor manuell mit dem Operator new aufrufen und den Speicher mit dem Operator delete freigeben.
Schauen Sie sich nun Zeile 156 genau an, in der der Destruktor für die Klasse C_Controls implementiert ist. Achten Sie sehr genau auf das Folgende, denn es ist entscheidend für das Verständnis des Themas. Wenn der Destruktor in Zeile 156 die Zeile 158 ausführt, ruft er den Destruktor für die Klasse C_DrawImage auf, sofern dieser existiert. Der Haken an der Sache ist jedoch, dass bei der Ausführung dieser Zeile auch der vom Zeiger belegte Speicher freigegeben werden muss. Wenn dies korrekt geschieht, gibt MetaTrader 5 keine Warnungen über Speicherlecks aus. Mit anderen Worten: Der Speicher wird ohne Probleme freigegeben. Das ist jedoch nicht der Fall. Die Frage ist also: Warum nicht?
Die Ursache liegt darin, dass irgendwo im vorangehenden Code etwas den Operator delete stört, und das einzige, das dazu in der Lage ist, ist der Operator NEW. Das Problem liegt also darin, wie „new“ verwendet wird. Aber warum passiert dieser Fehler hier? Denn in MQL5 scheint sich der NEW-Operator ähnlich zu verhalten wie in C/C++. Einige Sprachen implementieren NEW anders, aber nicht MQL5, soweit ich das beurteilen kann. Sehen wir uns also an, was hier wirklich passiert.
Wann immer die von C_DrawImage verwalteten Objekte erstellt werden müssen, geschieht dies mit dem Operator NEW. Sie können dies in den folgenden Zeilen sehen:
- Zeile 75: Hier wird die Schaltfläche erstellt, die das Abspiel-/Pausenbild darstellt.
- Zeilen 84 bis 86: Hier werden die Schaltflächen für den Schieberegler instanziiert.
Es gibt jedoch nur zwei Stellen, an denen der Speicher für C_DrawImage explizit freigegeben wird. Dies geschieht im Destruktor der Klasse C_Controls und in Zeile 95. Achten Sie nun auf dieses wichtige Detail: In Zeile 96, unmittelbar nachdem der Speicher mit delete freigegeben wurde, wird dem Zeiger der Wert NULL zugewiesen. Warum? Denn wenn Sie versuchen, auf einen Speicherbereich zu verweisen, der nicht mehr gültig ist, riskieren Sie, Müll zu lesen, wichtige Daten zu überschreiben oder sogar bösartigen Code auszuführen. Deshalb gilt es als beste Praxis, ungültig gewordenen Zeigern immer NULL zuzuweisen. IMMER. Obwohl diese Vorsichtsmaßnahme bereits getroffen wurde, trat der Fehler dennoch auf.
Gehen wir also noch einmal darauf ein, wo der Operator NEW verwendet wurde. Die Zeile 74 ist seit Beginn der Implementierung der Klasse vorhanden. Damit wird sichergestellt, dass nur dann Speicher zugewiesen wird, wenn der Zeiger nicht bereits verwendet wird. Das Problem ist also definitiv nicht vorhanden. Aber Zeile 82 gab es ursprünglich nicht. Und warum nicht? Ehrlich gesagt, kann ich das nicht mit Sicherheit sagen. Vielleicht habe ich es vergessen. Vielleicht habe ich nicht geglaubt, dass es einen Unterschied machen würde. Ich weiß es wirklich nicht. Aber es war das Fehlen von Zeile 82, das MetaTrader 5 veranlasste, uns auf ein Speicherleck hinzuweisen.
Möglicherweise sind Sie sich der möglichen Auswirkungen eines scheinbar trivialen Schrittes wie der Überprüfung, ob ein Zeiger bereits verwendet wird, nicht ganz bewusst. Vielleicht war es nicht einmal ein Programmierfehler, denn die Methode CreateCtrlSlider sollte erst nach RemoveCtrlSlider aufgerufen werden. Es gibt jedoch einen speziellen Fall, in dem dies nicht zutrifft. Die Zeile 212 ist die eigentliche Ursache des Problems. Er veranlasst den NEW-Operator, C_DrawImage wiederholt zu instanziieren und jedes Mal neuen Speicher zuzuweisen, ohne die vorherige Instanz freizugeben, wodurch Objekte dupliziert werden. Mit der Zeit sammelt sich dieser Speicher an und nimmt immer mehr Platz ein, bis die Anwendung geschlossen wird. An diesem Punkt meldet MetaTrader 5 einen Fehler.
Schlussfolgerung
Wie ich bereits sagte, verhindern einige Programmiersprachen, dass ein bereits verwendeter Zeiger einem anderen zugewiesen wird. Aber das ist bei MQL5 nicht der Fall, zumindest nicht, soweit ich das beobachtet habe. Was ich hier zeige, ist keine Schwachstelle in MQL5, sondern etwas, das Sie beim Programmieren beachten und mit Vorsicht behandeln müssen. Auch wenn viele behaupten werden, dass MQL5 keine Zeiger unterstützt, ist das nicht ganz richtig. Und wenn man sie nicht richtig verwaltet, stößt man auf ernsthafte Probleme, selbst bei scheinbar einfachen Programmen ohne offensichtliche Implementierungsprobleme.
Im Laufe der Jahre hatte ich mit unzähligen Fehlern zu tun, bei denen ein Zeiger einfach anfing, sich unberechenbar zu verhalten. Und selbst nach all dieser Zeit, nach Jahren der Erfahrung, bin ich immer noch Opfer eines Zeigerfehlers geworden. Und das alles nur, weil ich den einfachen Test in Zeile 82 nicht eingebaut habe, der sichergestellt hätte, dass wir nicht einen Zeiger verwenden, der bereits in Gebrauch ist. Ich habe auch den Aufruf in Zeile 212 übersehen, der sich in einem Mausereignis befindet und jedes Mal ausgelöst wird, wenn sich die Maus unter bestimmten Bedingungen bewegt.
Ich hoffe wirklich, dass diese Erklärung Ihnen hilft und als Warnung dient. Unterschätzen Sie niemals Zeiger. Sie sind äußerst leistungsfähige Instrumente, können aber bei unsachgemäßem Gebrauch auch ernsthafte Probleme verursachen. Im folgenden Video können Sie sehen, wie sich das System nach der Behebung des Problems verhält.
Demo-Video
Übersetzt aus dem Portugiesischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/pt/articles/12250





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