Русский 中文 Español Deutsch 日本語 Português
preview
Creating a ticker tape panel: Improved version

Creating a ticker tape panel: Improved version

MetaTrader 5Indicators | 13 February 2023, 15:01
4 456 0
Daniel Jose
Daniel Jose

Introduction

In the previous article Creating a ticker panel: Basic version, we have seen how to create an indicator in the form of a panel displaying a tape of real-time symbol prices. However, in the previous article we did not implement the indicator completely, not because it was impossible, but because our goal was to show the process of creating the indicator, to see how to make it work with the least amount of code in order to give the impression that it moves.

We have seen that it is not necessary to create special code or complex calculations to make the panel move from right to left or vice versa. The only thing the user had to do was indicate whether the value was increasing or decreasing. By only this it is possible to make the indicator move to the right, to the left, or stay still.

But this data is not always enough and does not always reflect what people really want to see on the panel. It would be great to have some more details. This is what we are going to implement here. We will also implement a few more things to make the panel more useful. The mere fact that there is a panel in a new form is still completely useless, since there are other, more suitable ways to create it.

Therefore, the first thing we will do is modify the view by adding an image, such as an asset logo or other image, so that the user could quickly and easily identify the displayed asset. They say a picture is worth more than a thousand words. Let's see if that's really the case.


Implementing a new quote panel system

The first thing we will do in this new implementation is crate a new object class to abstract the object that will represent the asset image. We will use the following code here:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Object_Base.mqh"
//+------------------------------------------------------------------+
class C_Object_BtnBitMap : public C_Object_Base
{
        public  :
//+------------------------------------------------------------------+
                void Create(string szObjectName, string szResource1)
                        {
                                C_Object_Base::Create(szObjectName, OBJ_BITMAP_LABEL);
                                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_BMPFILE, 0, "\\Images\\Widget\\Bitmaps\\" + szResource1 + ".bmp");
                                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE, false);
                        };
//+------------------------------------------------------------------+
};

Before explaining what's going on here, let me briefly explain why we're doing it this way. We start by creating a Bitmap object to store the image. We use a general class to make the object creation easier. Then we inform the object about which image it should use based on the image location and name. And finally, we inform the object that the image to be displayed is the image at index 0. This way we achieve a very high level of abstraction in the system.

Now let's see why we are doing it exactly this way.

First of all, it should be remembered that there are certain restrictions related to objects. They concern not what can be done but how this should be done. The use of a class to creation a system abstraction allows us to hide what is actually going on with the rest of the code. We don't really care what the C_Object_Bitmap class should do in order to display an image on the screen.

Now pay attention to the following: if the image does not exist or has a different format, neither the rest of the code nor the user will be informed about this. What we will have is an empty area indicating there is an error. Therefore, in order to find out whether there is an error, we should check the return of the ObjectSetString function. If false is returned, it means the image could not be loaded and the object can be deleted from the list of objects. But we haven't done so. Why?

Actually, for me it doesn't make the slightest difference, because all the assets that are placed on the panel will have an image that will represent the asset. But there is one more reason which is perhaps even more important.

If you look carefully, you will see that the code in this form does not allow using any other image type except the bitmap file, for example you can't place an image on a transparent background. While the transparent background itself can be useful sometimes used, but if you try to implement it with the existing code, you may receive an image showing very strange information instead of the required asset logo. Sow we would need to find another way of presenting such images in addition to the one shown in the code above.

One of such methods was shown in the article Making charts more interesting: Adding a background. In that article, we created images directly through a resource. Although I also used a bitmap file (since it has a simpler internal code construction model), nothing prevents you from using other formats, for example a GIF file to have an animation or any other file format. The main point is that by using this method, you can create and use any images, including the transparent background, which is not possible in the code considered earlier in this article.

Therefore, this abstraction is very important: if you want to use a different image format or even use a transparent background image, you will not have to implement any changes to the rest of the code - simply change the code of this class to implement what you need.

For this reason, this code is implemented as a class, although it is quite a compact one. By using the class, we, can hide everything happening here from other parts of the panel.

We have one more change in the C_Object_Edit class. Let's see what has changed here:

template < typename T >
void Create(string szObjectName, color corTxt, color corBack, T InfoValue, string szFont = "Lucida Console", int iSize = 10)
        {
                C_Object_Base::Create(szObjectName, OBJ_EDIT);
                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_FONT, szFont);
                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_FONTSIZE, iSize);
                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_ALIGN, ALIGN_LEFT);
                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BGCOLOR, corBack);
                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_BORDER_COLOR, corBack);
                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_READONLY, true);
                SetTextValue(szObjectName, InfoValue, corTxt);
        };

