
Developing a Replay System (Part 72): An Unusual Communication (I)
Introduction
In the previous article, Developing a Replay System (Part 71): Getting the Time Right (IV), I showed how to add to the replay/simulator service what was presented in another article.
Specifically, in the article Developing a Replay System (Part 70): Getting the Time Right (III), where we used a test service to better understand how to work with order book events. This happens when the focus is on a custom symbol.
The entire process in these last two articles was quite interesting. This is mainly due to the approach we had to take to achieve the desired results. I believe many of you have learned and understood the correct way to get MetaTrader 5 to use the order book. Once again, I emphasize that we are talking about a custom symbol. Do not forget this.
It was quite interesting to see that simply by adding the order book, we could allow the mouse indicator to utilize the OnCalculate function, where data is placed into arrays by MetaTrader 5. This significantly streamlines the process since we no longer need to use the iSpread function just to obtain the bar spread.
Without the knowledge presented in those two articles, it would not be possible to retrieve the data through the arguments of the OnCalculate function when the timeframe is greater than one minute. Aside from this, there are no issues in obtaining data via the OnCalculate arguments. However, as soon as custom book events are used, we gain the ability to read the spread directly from the OnCalculate arguments.
But at the end of the last article, I explained a problem that needs to be solved. If it is not addressed, we will be entirely unable to properly use the application. So, for those who are just joining us in this article, let's quickly review what the issue is.
Recapping the Problem
The issue we need to address is a failure that occurs whenever the timeframe is changed. While not catastrophic, it is quite inconvenient. It manifests as follows: when we start the replay/simulator service, we can select the desired timeframe. Once the chart is opened, it will operate on this selected timeframe.
However, if you change the timeframe using MetaTrader 5, the information provided by the mouse indicator will shift from "auction active" to "market closed". Additionally, the control indicator will completely disappear from the chart, preventing any further interaction.
Although this is an inconvenience, the failure happens because MetaTrader 5 simply reloads the indicators on the chart and immediately calls the OnInit function. In any case, MetaTrader 5 is functioning exactly as it was designed to. It's we who are attempting to use it in a completely different way. Because we require it to operate differently for our purposes.
So what happens in these situations? When this failure is triggered, we must wait for the service to fire a custom event to the control indicator so that it can be reinitialized with new values. The problem is that this may take quite some time. Furthermore, in order for the service to actually fire the custom event that allows the control indicator to be reinitialized, the service must be in play mode.
If, by any chance, the service is not in play mode, the control indicator will never receive the necessary custom event for its initialization, rendering it completely inaccessible to the user. In this case, the user would have to stop the service and restart it, which is quite a hassle. Now, assuming the service is in play mode, we would still have to wait for the custom event to be triggered. Once the event is fired, the controls of the control indicator will become accessible again. At that point, the user must pause the replay/simulator service and then press play again. This will refresh the information displayed by the mouse indicator, specifically the remaining time of the bar, which will once again be shown correctly. If the user changes the timeframe again, the entire issue will repeat itself.
In the previous article, I explained this problem in more detail. Here, I'm simply recapping it for context, especially for those who are starting this series with this article. In any case, I left a small challenge for my readers in the previous article. The challenge was to imagine a way to solve this issue. Although many believe that programming is simply the art of writing code that computers can execute, programming is much more than that. Writing code is the easy part. The real challenge is figuring out how to solve problems. Programming is, in fact, the art of solving problems through code.
Honestly, I don't mind whether you, dear reader, were able to solve the problem or not. What matters is that you at least made the effort to try. Because solving problems and devising solutions truly is an art. Typing code is merely a formality. So, let's see how we will go about solving this particular problem.
First Solution Attempt
If you truly understand the problem, your immediate thought might be the following solution: implement a way to detect when the chart timeframe has been changed. As soon as that happens, we would make the indicators restore their state before their removal from the chart. This way, when MetaTrader 5 returns them to the chart, both the control and mouse indicators would know their last state and could resume from that point. A great idea. But there's a downside to this. No matter how you store the last known state of the indicator, this information must not be directly tied to the indicator itself. It should be made available to the indicator as soon as the OnInit function is called. Otherwise, we'll have problems.
There are several ways to achieve this, but all of them require additional implementation and testing to verify whether the saved state can safely be used. Personally, I find this solution overly cumbersome due to the amount of extra testing required just to validate whether the stored information is usable.
How could this actually be implemented? First, let's think about what information we need to store. In the case of the control indicator, we'd need to store whether it was in pause or play mode. Well, that seems enough. As for the mouse indicator, it would only need to come back indicating that the asset was in auction mode. Alright. Storing this data is fairly simple. You could write it to a file or to a global terminal variable. Either way, this part is easy enough to implement. Now let's consider the problems this would introduce.
With the control indicator, there wouldn't be much trouble. It's only added to the chart when the replay/simulator service is running. So either solution, i.e. file or global terminal variable, would work. We check if the file or variable exists. If so, use its value. But here's the problem: over time, that data could become out of sync with the current state of the service. We would have to ensure that the service deletes the global variable or file as soon as it shuts down.
In other words, just more programming overhead to manage and worry about. Now, back to the mouse indicator. If you initialize it with the value "auction mode", this could cause issues when it's added to a chart of a real asset. That's because the market could actually be closed, while the mouse indicator would be incorrectly displaying auction mode. And I also have plans to further enhance the mouse indicator, so forcing it to start in auction mode would only complicate things. It introduces an unnecessary layer of future problems, which I want to avoid at all costs.
So, to sum it up: using a global terminal variable is completely off the table. It would introduce two different initialization paths for the indicators. Likewise, using a file would bring about the same issues. This solution simply doesn't serve our needs. We need another solution, that remains tied to the indicator, but also allows us to test what's happening on the chart itself.
Second Solution Attempt
Someone might suggest making the service check the current timeframe of the custom symbol. This would, in fact, be a very clever solution, because it would eliminate the need to store any state information for the indicators. Remember: the replay/simulator service always knows the status of the indicator at any given time during execution. However, this solution comes with a small problem: there is no way for the service to directly retrieve the current chart timeframe.
But before we abandon this alternative, let's think for a moment. If we can somehow detect when the timeframe changes, all we would need to do is have the service call the UpdateIndicatorControl method, found in the header file C_Replay.mqh, and the issue would be resolved. That's almost entirely true. We would only need to make a minor adjustment to the UpdateIndicatorControl procedure, but this is far simpler than having to create an entirely separate initialization path for the indicators. So, we need the service to somehow detect that the chart timeframe has been changed. After that, it can use the mechanisms we've already implemented to properly reinitialize the indicators. Even if some minor changes are required within the service to use UpdateIndicatorControl, these adjustments would be far smaller and more manageable than having to modify the indicators themselves to handle this situation. In short, this alternative is definitely worth considering.
Third Solution Attempt
How about we try something a bit more unconventional? Let's combine both previous approaches into a single solution, but without using global terminal variables or files. Instead, we'll use the data buffer to transfer the information we need - in this case, the timeframe. With that, the service can monitor what's happening. Thus, whenever the timeframe changes, the service will immediately detect it. When that happens, the service can notify the indicators that they need to update themselves.
Our real challenge then becomes writing the timeframe information into the data buffer and the reading this information from within the service. When the user changes the timeframe, the entire application should receive a custom event from the service. This event will ensure that the mouse indicator receives the correct value to display to the user, and the control indicator will also be updated accordingly. This will prevent it from becoming inaccessible for controlling play and pause modes.
Starting the Implementation Test
Contrary to what some may think, we programmers always test things before actually putting them into practice. We never begin by modifying a program or application that's already in an advanced stage of development. Whenever a new feature needs to be implemented, we first create a test version. This version is as simple as possible. We use it to fine-tune the implementation and study how the final process should actually work. This saves us from having to remove or rewrite sections of code that later prove to be unsuitable for the final version.
So, let's create a very simple indicator to test the issue of timeframe changes. The complete source code for this indicator can be seen below.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property version "1.00" 04. #property indicator_chart_window 05. #property indicator_plots 0 06. //+------------------------------------------------------------------+ 07. int OnInit() 08. { 09. Print(_Period); 10. 11. return INIT_SUCCEEDED; 12. } 13. //+------------------------------------------------------------------+ 14. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 15. { 16. return rates_total; 17. } 18. //+------------------------------------------------------------------+
Source code of the test indicator
What does this little code do? In short, everything it does happens on line nine. That is, it prints to the terminal the value stored in the internal variable _Period. An example of its execution can be seen in the image below:
Execution Result
Notice something interesting here. In the image above, in the SOURCE column, we see the name of the indicator, but what really matters to us is what's inside the parentheses. More precisely, the value after the comma. As you can see, I changed the timeframe several times, and for each change, a different value was printed in the messages column. This may seem obvious. But for now, ignore the fact that the values are different. And don't try to derive any particular logic from them. What we're really interested in is: what is the highest value that _Period can actually hold? Well, MetaTrader 5 allows us to use the monthly timeframe. In the image, you can see this value on the second-to-last line: the highest value is 49153. Why is knowing this value important? Because we need a way for the service to detect when this value changes. But we can't afford to use any arbitrary bit length to achieve this. We need to ensure that the number of bits used is as small as possible. You will soon understand the reason.
Let's make a small modification to the code above. Here's what we have:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property version "1.00" 04. #property indicator_chart_window 05. #property indicator_plots 0 06. //+------------------------------------------------------------------+ 07. int OnInit() 08. { 09. Print(_Period < 60 ? _Period : (_Period < PERIOD_D1 ? _Period - 16325 : (_Period == PERIOD_D1 ? 84 : (_Period == PERIOD_W1 ? 91 : 96)))); 10. 11. return INIT_SUCCEEDED; 12. } 13. //+------------------------------------------------------------------+ 14. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 15. { 16. return rates_total; 17. } 18. //+------------------------------------------------------------------+
Source code of the test indicator
Before we analyze what line nine is doing now, let's take a look at the execution result shown in the image below.
Execution Result
Now, pay attention to the following point. Previously, we needed 16 bits to store the value that would allow us to detect when the timeframe had changed. But now, observe the highest possible value: 96. Wow! But can we actually do this? Yes, we can and we will. Because apparently MetaTrader 5 has already consolidated these timeframe values. So we can safely modify things to operate using this simplified value.
Now, going back to the code, notice that the reduction process is quite simple. It can be executed in a single line using only constants. This is more than sufficient for our purposes, because now we only need 7 bits to transfer the value. So we need to think a bit about how to implement this. Keep in mind: we are not interested in knowing the timeframe itself, but rather whether it has changed or not.
Starting to Transfer the Information to the Service
In order to transfer the timeframe information to the service so that it can detect when it changes, we'll need to use the indicator buffer, as we don't have any other suitable mechanism for this. Actually, there are other options. But I intentionally want to use the indicator buffer for this task. This approach keeps everything invisible to the user while also providing a certain level of information encapsulation during the transfer.
But before doing this, let's integrate the latest code we tested into our system. It will be added as a definition. Therefore, the code in the header file Defines.mqh will be as follows:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_VERSION_DEBUG 05. //+------------------------------------------------------------------+ 06. #ifdef def_VERSION_DEBUG 07. #define macro_DEBUG_MODE(A) \ 08. Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + #A + " = " + (string)(A)); 09. #else 10. #define macro_DEBUG_MODE(A) 11. #endif 12. //+------------------------------------------------------------------+ 13. #define def_SymbolReplay "RePlay" 14. #define def_MaxPosSlider 400 15. #define def_MaskTimeService 0xFED00000 16. #define def_IndicatorTimeFrame (_Period < 60 ? _Period : (_Period < PERIOD_D1 ? _Period - 16325 : (_Period == PERIOD_D1 ? 84 : (_Period == PERIOD_W1 ? 91 : 96)))) 17. //+------------------------------------------------------------------+ 18. union uCast_Double 19. { 20. double dValue; 21. long _long; // 1 Information 22. datetime _datetime; // 1 Information 23. uint _32b[sizeof(double) / sizeof(uint)]; // 2 Informations 24. ushort _16b[sizeof(double) / sizeof(ushort)]; // 4 Informations 25. uchar _8b [sizeof(double) / sizeof(uchar)]; // 8 Informations 26. }; 27. //+------------------------------------------------------------------+ 28. enum EnumEvents { 29. evHideMouse, //Hide mouse price line 30. evShowMouse, //Show mouse price line 31. evHideBarTime, //Hide bar time 32. evShowBarTime, //Show bar time 33. evHideDailyVar, //Hide daily variation 34. evShowDailyVar, //Show daily variation 35. evHidePriceVar, //Hide instantaneous variation 36. evShowPriceVar, //Show instantaneous variation 37. evCtrlReplayInit //Initialize replay control 38. }; 39. //+------------------------------------------------------------------+
Defines.mqh file source code
Alright. Now look at line 16. This is the code that was used in the tests described in the previous section. Excellent. This guarantees that we're using code that has already been tested and is functioning correctly. Now comes the more complicated part, at least if you haven't been following this article series from the beginning or are just joining now.
The indicator's data buffer is always of type double, so it occupies 8 bytes of memory. For reasons I've explained in earlier articles in this series, any information we transfer from the indicator to the service must fit within these 8 bytes. We cannot afford to require more than 8 bytes for this information exchange. Everything must be packed into these 8 bytes. Let's think this through. The mouse indicator can be used on a live market. In other words, it can operate on a symbol receiving data from the actual trading server. But the problem we're addressing here only occurs when using the replay/simulation service. So far, the only indicator that's strictly required exclusively for the replay/simulation mode is the control indicator. Therefore, it makes perfect sense to focus on how the control indicator's data buffer is currently structured. You can see this below:
QWORD is a term that comes from Assembly language. It indicates that we're dealing with a 64-bit value. The byte on the far right is byte zero, the byte on the far left is byte seven. In the case of the control indicator, it currently uses 4 bytes. To better understand this, take a look at the code excerpt from the header file C_Controls.mqh, shown below:
168. //+------------------------------------------------------------------+ 169. void SetBuffer(const int rates_total, double &Buff[]) 170. { 171. uCast_Double info; 172. 173. info._16b[eCtrlPosition] = m_Slider.Minimal; 174. info._16b[eCtrlStatus] = (ushort)(m_Slider.Minimal > def_MaxPosSlider ? m_Slider.Minimal : (m_Section[ePlay].state ? ePlay : ePause)); 175. if (rates_total > 0) 176. Buff[rates_total - 1] = info.dValue; 177. } 178. //+------------------------------------------------------------------+
Excerpt from file C_Controls.mqh
On line 171, we declare the union that was previously defined in the Defines.mqh file shown above. Observe that we're using two array values, each of them uses 16 bits. This gives us a total of 32 bits. Although, not all bits are actually being used, but for simplicity, let's assume they are. Thus, four bytes out of the available eight are already in use. However, since our modeling system has allowed us to encode the timeframe using a single byte, we'll now be using five bytes out of the eight available. But the most critical part is determining which index we should use for this new data. This type of detail tends to confuse many beginners in programming, because they often forget that when using a union, some bytes are already occupied with useful information. If you accidentally use the wrong index, you risk overwriting valid data, resulting in corrupted values. So, let's once again refer to the QWORD image. There, four bytes are already allocated, starting at index zero. This means that indexes from 0 to 3 are in use and must not be reused for any other purpose. Therefore, the first available index for new data is index 4.
So, we update the Defines.mqh header file as follows:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_VERSION_DEBUG 05. //+------------------------------------------------------------------+ 06. #ifdef def_VERSION_DEBUG 07. #define macro_DEBUG_MODE(A) \ 08. Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + #A + " = " + (string)(A)); 09. #else 10. #define macro_DEBUG_MODE(A) 11. #endif 12. //+------------------------------------------------------------------+ 13. #define def_SymbolReplay "RePlay" 14. #define def_MaxPosSlider 400 15. #define def_MaskTimeService 0xFED00000 16. #define def_IndicatorTimeFrame (_Period < 60 ? _Period : (_Period < PERIOD_D1 ? _Period - 16325 : (_Period == PERIOD_D1 ? 84 : (_Period == PERIOD_W1 ? 91 : 96)))) 17. #define def_IndexTimeFrame 4 18. //+------------------------------------------------------------------+ 19. union uCast_Double 20. { 21. double dValue; 22. long _long; // 1 Information 23. datetime _datetime; // 1 Information 24. uint _32b[sizeof(double) / sizeof(uint)]; // 2 Informations 25. ushort _16b[sizeof(double) / sizeof(ushort)]; // 4 Informations 26. uchar _8b [sizeof(double) / sizeof(uchar)]; // 8 Informations 27. }; 28. //+------------------------------------------------------------------+ 29. enum EnumEvents { 30. evHideMouse, //Hide mouse price line 31. evShowMouse, //Show mouse price line 32. evHideBarTime, //Hide bar time 33. evShowBarTime, //Show bar time 34. evHideDailyVar, //Hide daily variation 35. evShowDailyVar, //Show daily variation 36. evHidePriceVar, //Hide instantaneous variation 37. evShowPriceVar, //Show instantaneous variation 38. evCtrlReplayInit //Initialize replay control 39. }; 40. //+------------------------------------------------------------------+
Defines.mqh file source code
On line 17, we have a new definition that allows us to safely reference the correct index. So, now we have the foundation we need. However, as the task is a bit more complex than it may initially appear, let's first take a look at how the C_Controls.mqh header file will actually be structured. The full code can be seen below:
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_ButtonCtrl def_PathBMP + "Ctrl.bmp" 011. #define def_ButtonCtrlBlock def_PathBMP + "Ctrl_Block.bmp" 012. #define def_ButtonPin def_PathBMP + "Pin.bmp" 013. #resource "\\" + def_ButtonPlay 014. #resource "\\" + def_ButtonPause 015. #resource "\\" + def_ButtonCtrl 016. #resource "\\" + def_ButtonCtrlBlock 017. #resource "\\" + def_ButtonPin 018. //+------------------------------------------------------------------+ 019. #define def_ObjectCtrlName(A) "MarketReplayCTRL_" + (typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (string)(A)) 020. #define def_PosXObjects 120 021. //+------------------------------------------------------------------+ 022. #define def_SizeButtons 32 023. #define def_ColorFilter 0xFF00FF 024. //+------------------------------------------------------------------+ 025. #include "..\Auxiliar\C_Mouse.mqh" 026. //+------------------------------------------------------------------+ 027. class C_Controls : private C_Terminal 028. { 029. protected: 030. private : 031. //+------------------------------------------------------------------+ 032. enum eMatrixControl {eCtrlPosition, eCtrlStatus}; 033. enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + 1)}; 034. //+------------------------------------------------------------------+ 035. struct st_00 036. { 037. string szBarSlider, 038. szBarSliderBlock; 039. ushort Minimal; 040. }m_Slider; 041. struct st_01 042. { 043. C_DrawImage *Btn; 044. bool state; 045. short x, y, w, h; 046. }m_Section[eObjectControl::eNull]; 047. C_Mouse *m_MousePtr; 048. //+------------------------------------------------------------------+ 049. inline void CreteBarSlider(short x, short size) 050. { 051. ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSlider = def_ObjectCtrlName("B1"), OBJ_RECTANGLE_LABEL, 0, 0, 0); 052. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XDISTANCE, def_PosXObjects + x); 053. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Section[ePin].y + 11); 054. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XSIZE, size); 055. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YSIZE, 9); 056. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue); 057. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack); 058. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_WIDTH, 3); 059. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT); 060. ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSliderBlock = def_ObjectCtrlName("B2"), OBJ_RECTANGLE_LABEL, 0, 0, 0); 061. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, def_PosXObjects + x); 062. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Section[ePin].y + 6); 063. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, 19); 064. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown); 065. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED); 066. } 067. //+------------------------------------------------------------------+ 068. void SetPlay(bool state) 069. { 070. if (m_Section[ePlay].Btn == NULL) 071. m_Section[ePlay].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay); 072. 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, state ? "Press to Pause" : "Press to Start"); 073. if (!state) CreateCtrlSlider(); 074. } 075. //+------------------------------------------------------------------+ 076. void CreateCtrlSlider(void) 077. { 078. if (m_Section[ePin].Btn != NULL) return; 079. CreteBarSlider(77, 436); 080. m_Section[eLeft].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonCtrl, "::" + def_ButtonCtrlBlock); 081. m_Section[eRight].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonCtrl, "::" + def_ButtonCtrlBlock, true); 082. m_Section[ePin].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin, NULL); 083. PositionPinSlider(m_Slider.Minimal); 084. } 085. //+------------------------------------------------------------------+ 086. inline void RemoveCtrlSlider(void) 087. { 088. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 089. for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++) 090. { 091. delete m_Section[c0].Btn; 092. m_Section[c0].Btn = NULL; 093. } 094. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("B")); 095. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 096. } 097. //+------------------------------------------------------------------+ 098. inline void PositionPinSlider(ushort p) 099. { 100. int iL, iR; 101. string szMsg; 102. 103. m_Section[ePin].x = (short)(p < m_Slider.Minimal ? m_Slider.Minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); 104. iL = (m_Section[ePin].x != m_Slider.Minimal ? 0 : 1); 105. iR = (m_Section[ePin].x < def_MaxPosSlider ? 0 : 1); 106. m_Section[ePin].x += def_PosXObjects; 107. m_Section[ePin].x += 95 - (def_SizeButtons / 2); 108. for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++) if (m_Section[c0].Btn != NULL) 109. { 110. switch (c0) 111. { 112. case eLeft : szMsg = "Previous Position"; break; 113. case eRight : szMsg = "Next Position"; break; 114. case ePin : szMsg = "Go To: " + IntegerToString(p); break; 115. default : szMsg = "\n"; 116. } 117. m_Section[c0].Btn.Paint(m_Section[c0].x, m_Section[c0].y, m_Section[c0].w, m_Section[c0].h, 20, (c0 == eLeft ? iL : (c0 == eRight ? iR : 0)), szMsg); 118. } 119. 120. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + 2); 121. } 122. //+------------------------------------------------------------------+ 123. inline eObjectControl CheckPositionMouseClick(short &x, short &y) 124. { 125. C_Mouse::st_Mouse InfoMouse; 126. 127. InfoMouse = (*m_MousePtr).GetInfoMouse(); 128. x = (short) InfoMouse.Position.X_Graphics; 129. y = (short) InfoMouse.Position.Y_Graphics; 130. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) 131. { 132. if ((m_Section[c0].Btn != NULL) && (m_Section[c0].x <= x) && (m_Section[c0].y <= y) && ((m_Section[c0].x + m_Section[c0].w) >= x) && ((m_Section[c0].y + m_Section[c0].h) >= y)) 133. return c0; 134. } 135. 136. return eNull; 137. } 138. //+------------------------------------------------------------------+ 139. public : 140. //+------------------------------------------------------------------+ 141. C_Controls(const long Arg0, const string szShortName, C_Mouse *MousePtr) 142. :C_Terminal(Arg0), 143. m_MousePtr(MousePtr) 144. { 145. if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown); 146. if (_LastError >= ERR_USER_ERROR_FIRST) return; 147. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 148. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("")); 149. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 150. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) 151. { 152. m_Section[c0].h = m_Section[c0].w = def_SizeButtons; 153. m_Section[c0].y = 25; 154. m_Section[c0].Btn = NULL; 155. } 156. m_Section[ePlay].x = def_PosXObjects; 157. m_Section[eLeft].x = m_Section[ePlay].x + 47; 158. m_Section[eRight].x = m_Section[ePlay].x + 511; 159. m_Slider.Minimal = eTriState; 160. } 161. //+------------------------------------------------------------------+ 162. ~C_Controls() 163. { 164. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) delete m_Section[c0].Btn; 165. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("")); 166. delete m_MousePtr; 167. } 168. //+------------------------------------------------------------------+ 169. void SetBuffer(const int rates_total, double &Buff[]) 170. { 171. uCast_Double info; 172. 173. info._16b[eCtrlPosition] = m_Slider.Minimal; 174. info._16b[eCtrlStatus] = (ushort)(m_Slider.Minimal > def_MaxPosSlider ? m_Slider.Minimal : (m_Section[ePlay].state ? ePlay : ePause)); 175. info._8b[def_IndexTimeFrame] = (uchar) def_IndicatorTimeFrame; 176. if (rates_total > 0) 177. Buff[rates_total - 1] = info.dValue; 178. } 179. //+------------------------------------------------------------------+ 180. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 181. { 182. short x, y; 183. static ushort iPinPosX = 0; 184. static short six = -1, sps; 185. uCast_Double info; 186. 187. switch (id) 188. { 189. case (CHARTEVENT_CUSTOM + evCtrlReplayInit): 190. info.dValue = dparam; 191. if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break; 192. iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition])); 193. SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay); 194. break; 195. case CHARTEVENT_OBJECT_DELETE: 196. if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName("")) 197. { 198. if (sparam == def_ObjectCtrlName(ePlay)) 199. { 200. delete m_Section[ePlay].Btn; 201. m_Section[ePlay].Btn = NULL; 202. SetPlay(m_Section[ePlay].state); 203. }else 204. { 205. RemoveCtrlSlider(); 206. CreateCtrlSlider(); 207. } 208. } 209. break; 210. case CHARTEVENT_MOUSE_MOVE: 211. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick(x, y)) 212. { 213. case ePlay: 214. SetPlay(!m_Section[ePlay].state); 215. if (m_Section[ePlay].state) 216. { 217. RemoveCtrlSlider(); 218. m_Slider.Minimal = iPinPosX; 219. }else CreateCtrlSlider(); 220. break; 221. case eLeft: 222. PositionPinSlider(iPinPosX = (iPinPosX > m_Slider.Minimal ? iPinPosX - 1 : m_Slider.Minimal)); 223. break; 224. case eRight: 225. PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + 1 : def_MaxPosSlider)); 226. break; 227. case ePin: 228. if (six == -1) 229. { 230. six = x; 231. sps = (short)iPinPosX; 232. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false); 233. } 234. iPinPosX = sps + x - six; 235. PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX))); 236. break; 237. }else if (six > 0) 238. { 239. six = -1; 240. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); 241. } 242. break; 243. } 244. ChartRedraw(GetInfoTerminal().ID); 245. } 246. //+------------------------------------------------------------------+ 247. }; 248. //+------------------------------------------------------------------+ 249. #undef def_PosXObjects 250. #undef def_ButtonPlay 251. #undef def_ButtonPause 252. #undef def_ButtonCtrl 253. #undef def_ButtonCtrlBlock 254. #undef def_ButtonPin 255. #undef def_PathBMP 256. //+------------------------------------------------------------------+
Source code of the C_Controls.mqh file
Due to several changes made in previous articles, it's necessary that you understand and apply at least a few modifications to the C_Controls.mqh header file, along with a few other changes I'll show now. Take a look at line 10: this line references the image Ctrl.bmp. But which image is this? This is actually the same image that was previously called LEFT.BMP. Similarly, line 11 references what was formerly known as LEFT_BLOCK.BMP. From now on, we'll be using different names for these images. In any case, I've included these images in the attachment, so that you can easily place them in your project if you have any doubts.
There's another change to highlight - it's in the constructor of the C_Controls class. Look at line 146. The constructor will now exit early only if the error encountered is one that we as developers have explicitly indicated in the code. Other types of errors will be ignored, at least for now.
The important part that really matters to us appears on line 175. Here we define where and what will be written into the indicator buffer, as proposed in this article. But since making use of this information is somewhat more complex than simply writing it to the buffer, I won't show the full code just yet. That's because it's important to explain how things work for those who are still learning how to program for MetaTrader 5. So, dear reader, if you are already more experienced, I apologize for not presenting the complete code right now.
Final Thoughts
Since I still need to explain a few additional changes that will be made to the service code and to the indicator code, I won't present the full code in this article. I apologize to those who already have solid experience with MQL5. However, throughout the time I've been publishing these articles, I've noticed that many readers are still just starting out with MQL5 - aspiring programmers who are eager to learn. So I try to explain things thoroughly. Nonetheless, you'll find two images attached to this article. These are the images referenced by the C_Controls.mqh header file.
Don't forget to apply this adjustment to your project if you intend to continue following along.
In the next article, we will cover the following tasks: present and explain the modifications made to the control indicator's source code. We have already seen changes in the header file, but we still need to modify the indicator code itself. Also, I will show how to adjust the source code of the C_Replay.mqh header file. Without these changes, the service will not be able to determine that the timeframe has been changed simply by "looking" at the indicator on the chart.
So, see you next time. And don't forget to study this content carefully.
Translated from Portuguese by MetaQuotes Ltd.
Original article: https://www.mql5.com/pt/articles/12362





- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use