Since we are implementing more information on the panel, we need to create a class responsible for placing a text on the chart. It is a little more complicated than the one that existed originally. Now we will control the type of font used and its size. Thus, we will be able to place much more diversified things on the panel that it was possible before. This increased diversity also requires changes in another function of this class.

template < typename T >
void SetTextValue(string szObjectName, T InfoValue, color cor = clrNONE, const string szSufix = "")
        {
                color clr = (cor != clrNONE ? cor : (color)ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR));
                string sz0;
                
                if (typename(T) == "string") sz0 = (string)InfoValue; else
                if (typename(T) == "double")
                {
                        clr = (cor != clrNONE ? cor : ((double)InfoValue < 0.0 ? def_ColorNegative : def_ColoPositive));
                        sz0 = Terminal.ViewDouble((double)InfoValue < 0.0 ? -((double)InfoValue) : (double)InfoValue) + szSufix;
                }else   sz0 = "?#?";
                ObjectSetString(Terminal.Get_ID(), szObjectName, OBJPROP_TEXT, sz0);
                ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_COLOR, clr);
        };

You can see how abstraction makes all the difference: we can indicate several things, more than it was possible in the previous version. So, let's see what we are actually doing on the function above.

If a color is indicated, this color will be used. If no color is specified, we will use the one that already exists in the object. This is very helpful because sometimes we may simply change the text without changing the color or anything else that the object already has. At this stage we check if the specified type is a string. We do this at RUN TIME, so we must ensure that things are correct.

We can also use type double which is mainly used to specify the asset price. If this is detected at RUN TIME, we will set the values accordingly and will use the appropriate color. Now we have an interesting fact which allows us to talk a little more about the value - we will use a suffix that will be informed during the call. If something is not clear, we also have formatting to indicate this. Next we present the text and adjust its color.

By simply implementing these changes we can already create information as shown below:


We have an image in the left corner. The asset code appears in the upper left corner. The upper right corner shows the daily asset change. The lower left corner contains an arrow which indicates whether the asset change is positive or negative. The last quote of the asset is shown in the lower right corner.

But in order to present all this information, we need to implement some changes in the C_Widget class. Let's see what we will have now.


The new C_Widget class

Open the header file C_Widget.mqh to see what has changed compared to the first basic version of the system. Let's take a look at these changes:

#include "Elements\C_Object_Edit.mqh"
#include "Elements\C_Object_BackGround.mqh"
#include "Elements\C_Object_BtnBitMap.mqh"
//+------------------------------------------------------------------+
C_Terminal Terminal;
//+------------------------------------------------------------------+
#define def_PrefixName  "WidgetPrice"
#define def_MaxWidth    160
//+------------------------------------------------------------------+
#define macro_MaxPosition (Terminal.GetWidth() >= (m_Infos.nSymbols * def_MaxWidth) ? Terminal.GetWidth() : m_Infos.nSymbols * def_MaxWidth)
#define macro_ObjectName(A, B) (def_PrefixName + (string)Terminal.GetSubWin() + CharToString(A) + "#" + B)

In the code above, we have connected the C_Object_BtnBitMap.mqh class. This header file supports images to be used as logos. Also, the cell width has changed to display more details.

Perhaps most interesting thing is the macro that creates the name to be used in the objects. Pay attention to it as it will be used a few more times.

Next, we move on to the part where we have the declarations of all the private elements in the object class:

class C_Widget
{
        protected:
                enum EventCustom {Ev_RollingTo};
        private :
                enum EnumTypeObject {en_Background = 35, en_Symbol, en_Price, en_Percentual, en_Icon, en_Arrow};
                struct st00
                {
                        color CorBackGround,
                              CorSymbol;
                        int   nSymbols,
                              MaxPositionX;
                        struct st01
                        {
                              string szCode;
                        }Symbols[];
                }m_Infos;

// ... Rest of the code

It may seem that there are no changes. But the code actually has some changes. Let's take a quick look at them. These things will appear later many times and you will be able to understand them better when they are used. The first thing that catches the eye is the enumeration of object types.. Actually, this does not indicate what type of object we are creating, but what the purpose of that object is. This will become clearer as we move on with the class code.

You might wonder why we don't use definitions instead of this enumeration. This is because we must guarantee that each object is unique. So, we guaranty these by using the enumeration. If we used definitions, we would run the risk of having two definitions with the same value and thus the objects would not be unique. Remember this detail when programming.

Now let's look at the changes that happened in the system, regarding the internal procedures. We will start with the function that creates the background.

void CreateBackGround(void)
        {
                C_Object_BackGround backGround;
                string sz0 = macro_ObjectName(en_Background, "");
                                
                backGround.Create(sz0, m_Infos.CorBackGround);
                backGround.Size(sz0, TerminalInfoInteger(TERMINAL_SCREEN_WIDTH), Terminal.GetHeight());
        }

Here we use the first of the enumerations. So, it's really worth explaining why the enumerations are starting with the value 35 and not another one. But first let's finish with one last important thing here: when we resize the chart, the OnChartEvent call is generated which allows us to check the new size and update the panel accordingly.

But we can do without resizing the background in a sense, as the panel height is always fixed. Later we will see where we define this value. But in order to fix the width and thus to be able to ignore changes in the panel width, we will use the method that may not seem very effective but that guarantees that the background is big enough.

Now let's see why we start enumerations at 35, even though we could use a smaller one, but not any value.

If you look at the macro that generates the names of the objects, you will notice something:

#define macro_ObjectName(A, B) (def_PrefixName + (string)Terminal.GetSubWin() + CharToString(A) + "#" + B)

The first argument of the macro is the enumeration that we have defined in the class. The enumeration will be transformed into the corresponding string, and thus based on the value we will have a character which will be used to make the name unique. If you look at the ASCII table, you will see that the first valid value is 32 which stands for the space character. So, we could initialize the value with 32, but initializing a lower number makes no sense.

Therefore, this happens only because we are converting the value into a character. If the value were converted to a corresponding sting, we could initialize the enumeration with 0. But in our case, when we deal with characters, the minimum suitable value is 32.

Now let's look at the function responsible for adding the objects to be used.

void AddSymbolInfo(const string szArg, const bool bRestore = false)
        {
                C_Object_Edit edit;
                C_Object_BtnBitMap bmp;
                string sz0;
                const int x = 9999;

                bmp.Create(sz0 = macro_ObjectName(en_Icon, szArg), szArg);
                bmp.PositionAxleX(sz0, x);
                bmp.PositionAxleY(sz0, 15);
                edit.Create(sz0 = macro_ObjectName(en_Symbol, szArg), m_Infos.CorSymbol, m_Infos.CorBackGround, szArg);
                edit.PositionAxleX(sz0, x);
                edit.PositionAxleY(sz0, 10);
                edit.Size(sz0, 56, 16);
                edit.Create(sz0 = macro_ObjectName(en_Percentual, szArg), m_Infos.CorSymbol, m_Infos.CorBackGround, 0.0, "Lucida Console", 8);
                edit.PositionAxleX(sz0, x);
                edit.PositionAxleY(sz0, 10);
                edit.Size(sz0, 50, 11);
                edit.Create(sz0 = macro_ObjectName(en_Arrow, szArg), m_Infos.CorSymbol, m_Infos.CorBackGround, "", "Wingdings 3", 10);
                edit.PositionAxleX(sz0, x);
                edit.PositionAxleY(sz0, 26);
                edit.Size(sz0, 20, 16);
                edit.Create(sz0 = macro_ObjectName(en_Price, szArg), 0, m_Infos.CorBackGround, 0.0);
                edit.PositionAxleX(sz0, x);
                edit.PositionAxleY(sz0, 26);
                edit.Size(sz0, 60, 16);
                if (!bRestore)
                {
                        ArrayResize(m_Infos.Symbols, m_Infos.nSymbols + 1, 10);
                        m_Infos.Symbols[m_Infos.nSymbols].szCode = szArg;
                        m_Infos.nSymbols++;
                }
        }

Here we can implement something that many will find strange. But for me it's just a joke. I like to play and explore the possibilities of things. And I ask: "Why did I do this? 

Why did I declare a constant variable, and then apply it in the calls? And the reason is, as I said earlier, I like to make jokes around the possibilities that language, whatever it may be, to find out what it allows us to do.

In fact, you could do without declaring such a constant. You could use the compilation definition (#define) or simply put the constant value in each of the calls. The result would be the same. Regardless of this, let's look at the rest of the function code: you can see that the elements are added so that their names are unique, i.e. there cannot be two elements with the same name. We create them in the following order: BitMap, asset code, daily percent change, arrow indicating the change direction and the current asset price.

Remember one important thing: the bitmaps that are used in the asset must be 32 x 32 bits. If you use another size, you will have to resize it - but not here. Later we will see where it is done. But you must bear in mind that the dimensions of all other objects should also be changed adjusted here, so if you want to make you panel larger or smaller than the one we have in the attachment, set the required values here. Remember that each of these functions is associated with an object that we declared just before the call.

Now let's see how the data will be actually presented.

inline void UpdateSymbolInfo(int x, const string szArg)
        {
                C_Object_Edit edit;
                C_Object_BtnBitMap bmp;
                string sz0;
                double v0[], v1;
                                
                ArraySetAsSeries(v0, true);
                if (CopyClose(szArg, PERIOD_D1, 0, 2, v0) < 2) return;
                v1 = ((v0[0] - v0[1]) / v0[(v0[0] > v0[1] ? 0 : 1)]) * 100.0;
                bmp.PositionAxleX(sz0 = macro_ObjectName(en_Icon, szArg), x);
                x += (int) ObjectGetInteger(Terminal.Get_ID(), sz0, OBJPROP_XSIZE);
                edit.PositionAxleX(macro_ObjectName(en_Symbol, szArg), x + 2);
                edit.PositionAxleX(sz0 = macro_ObjectName(en_Arrow, szArg), x  + 2);
                edit.SetTextValue(sz0, CharToString(v1 >= 0 ? (uchar)230 : (uchar)232), (v1 >= 0 ? def_ColoPositive : def_ColorNegative));
                edit.SetTextValue(sz0 = macro_ObjectName(en_Percentual, szArg), v1 , clrNONE, "%");
                edit.PositionAxleX(sz0, x + 62);
                edit.SetTextValue(sz0 = macro_ObjectName(en_Price, szArg), v0[0] * (v1 >= 0 ? 1 : -1));
                edit.PositionAxleX(sz0, x + 24);
        }

Here we make sure that the series of values always goes from the most recent to the oldest. Once we are sure, we can capture the close prices. Note that we are doing the daily reading since we display the daily change in the panel. Another point: if the amount of data returned is less than expected, we will immediately exit the function, because if we continue, we will eventually break the indicator — the relevant information will appear in the MetaTrader 5 toolbox. But we don't want to break the indicator because of an error which is usually temporary.

Now pay attention to the following calculation. It is responsible for the correctness of information about the percentage change in the asset price. I think there is no difficulty in understanding this calculation, as it is a very common calculation which is used to determine the percentage price rise or fall.

Next, we will position the objects on the canvas. First we place the image. Remember when we created the objects, we mentioned that the size will be used somewhere else. So, it is exactly here, although the only information we need is the width of the image. But, as we previously said, it is better to use 32 x 32, since if you need another size, especially a bigger one, you will have to adjust other values accordingly.

If the size is smaller, there will be no problem as the code will adjust to a smaller images size. But if you create a bigger panel, especially bigger in width, this will mean that without adjusting the value in def_MaxWidth, the information will rotate in a very strange way. Here you should increase the value in the compilation definition. But be careful: if you set a too high value, you will have to wait longer for then information to reappear, than if you use adequate values.

All subsequent values will depend on this value contained in the width of the image, so it will determine the situation. There is a moment that may not make sense: we have a value which is converted to a character, but the character itself does not appear on the screen. What is happening at this moment? Here we create the arrows indicating whether the price goes up or down, based on the daily change. This may seem a little confusing, but it all works well. But please note that we are using exactly the font Wingdings 3. If you want to you other font, you will have to adjust these values to have a correct display.

There is another interesting point. We are sending string and double values to the same function. We would expect two different function, but the object code contains only one. Here this function is overloaded. But the overloading is created by the compiler and the linker. So, we had to make tests inside the function, but if you look more closely, you can see that we have a symbol widely used to denote percentage values (%). Now go inside the Edit object and see where the percentage character is added and how it is done.

To finish this function, we have a point where we adjust the price so that its color is displayed correctly on the panel.

All these changes are implemented directly within the function. We didn't have any modifications regarding the way the system should actually be called. All the necessary changes are within the C_Widget class. But we also have some small changes in the indicator code, so let's see them.

#property indicator_separate_window
#property indicator_plots 0
#property indicator_height 32
//+------------------------------------------------------------------+
#include <Widget\Rolling Price\C_Widget.mqh>
//+------------------------------------------------------------------+
input string user00 = "Config.cfg";  //Settings file
input int    user01 = -1;            //Shift
input int    user02 = 10;            //Pause in milliseconds
input
 color  user03 = clrWhiteSmoke; //Asset color
input color  user04 = clrBlack;      //Price color
//+------------------------------------------------------------------+
C_Widget Widget;
//+------------------------------------------------------------------+
int OnInit()
{
        if (!Widget.Initilize(user00, "Widget Price", user03, user04))
                return INIT_FAILED;
        EventSetMillisecondTimer(user02);
        
        return INIT_SUCCEEDED;
}

The only moment worth mentioning is where we set the indicator height value. Everything else is the same as in the basic version.

We could finish the indicator here. But how do you like the idea of implementing some additional functionality that will be useful during the trading period? We will talk about this in the next article.


Additional functions

How about adding to the panel the assets that you want to trade? And not only that. Let's go a step further.

How about this: When you click on the asset displayed on the panel, the relevant asset chart is instantly opened so that you can follow the situation and enter a trade when you see that a trend is beginning. And you can do all this in a super easy way, without having to type anything!

A great idea, isn't it? You must be thinking that this is something ultra super mega difficult to do. That you need to be a professional programmer with experience and knowledge in nuclear physics or alien technology. But not. This can be easily done using MQL5 with the MetaTrader 5 platform.

To do this, we need to add a small piece of code to the message handling system. This is done below:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
        {
                static int tx = 0;
                string szRet[];
                                                        
                switch (id)
                {

// ... Internal code...

                        case CHARTEVENT_OBJECT_CLICK:
                                if (StringSplit(sparam, '#', szRet) == 2)
                                {
                                        SymbolSelect(szRet[1], true);
                                        szRet[0] = ChartSymbol(Terminal.Get_ID());
                                        if (ChartSetSymbolPeriod(Terminal.Get_ID(), szRet[1], PERIOD_CURRENT)) SymbolSelect(szRet[0], false);
                                        else SymbolSelect(szRet[1], false);
                                }
                                break;
                }
        }

When we create objects, we don't just create them in any way we can - we use a very specific format, and thanks to this, we can analyze the situation to which object is clicked on and which asset it was associated with. So, here we find out which asset the object is associated with. To do this, we use a very interesting function. 

StringSplit can split data depending on how it was formatted. So, we will obtain an indication of which asset the object, which was clicked, is associated with. Based on this, we instruct the MetaTrader 5 platform to open the corresponding asset chart.

However, to make this work, the asset must be present in the Market Watch window. So, we execute this line to make the symbol appear in Market Watch. Then, before deleting the current asset from the window, we capture its code and try to make the clicked asset to appear in the window. If we succeed, we try to remove the asset that was on the chart from Market Watch. But if an attempt to change the asset fails, the asset we are trying to open would be removed from the market watch.

Please note a few points related to this change of assets using the panel. First, the asset required will be opened with the same timeframe, which the previous asset had. You can change the timeframe later, but initially it will have the same timeframe. Another equally important point is that we must select assets so that there is a reasonable number of assets on the panel, because it will take a lot of time to display them on the panel again. With each change in timeframe or asset, the panel will always start from the first asset in the list.

The following video demonstrates how the system works in practice.



Translated from Portuguese by MetaQuotes Ltd.
Original article: https://www.mql5.com/pt/articles/10963

Attached files |
DoEasy. Controls (Part 31): Scrolling the contents of the ScrollBar control DoEasy. Controls (Part 31): Scrolling the contents of the ScrollBar control
In this article, I will implement the functionality of scrolling the contents of the container using the buttons of the horizontal scrollbar.
Creating a ticker tape panel: Basic version Creating a ticker tape panel: Basic version
Here I will show how to create screens with price tickers which are usually used to display quotes on the exchange. I will do it by only using MQL5, without using complex external programming.
How to choose an Expert Advisor: Twenty strong criteria to reject a trading bot How to choose an Expert Advisor: Twenty strong criteria to reject a trading bot
This article tries to answer the question: how can we choose the right expert advisors? Which are the best for our portfolio, and how can we filter the large trading bots list available on the market? This article will present twenty clear and strong criteria to reject an expert advisor. Each criterion will be presented and well explained to help you make a more sustained decision and build a more profitable expert advisor collection for your profits.
Population optimization algorithms: Firefly Algorithm (FA) Population optimization algorithms: Firefly Algorithm (FA)
In this article, I will consider the Firefly Algorithm (FA) optimization method. Thanks to the modification, the algorithm has turned from an outsider into a real rating table leader.