
Graphics in DoEasy library (Part 78): Animation principles in the library. Image slicing
Contents
Concept
A graphical interface inherently implies the presence of non-static images. Any displayed data (for example, the one presented in tables) may change over time. GUI elements may react to user actions by using various visual effects, etc.
I will create the methods arranging various visual effects and endow the library with the ability to work with sprite animation. The animation is based on using a changing (frame by frame) sequence of static images.
The CCanvas class allows drawing images on canvas. From a series of images drawn and saved in image arrays, we can build a certain sequence, which will eventually be an animated image. But if we just draw each subsequent image on the canvas one by one, then they will simply overlap each other eventually leading to a chaotic pile of pixels, like in the image below (here I simply display the text in different locations of the form object):
To avoid this, we need to completely erase the previous image, re-draw the background and display the text on it (I did this in one of the previous articles when placing the text, describing the text anchor method, in the form). This option is viable only as long as the size and complexity of the redrawn form are small. Another option is saving a part of the background the text is to be superimposed on in memory (in the array), then add the text. When it should be relocated to new coordinates, overwrite the drawn text using the previously saved background image from the array (to restore the background) and draw the text in the new location (preliminarily saving the part of the background of the location the text is moved to). Thus, the background of the location an image is to be superimposed on is constantly stored in memory and restored if the image needs to be changed.
This is the minimum element of the sprite animation concept I am going to introduce in the library:
- Saving a background with necessary coordinates
- Displaying an image using the coordinates
- Restoring the background when redrawing the image
To achieve all this, I will create a small class for storing the image coordinates and size. The method saving a part of a background image using these coordinates and size will be created in the class as well. Besides, we will need the second method, which stores the background saved in the array (the size and coordinates will be saved in the class variables when saving the background to the array).
Why am I creating a class rather than making two such methods for the form object? Here all is simple: if we need to display only text or a single animated image, then two methods are sufficient. But if we need to display several texts in different locations of the form, the class is more convenient. Each animated image receives its own class instances that can be managed separately.
Such a concept allows drawing something using previously drawn image as a background — we will save both the background and the drawn image, which in turn can then be removed from the background.
I will use this concept to develop the class for creating, storing and displaying various sprite animations on the form object — each class instance contains a sequence of images that can be dynamically added to the list and handled.
Improving library classes
As usual, let's add new message indices to \MQL5\Include\DoEasy\Data.mqh:
//--- CChartObjCollection MSG_CHART_COLLECTION_TEXT_CHART_COLLECTION, // Chart collection MSG_CHART_COLLECTION_ERR_FAILED_CREATE_CHART_OBJ, // Failed to create a new chart object MSG_CHART_COLLECTION_ERR_FAILED_ADD_CHART, // Failed to add a chart object to the collection MSG_CHART_COLLECTION_ERR_CHARTS_MAX, // Cannot open new chart. Number of open charts at maximum MSG_CHART_COLLECTION_CHART_OPENED, // Chart opened MSG_CHART_COLLECTION_CHART_CLOSED, // Chart closed MSG_CHART_COLLECTION_CHART_SYMB_CHANGED, // Chart symbol changed MSG_CHART_COLLECTION_CHART_TF_CHANGED, // Chart timeframe changed MSG_CHART_COLLECTION_CHART_SYMB_TF_CHANGED, // Chart symbol and timeframe changed //--- CGCnvElement MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY, // Error! Empty array //--- CForm MSG_FORM_OBJECT_TEXT_NO_SHADOW_OBJ_FIRST_CREATE_IT,// No shadow object. Create it using the CreateShadowObj() method MSG_FORM_OBJECT_ERR_FAILED_CREATE_SHADOW_OBJ, // Failed to create new shadow object MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ, // Failed to create new pixel copier object MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST, // Pixel copier object with ID already present in the list MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST, // No pixel copier object with ID in the list //--- CShadowObj MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE, // Error! Image size too small or blur too extensive }; //+------------------------------------------------------------------+
and message texts corresponding to newly added indices:
//--- CChartObjCollection {"Коллекция чартов","Chart collection"}, {"Не удалось создать новый объект-чарт","Failed to create new chart object"}, {"Не удалось добавить объект-чарт в коллекцию","Failed to add chart object to collection"}, {"Нельзя открыть новый график, так как количество открытых графиков уже максимальное","You cannot open a new chart, since the number of open charts is already maximum"}, {"Открыт график","Open chart"}, {"Закрыт график","Closed chart"}, {"Изменён символ графика","Changed chart symbol"}, {"Изменён таймфрейм графика","Changed chart timeframe"}, {"Изменён символ и таймфрейм графика","Changed the symbol and timeframe of the chart"}, //--- CGCnvElement {"Ошибка! Пустой массив","Error! Empty array"}, //--- CForm {"Отсутствует объект тени. Необходимо сначала его создать при помощи метода CreateShadowObj()","There is no shadow object. You must first create it using the CreateShadowObj () method"}, {"Не удалось создать новый объект для тени","Failed to create new object for shadow"}, {"Не удалось создать новый объект-копировщик пикселей","Failed to create new pixel copier object"}, {"В списке уже есть объект-копировщик пикселей с идентификатором ","There is already a pixel copier object in the list with ID "}, {"В списке нет объекта-копировщика пикселей с идентификатором ","No pixel copier object with ID "}, //--- CShadowObj {"Ошибка! Размер изображения очень маленький или очень большое размытие","Error! Image size is very small or very large blur"}, }; //+---------------------------------------------------------------------+
Since we are going to draw images and texts on ready-made form objects, inherited from the graphical element object, or on any other GUI objects of custom programs, we need to always have the initial appearance of the object at hand so that we can restore it to its original form at any time.
Of course, we can redraw it anew, but it will be much faster to just copy one array to another.
To do this, I will implement some changes and improvements in \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh of the graphical element object class.
In the protected section of the class, declare the array, which is to contain all pixels of the initial object (its appearance) immediately after its creation, and the method saving the graphical resource of the CCanvas class instance to the array:
//+------------------------------------------------------------------+ //| Class of the graphical element object | //+------------------------------------------------------------------+ class CGCnvElement : public CGBaseObj { protected: CCanvas m_canvas; // CCanvas class object CPause m_pause; // Pause class object bool m_shadow; // Shadow presence color m_chart_color_bg; // Chart background color uint m_data_array[]; // Array for storing resource data copy //--- Return the cursor position relative to the (1) entire element and (2) the element's active area bool CursorInsideElement(const int x,const int y); bool CursorInsideActiveArea(const int x,const int y); //--- Create (1) the object structure and (2) the object from the structure virtual bool ObjectToStruct(void); virtual void StructToObject(void); //--- Save the graphical resource to the array bool ResourceCopy(const string source); private:
Thus, sacrificing a small amount of memory, we can quickly restore the appearance of any element of the program interface to its original form by simply copying one array to another.
In the private section of the class, declare two variables for storing X and Y coordinates of the last drawn text:
long m_long_prop[ORDER_PROP_INTEGER_TOTAL]; // Integer properties double m_double_prop[ORDER_PROP_DOUBLE_TOTAL]; // Real properties string m_string_prop[ORDER_PROP_STRING_TOTAL]; // String properties ENUM_TEXT_ANCHOR m_text_anchor; // Current text alignment int m_text_x; // Text last X coordinate int m_text_y; // Text last Y coordinate color m_color_bg; // Element background color uchar m_opacity; // Element opacity //--- Return the index of the array the order's (1) double and (2) string properties are located at
In the public section of the class, write the method returning the pointer to the current class instant and declare the method for saving an image in the specified array:
//--- Return the flag of the object supporting this property virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_DOUBLE property) { return false; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property) { return true; } //--- Return itself CGCnvElement *GetObject(void) { return &this; } //--- Compare CGCnvElement objects with each other by all possible properties (for sorting the lists by a specified object property) virtual int Compare(const CObject *node,const int mode=0) const; //--- Compare CGCnvElement objects with each other by all properties (to search equal objects) bool IsEqual(CGCnvElement* compared_obj) const; //--- (1) Save the object to file and (2) upload the object from the file virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); //--- Create the element bool Create(const long chart_id, const int wnd_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool redraw=false); //--- Return the pointer to a canvas object CCanvas *GetCanvasObj(void) { return &this.m_canvas; } //--- Set the canvas update frequency void SetFrequency(const ulong value) { this.m_pause.SetWaitingMSC(value); } //--- Update the coordinates (shift the canvas) bool Move(const int x,const int y,const bool redraw=false); //--- Save an image to the array bool ImageCopy(const string source,uint &array[]);
The method allowing the class to return the pointer to itself is necessary to pass the pointer to the class to the pixel copier class considered below, while the method copying the graphical resource of the CCanvas instance is necessary to quickly copy the form appearance to the necessary array in the library-based program.
In the code block of the methods for working with a text, add two methods for returning X and Y coordinates of the last drawn text:
//+------------------------------------------------------------------+ //| Methods of working with text | //+------------------------------------------------------------------+ //--- Return (1) alignment type (anchor method), the last (2) X and (3) Y text coordinate ENUM_TEXT_ANCHOR TextAnchor(void) const { return this.m_text_anchor; } int TextLastX(void) const { return this.m_text_x; } int TextLastY(void) const { return this.m_text_y; } //--- Set the current font
The methods simply return the values of the appropriate variables.
In order for these values to always remain relevant, write the coordinates, passed to the method arguments, to the variables in the method displaying the text using the current font:
//--- Display the text in the current font void Text(int x, // X coordinate of the text anchor point int y, // Y coordinate of the text anchor point string text, // Display text const color clr, // Color const uchar opacity=255, // Opacity uint alignment=0) // Text anchoring method { this.m_text_anchor=(ENUM_TEXT_ANCHOR)alignment; this.m_text_x=x; this.m_text_y=y; this.m_canvas.TextOut(x,y,text,::ColorToARGB(clr,opacity),alignment); }
The drawn text can have nine anchor points:
For example, if the text anchor point is located in the bottom right corner (Right|Bottom), this will be the starting XY coordinate. All initial coordinates in the library correspond to the rectangle left top corner (Left|Top). So, if we save the image with initial text coordinates, the text will be located to the right bottom of the saved image. This will not allow us to correctly save the area of the background the text will be superimposed on.
Therefore, we need to calculate the offsets of the coordinates of the text outlining rectangle, where it is necessary to save the background to the array for its subsequent restoration. The width and height of the future text are calculated in advance — before drawing the text. We just need to specify the text itself. The TextSize() method of the CCanvas class returns the width and height of the outlining rectangle.
In the public section of the class, declare the method returning X/Y offsets depending on the text alignment method:
//--- Return coordinate offsets relative to the text anchor point void TextGetShiftXY(const string text, // Text for calculating the size of its outlining rectangle const ENUM_TEXT_ANCHOR anchor,// Text anchor point, relative to which the offsets are calculated int &shift_x, // X coordinate of the rectangle upper left corner int &shift_y); // Y coordinate of the rectangle upper left corner }; //+------------------------------------------------------------------+ //| Parametric constructor | //+------------------------------------------------------------------+
The method will be considered below.
Initialize the coordinates of the last drawn text in the parametric class constructor:
//+------------------------------------------------------------------+ //| Parametric constructor | //+------------------------------------------------------------------+ CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const int element_id, const int element_num, const long chart_id, const int wnd_num, const string name, const int x, const int y, const int w, const int h, const color colour, const uchar opacity, const bool movable=true, const bool activity=true, const bool redraw=false) : m_shadow(false) { this.m_chart_color_bg=(color)::ChartGetInteger(chart_id,CHART_COLOR_BACKGROUND); this.m_name=this.m_name_prefix+name; this.m_chart_id=chart_id; this.m_subwindow=wnd_num; this.m_type=element_type; this.SetFont("Calibri",8); this.m_text_anchor=0; this.m_text_x=0; this.m_text_y=0; this.m_color_bg=colour; this.m_opacity=opacity; if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,colour,opacity,redraw)) { this.SetProperty(CANV_ELEMENT_PROP_NAME_RES,this.m_canvas.ResourceName()); // Graphical resource name this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID()); // Chart ID this.SetProperty(CANV_ELEMENT_PROP_WND_NUM,CGBaseObj::SubWindow()); // Chart subwindow index this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,CGBaseObj::Name()); // Element object name this.SetProperty(CANV_ELEMENT_PROP_TYPE,element_type); // Graphical element type this.SetProperty(CANV_ELEMENT_PROP_ID,element_id); // Element ID this.SetProperty(CANV_ELEMENT_PROP_NUM,element_num); // Element index in the list this.SetProperty(CANV_ELEMENT_PROP_COORD_X,x); // Element's X coordinate on the chart this.SetProperty(CANV_ELEMENT_PROP_COORD_Y,y); // Element's Y coordinate on the chart this.SetProperty(CANV_ELEMENT_PROP_WIDTH,w); // Element width this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,h); // Element height this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,0); // Active area offset from the left edge of the element this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,0); // Active area offset from the upper edge of the element this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,0); // Active area offset from the right edge of the element this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,0); // Active area offset from the bottom edge of the element this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,movable); // Element moveability flag this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,activity); // Element activity flag this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.RightEdge()); // Element right border this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.BottomEdge()); // Element bottom border this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_X,this.ActiveAreaLeft()); // X coordinate of the element active area this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_Y,this.ActiveAreaTop()); // Y coordinate of the element active area this.SetProperty(CANV_ELEMENT_PROP_ACT_RIGHT,this.ActiveAreaRight()); // Right border of the element active area this.SetProperty(CANV_ELEMENT_PROP_ACT_BOTTOM,this.ActiveAreaBottom()); // Bottom border of the element active area } else { ::Print(CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.m_name); } } //+------------------------------------------------------------------+
Initialize the variables in the protected constructor the same way:
//+------------------------------------------------------------------+ //| Protected constructor | //+------------------------------------------------------------------+ CGCnvElement::CGCnvElement(const ENUM_GRAPH_ELEMENT_TYPE element_type, const long chart_id, const int wnd_num, const string name, const int x, const int y, const int w, const int h) : m_shadow(false) { this.m_chart_color_bg=(color)::ChartGetInteger(chart_id,CHART_COLOR_BACKGROUND); this.m_name=this.m_name_prefix+name; this.m_chart_id=chart_id; this.m_subwindow=wnd_num; this.m_type=element_type; this.SetFont("Calibri",8); this.m_text_anchor=0; this.m_text_x=0; this.m_text_y=0; this.m_color_bg=NULL_COLOR; this.m_opacity=0; if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,this.m_color_bg,this.m_opacity,false)) { ...
Now let's consider the implementation of the methods declared above.
Implementing the method saving the image to the array:
//+------------------------------------------------------------------+ //| Save the image to the array | //+------------------------------------------------------------------+ bool CGCnvElement::ImageCopy(const string source,uint &array[]) { ::ResetLastError(); int w=0,h=0; if(!::ResourceReadImage(this.NameRes(),array,w,h)) { CMessage::ToLog(source,MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES,true); return false; } return true; } //+------------------------------------------------------------------+
The method receives the name of the method or function it has been called from (to find a possible error) and the link to the array the graphical resource data (image pixels) should be written to.
Use the ResourceReadImage() function to read into the array the data of the graphical resource created by the CCanvas class and containing the image of the form. In case of a resource reading error, inform of that and return false. If all is well, return true. All image pixels stored in the resource are written to the array passed to the method.
The method saving the graphical resource to the array:
//+------------------------------------------------------------------+ //| Save the graphical resource to the array | //+------------------------------------------------------------------+ bool CGCnvElement::ResourceCopy(const string source) { return this.ImageCopy(DFUN,this.m_data_array); } //+------------------------------------------------------------------+
The method returns the result of calling the method considered above. The only difference is that the graphical resource data is written in the previously declared special array for storing the copy of the image of the entire form object, rather than in the array passed by the link.
The method returning coordinate offsets relative to the text anchor point:
//+------------------------------------------------------------------+ //| Return coordinate offsets relative to the text anchor point | //+------------------------------------------------------------------+ void CGCnvElement::TextGetShiftXY(const string text,const ENUM_TEXT_ANCHOR anchor,int &shift_x,int &shift_y) { int tw=0,th=0; this.TextSize(text,tw,th); switch(anchor) { case TEXT_ANCHOR_LEFT_TOP : shift_x=0; shift_y=0; break; case TEXT_ANCHOR_LEFT_CENTER : shift_x=0; shift_y=-th/2; break; case TEXT_ANCHOR_LEFT_BOTTOM : shift_x=0; shift_y=-th; break; case TEXT_ANCHOR_CENTER_TOP : shift_x=-tw/2; shift_y=0; break; case TEXT_ANCHOR_CENTER : shift_x=-tw/2; shift_y=-th/2; break; case TEXT_ANCHOR_CENTER_BOTTOM : shift_x=-tw/2; shift_y=-th; break; case TEXT_ANCHOR_RIGHT_TOP : shift_x=-tw; shift_y=0; break; case TEXT_ANCHOR_RIGHT_CENTER : shift_x=-tw; shift_y=-th/2; break; case TEXT_ANCHOR_RIGHT_BOTTOM : shift_x=-tw; shift_y=-th; break; default: shift_x=0; shift_y=0; break; } } //+------------------------------------------------------------------+
Here we first get the size of the text passed to the method (the size is set in the declared variables) and then simply calculate the number of pixels required to move the X and Y coordinates relative to the initial text coordinates depending on the anchor method of the text passed to the method.
Now it is time to improve the shadow object class. Since I have just added the methods for reading the graphical resource and a constant array where I can store the copy of the graphical resource, excessive variables, arrays and code blocks can be removed from the shadow object class.
Let's improve the \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh file.
Remove the array and unnecessary variables from the Gaussian blur method:
//+------------------------------------------------------------------+ //| Gaussian blur | //| https://www.mql5.com/en/articles/1612#chapter4 | //+------------------------------------------------------------------+ bool CShadowObj::GaussianBlur(const uint radius) { //--- int n_nodes=(int)radius*2+1; uint res_data[]; // Array for storing graphical resource data uint res_w=this.Width(); // Graphical resource width uint res_h=this.Height(); // Graphical resource height //--- Read graphical resource data. If failed, return false
In the block of reading the graphical resource data, replace the strings with calling the method shown above:
//--- Read graphical resource data. If failed, return false ::ResetLastError(); if(!::ResourceReadImage(this.NameRes(),res_data,res_w,res_h)) { CMessage::OutByID(MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES); return false; } //--- Check the blur amount. If the blur radius exceeds half of the width or height, return 'false' //--- Read graphical resource data. If failed, return false if(!CGCnvElement::ResourceCopy(DFUN)) return false;
Instead of the removed res_w and res_h variables, I will use the Width() and Height() methods of the graphical element object class in the entire code. Instead of the res_data array, I will use the m_data_array array, which is now used for storing the copy of the graphical resource.
In general, all the improvements have boiled down to replacing unnecessary and removed variables with the methods of the graphical element object class:
//+------------------------------------------------------------------+ //| Gaussian blur | //| https://www.mql5.com/en/articles/1612#chapter4 | //+------------------------------------------------------------------+ bool CShadowObj::GaussianBlur(const uint radius) { //--- int n_nodes=(int)radius*2+1; //--- Read graphical resource data. If failed, return false if(!CGCnvElement::ResourceCopy(DFUN)) return false; //--- Check the blur amount. If the blur radius exceeds half of the width or height, return 'false' if((int)radius>=this.Width()/2 || (int)radius>=this.Height()/2) { ::Print(DFUN,CMessage::Text(MSG_SHADOW_OBJ_IMG_SMALL_BLUR_LARGE)); return false; } //--- Decompose image data from the resource into a, r, g, b color components int size=::ArraySize(this.m_data_array); //--- arrays for storing A, R, G and B color components //--- for horizontal and vertical blur uchar a_h_data[],r_h_data[],g_h_data[],b_h_data[]; uchar a_v_data[],r_v_data[],g_v_data[],b_v_data[]; //--- Change the size of component arrays according to the array size of the graphical resource data if(::ArrayResize(a_h_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"a_h_data\""); return false; } if(::ArrayResize(r_h_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"r_h_data\""); return false; } if(::ArrayResize(g_h_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"g_h_data\""); return false; } if(ArrayResize(b_h_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"b_h_data\""); return false; } if(::ArrayResize(a_v_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"a_v_data\""); return false; } if(::ArrayResize(r_v_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"r_v_data\""); return false; } if(::ArrayResize(g_v_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"g_v_data\""); return false; } if(::ArrayResize(b_v_data,size)==-1) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE); ::Print(DFUN_ERR_LINE,": \"b_v_data\""); return false; } //--- Declare the array for storing blur weight ratios and, //--- if failed to get the array of weight ratios, return 'false' double weights[]; if(!this.GetQuadratureWeights(1,n_nodes,weights)) return false; //--- Set components of each image pixel to the color component arrays for(int i=0;i<size;i++) { a_h_data[i]=GETRGBA(this.m_data_array[i]); r_h_data[i]=GETRGBR(this.m_data_array[i]); g_h_data[i]=GETRGBG(this.m_data_array[i]); b_h_data[i]=GETRGBB(this.m_data_array[i]); } //--- Blur the image horizontally (along the X axis) uint XY; // Pixel coordinate in the array double a_temp=0.0,r_temp=0.0,g_temp=0.0,b_temp=0.0; int coef=0; int j=(int)radius; //--- Loop by the image width for(int Y=0;Y<this.Height();Y++) { //--- Loop by the image height for(uint X=radius;X<this.Width()-radius;X++) { XY=Y*this.Width()+X; a_temp=0.0; r_temp=0.0; g_temp=0.0; b_temp=0.0; coef=0; //--- Multiply each color component by the weight ratio corresponding to the current image pixel for(int i=-1*j;i<j+1;i=i+1) { a_temp+=a_h_data[XY+i]*weights[coef]; r_temp+=r_h_data[XY+i]*weights[coef]; g_temp+=g_h_data[XY+i]*weights[coef]; b_temp+=b_h_data[XY+i]*weights[coef]; coef++; } //--- Save each rounded color component calculated according to the ratios to the component arrays a_h_data[XY]=(uchar)::round(a_temp); r_h_data[XY]=(uchar)::round(r_temp); g_h_data[XY]=(uchar)::round(g_temp); b_h_data[XY]=(uchar)::round(b_temp); } //--- Remove blur artifacts to the left by copying adjacent pixels for(uint x=0;x<radius;x++) { XY=Y*this.Width()+x; a_h_data[XY]=a_h_data[Y*this.Width()+radius]; r_h_data[XY]=r_h_data[Y*this.Width()+radius]; g_h_data[XY]=g_h_data[Y*this.Width()+radius]; b_h_data[XY]=b_h_data[Y*this.Width()+radius]; } //--- Remove blur artifacts to the right by copying adjacent pixels for(int x=int(this.Width()-radius);x<this.Width();x++) { XY=Y*this.Width()+x; a_h_data[XY]=a_h_data[(Y+1)*this.Width()-radius-1]; r_h_data[XY]=r_h_data[(Y+1)*this.Width()-radius-1]; g_h_data[XY]=g_h_data[(Y+1)*this.Width()-radius-1]; b_h_data[XY]=b_h_data[(Y+1)*this.Width()-radius-1]; } } //--- Blur vertically (along the Y axis) the image already blurred horizontally int dxdy=0; //--- Loop by the image height for(int X=0;X<this.Width();X++) { //--- Loop by the image width for(uint Y=radius;Y<this.Height()-radius;Y++) { XY=Y*this.Width()+X; a_temp=0.0; r_temp=0.0; g_temp=0.0; b_temp=0.0; coef=0; //--- Multiply each color component by the weight ratio corresponding to the current image pixel for(int i=-1*j;i<j+1;i=i+1) { dxdy=i*(int)this.Width(); a_temp+=a_h_data[XY+dxdy]*weights[coef]; r_temp+=r_h_data[XY+dxdy]*weights[coef]; g_temp+=g_h_data[XY+dxdy]*weights[coef]; b_temp+=b_h_data[XY+dxdy]*weights[coef]; coef++; } //--- Save each rounded color component calculated according to the ratios to the component arrays a_v_data[XY]=(uchar)::round(a_temp); r_v_data[XY]=(uchar)::round(r_temp); g_v_data[XY]=(uchar)::round(g_temp); b_v_data[XY]=(uchar)::round(b_temp); } //--- Remove blur artifacts at the top by copying adjacent pixels for(uint y=0;y<radius;y++) { XY=y*this.Width()+X; a_v_data[XY]=a_v_data[X+radius*this.Width()]; r_v_data[XY]=r_v_data[X+radius*this.Width()]; g_v_data[XY]=g_v_data[X+radius*this.Width()]; b_v_data[XY]=b_v_data[X+radius*this.Width()]; } //--- Remove blur artifacts at the bottom by copying adjacent pixels for(int y=int(this.Height()-radius);y<this.Height();y++) { XY=y*this.Width()+X; a_v_data[XY]=a_v_data[X+(this.Height()-1-radius)*this.Width()]; r_v_data[XY]=r_v_data[X+(this.Height()-1-radius)*this.Width()]; g_v_data[XY]=g_v_data[X+(this.Height()-1-radius)*this.Width()]; b_v_data[XY]=b_v_data[X+(this.Height()-1-radius)*this.Width()]; } } //--- Set the twice blurred (horizontally and vertically) image pixels to the graphical resource data array for(int i=0;i<size;i++) this.m_data_array[i]=ARGB(a_v_data[i],r_v_data[i],g_v_data[i],b_v_data[i]); //--- Display the image pixels on the canvas in a loop by the image height and width from the graphical resource data array for(int X=0;X<this.Width();X++) { for(uint Y=radius;Y<this.Height()-radius;Y++) { XY=Y*this.Width()+X; this.m_canvas.PixelSet(X,Y,this.m_data_array[XY]); } } //--- Done return true; } //+------------------------------------------------------------------+
Now all is ready for developing the class. Its object will enable us to manage the drawing of any graphical elements on the canvas so that we can later easily restore the background of the image the new drawing has been superimposed on. Further on, this will allow me to create the class for working with sprite animation.
Class for copying and pasting image parts
The form object class is to be the minimum object in the inheritance hierarchy, in which we are to be able to work with animation.
Since the class for saving and restoring a part of the image is to be small, we will place it directly in the form object class file \MQL5\Include\DoEasy\Objects\Graph\Form.mqh. I will name the class the 'pixel copier', which clearly describes its objective.
Each pixel copier class object is to have a custom ID allowing us to define an image the object is working with. It will be possible to refer to the necessary class object by its ID so that each animated object can be handled separately. For example, if we need to manage and change three images simultaneously with two of them being texts and one being an image, then, when creating a copier object for each image, we simply need to assign different IDs to them — text1 = ID0, text2 = ID1, image = ID2. In this case, each of the objects will store all the remaining parameters for working with it, namely:
- the array of pixels storing the part of a background an image is superimposed on,
- X and Y coordinates of the upper left corner of the rectangular area of the background an image is superimposed on,
- the rectangle area width and height
- and the calculated width and height of the area.
We need the calculated width and height in order to know exactly the width and height of the rectangular copy area in case the rectangle goes beyond the area of the form whose pixels should be saved. Further on, when restoring the background, we will no longer need to re-calculate the width and height of the actually copied rectangular background area, but simply use the already calculated values stored in the object variables.
In the private section of the class, declare the pointer to the graphical element object class (I will pass it to the newly created pixel copier class object to be able to use the data of the form, in which we create the instance of the copier object), the array to store the part of the form image that should be saved and restored, and all variables described above:
//+------------------------------------------------------------------+ //| Form.mqh | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" #property strict // Necessary for mql4 //+------------------------------------------------------------------+ //| Include files | //+------------------------------------------------------------------+ #include "GCnvElement.mqh" #include "ShadowObj.mqh" //+------------------------------------------------------------------+ //| Pixel copier class | //+------------------------------------------------------------------+ class CPixelCopier : public CObject { private: CGCnvElement *m_element; // Pointer to the graphical element uint m_array[]; // Pixel array int m_id; // ID int m_x; // X coordinate of the upper left corner int m_y; // Y coordinate of the upper left corner int m_w; // Copied image width int m_h; // Copied image height int m_wr; // Calculated copied image width int m_hr; // Calculated copied image height public:
In the public section of the class, write the method for comparing two copier objects, the methods for setting and receiving object properties, class constructors — default and parametric ones, and declare two methods for saving and restoring the background part:
public: //--- Compare CPixelCopier objects by a specified property (to sort the list by an object property) virtual int Compare(const CObject *node,const int mode=0) const { const CPixelCopier *obj_compared=node; return(mode==0 ? (this.ID()>obj_compared.ID() ? 1 : this.ID()<obj_compared.ID() ? -1 : 0) : WRONG_VALUE); } //--- Set the properties void SetID(const int id) { this.m_id=id; } void SetCoordX(const int value) { this.m_x=value; } void SetCoordY(const int value) { this.m_y=value; } void SetWidth(const int value) { this.m_w=value; } void SetHeight(const int value) { this.m_h=value; } //--- Get the properties int ID(void) const { return this.m_id; } int CoordX(void) const { return this.m_x; } int CoordY(void) const { return this.m_y; } int Width(void) const { return this.m_w; } int Height(void) const { return this.m_h; } int WidthReal(void) const { return this.m_wr; } int HeightReal(void) const { return this.m_hr; } //--- Copy the part or the entire image to the array bool CopyImgDataToArray(const uint x_coord,const uint y_coord,uint width,uint height); //--- Copy the part or the entire image from the array to the canvas bool CopyImgDataToCanvas(const int x_coord,const int y_coord); //--- Constructors CPixelCopier (void){;} CPixelCopier (const int id, const int x, const int y, const int w, const int h, CGCnvElement *element) : m_id(id), m_x(x),m_y(y),m_w(w),m_wr(w),m_h(h),m_hr(h) { this.m_element=element; } ~CPixelCopier (void){;} }; //+------------------------------------------------------------------+
Let's consider the methods in more details.
The method comparing two copier objects:
//--- Compare CPixelCopier objects by a specified property (to sort the list by an object property) virtual int Compare(const CObject *node,const int mode=0) const { const CPixelCopier *obj_compared=node; return(mode==0 ? (this.ID()>obj_compared.ID() ? 1 : this.ID()<obj_compared.ID() ? -1 : 0) : WRONG_VALUE); }
Here all is standard, like in other library classes. If the comparison mode (mode) is equal to 0 (by default), the IDs of the current object and the one, the pointer to which is passed to the method, are compared. If the current object ID is greater, 1 is returned, if less -1, if equal - 0. In all other cases (if mode != 0), -1 is returned. Currently, the method is able to compare only object IDs.
In the initialization list of the parametric class constructor, values passed in the arguments are assigned to all class member variables, while in the class body, the pointer value is assigned to the variable pointing to the graphical element object class. The pointer value is also passed in the arguments:
CPixelCopier (const int id, const int x, const int y, const int w, const int h, CGCnvElement *element) : m_id(id), m_x(x),m_y(y),m_w(w),m_wr(w),m_h(h),m_hr(h) { this.m_element=element; }
Now the newly created copier object will "know", which object created it, and will have access to its methods and parameters.
The method copying part or all the image into the array:
//+------------------------------------------------------------------+ //| Copy part or all of the image to the array | //+------------------------------------------------------------------+ bool CPixelCopier::CopyImgDataToArray(const uint x_coord,const uint y_coord,uint width,uint height) { //--- Assign coordinate values, passed to the method, to the variables int x1=(int)x_coord; int y1=(int)y_coord; //--- If X coordinates goes beyond the form on the right or Y coordinate goes beyond the form at the bottom, //--- there is nothing to copy, the copied area is outside the form. Return 'false' if(x1>this.m_element.Width()-1 || y1>this.m_element.Height()-1) return false; //--- Assign the width and height values of the copied area to the variables //--- If the passed width and height are equal to zero, assign the form width and height to them this.m_wr=int(width==0 ? this.m_element.Width() : width); this.m_hr=int(height==0 ? this.m_element.Height() : height); //--- If X and Y coordinates are equal to zero (the upper left corner of the form), as well as the width and height are equal to the form width and height, //--- the copied area is equal to the entire form area. Copy the entire form (returning it from the method) using the ImageCopy() method if(x1==0 && y1==0 && this.m_wr==this.m_element.Width() && this.m_hr==this.m_element.Height()) return this.m_element.ImageCopy(DFUN,this.m_array); //--- Calculate the right X coordinate and lower Y coordinate of the rectangle area int x2=int(x1+this.m_wr-1); int y2=int(y1+this.m_hr-1); //--- If the calculated X coordinate goes beyond the form, the right edge of the form will be used as the coordinate if(x2>=this.m_element.Width()-1) x2=this.m_element.Width()-1; //--- If the calculated Y coordinate goes beyond the form, the bottom edge of the form will be used as the coordinate if(y2>=this.m_element.Height()-1) y2=this.m_element.Height()-1; //--- Calculate the copied width and height this.m_wr=x2-x1+1; this.m_hr=y2-y1+1; //--- Define the necessary size of the array, which is to store all image pixels with calculated width and height int size=this.m_wr*this.m_hr; //--- If failed to set the array size, inform of that and return 'false' if(::ArrayResize(this.m_array,size)!=size) { CMessage::ToLog(DFUN,MSG_LIB_SYS_FAILED_ARRAY_RESIZE,true); return false; } //--- Set the index in the array for recording the image pixel int n=0; //--- In a loop by the calculated height of the copied area, starting from the specified Y coordinate for(int y=y1;y<y1+this.m_hr;y++) { //--- in a loop by the calculated width of the copied area, starting from the specified X coordinate for(int x=x1;x<x1+this.m_wr;x++) { //--- Copy the next image pixel to the array and increase the array index this.m_array[n]=this.m_element.GetCanvasObj().PixelGet(x,y); n++; } } //--- Successful - return 'true' return true; } //+------------------------------------------------------------------+
Each method string is described in detail in the code. In short, if the initial coordinates of the copied area are outside the form, there is nothing to copy — return false. If the initial coordinates of the copied area match the form coordinates, while the width and height of the copied area are either equal to zero or match the form width and height, the form image is copied in its entirety. If only a part of the image should be saved, first calculate the copied width and height so that they do not go beyond the form and copy all form image pixels falling into the copied area.
The method copying the part or the entire image from the array to the canvas:
//+------------------------------------------------------------------+ //| Copy the part or the entire image from the array to the canvas | //+------------------------------------------------------------------+ bool CPixelCopier::CopyImgDataToCanvas(const int x_coord,const int y_coord) { //--- If the array of saved pixels is empty, inform of that and return 'false' int size=::ArraySize(this.m_array); if(size==0) { CMessage::ToLog(DFUN,MSG_CANV_ELEMENT_ERR_EMPTY_ARRAY,true); return false; } //--- Set the index of the array for reading the image pixel int n=0; //--- In a loop by the previously calculated height of the copied area, starting from the specified Y coordinate for(int y=y_coord;y<y_coord+this.m_hr;y++) { //--- in a loop by the previously calculated width of the copied area, starting from the specified X coordinate for(int x=x_coord;x<x_coord+this.m_wr;x++) { //--- Restore the next image pixel from the array and increase the array index this.m_element.GetCanvasObj().PixelSet(x,y,this.m_array[n]); n++; } } return true; } //+------------------------------------------------------------------+
The method logic is also described in detail in the code comments. Unlike the method saving a part of the image, we no longer need to calculate the coordinates and size of the copied area here as they are all saved in the class variables after the first method operation. Here we only need to copy each line of the restored area to the canvas pixel by pixel in a loop by height, thus restoring a part of the image saved by the previous method.
Now we need to arrange the access to a newly written class from the form object class.
Since I will dynamically create the required number of copier objects, it is necessary to declare the list of such objects in the form object class. Each newly created copier object is added to the list, from which we will be able to get the pointers to the required objects and work with them.
Declare the following list in the private section of the class:
//+------------------------------------------------------------------+ //| Form object class | //+------------------------------------------------------------------+ class CForm : public CGCnvElement { private: CArrayObj m_list_elements; // List of attached elements CArrayObj m_list_pc_obj; // List of pixel copier objects CShadowObj *m_shadow_obj; // Pointer to the shadow object color m_color_frame; // Form frame color int m_frame_width_left; // Form frame width to the left int m_frame_width_right; // Form frame width to the right int m_frame_width_top; // Form frame width at the top int m_frame_width_bottom; // Form frame width at the bottom
Since we cannot have several copier objects with similar IDs, we need the method returning the object presence flag in the list with the specified ID. Let's declare the method:
//--- Create a shadow object void CreateShadowObj(const color colour,const uchar opacity); //--- Return the flag indicating the presence of the copier object with the specified ID in the list bool IsPresentPC(const int id); public:
In the public section of the class, write the method returning the pointer to the current form object and the method returning the list of copier objects:
public: //--- Constructors CForm(const long chart_id, const int subwindow, const string name, const int x, const int y, const int w, const int h); CForm(const int subwindow, const string name, const int x, const int y, const int w, const int h); CForm(const string name, const int x, const int y, const int w, const int h); CForm() { this.Initialize(); } //--- Destructor ~CForm(); //--- Supported form properties (1) integer and (2) string ones virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_INTEGER property) { return true; } virtual bool SupportProperty(ENUM_CANV_ELEMENT_PROP_STRING property) { return true; } //--- Return (1) itself, the list of (2) attached objects, (3) pixel copier objects and (4) the shadow object CForm *GetObject(void) { return &this; } CArrayObj *GetList(void) { return &this.m_list_elements; } CArrayObj *GetListPC(void) { return &this.m_list_pc_obj; } CGCnvElement *GetShadowObj(void) { return this.m_shadow_obj; }
Next, declare the method creating a new image pixel copier object:
//--- Create a new pixel copier object CPixelCopier *CreateNewPixelCopier(const int id,const int x_coord,const int y_coord,const int width,const int height); //--- Draw an object shadow void DrawShadow(const int shift_x,const int shift_y,const color colour,const uchar opacity=127,const uchar blur=4);
Add the code block for working with image pixels before the code block with the methods of a simplified access to object properties:
//+------------------------------------------------------------------+ //| Methods of working with image pixels | //+------------------------------------------------------------------+ //--- Return the pixel copier object by ID CPixelCopier *GetPixelCopier(const int id); //--- Copy the part or the entire image to the array bool ImageCopy(const int id,const uint x_coord,const uint y_coord,uint &width,uint &height); //--- Copy the part or the entire image from the array to the canvas bool ImagePaste(const int id,const uint x_coord,const uint y_coord); //+------------------------------------------------------------------+
Implement declared methods outside the class body.
The method returning the flag indicating the presence of the copier object with the specified ID in the list:
//+------------------------------------------------------------------+ //| Return the flag indicating the presence | //| of the copier object with the specified ID in the list | //+------------------------------------------------------------------+ bool CForm::IsPresentPC(const int id) { for(int i=0;i<this.m_list_pc_obj.Total();i++) { CPixelCopier *pc=this.m_list_pc_obj.At(i); if(pc==NULL) continue; if(pc.ID()==id) return true; } return false; } //+------------------------------------------------------------------+
Here we get the next object in a simple loop by the list of copier objects. If its ID is equal to the one passed to the method, return true. Upon the loop completion, return false.
The method creating a new image pixel copier object:
//+------------------------------------------------------------------+ //| Create a new pixel copier object | //+------------------------------------------------------------------+ CPixelCopier *CForm::CreateNewPixelCopier(const int id,const int x_coord,const int y_coord,const int width,const int height) { //--- If the object with such an ID is already present, inform of that in the journal and return NULL if(this.IsPresentPC(id)) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_PC_OBJ_ALREADY_IN_LIST),(string)id); return NULL; } //--- Create a new copier object with the specified parameters CPixelCopier *pc=new CPixelCopier(id,x_coord,y_coord,width,height,CGCnvElement::GetObject()); //--- If failed to create an object, inform of that and return NULL if(pc==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_ERR_FAILED_CREATE_PC_OBJ)); return NULL; } //--- If failed to add the created object to the list, inform of that, remove the object and return NULL if(!this.m_list_pc_obj.Add(pc)) { ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_OBJ_ADD_TO_LIST)," ID: ",id); delete pc; return NULL; } //--- Return the pointer to a newly created object return pc; } //+------------------------------------------------------------------+
The entire method logic is described in the comments to the code. If you have any questions, feel free to ask them in the comments below.
The method returning the pointer to the pixel copier object by ID:
//+------------------------------------------------------------------+ //| Return the pixel copier object by ID | //+------------------------------------------------------------------+ CPixelCopier *CForm::GetPixelCopier(const int id) { for(int i=0;i<this.m_list_pc_obj.Total();i++) { CPixelCopier *pc=m_list_pc_obj.At(i); if(pc==NULL) continue; if(pc.ID()==id) return pc; } return NULL; } //+------------------------------------------------------------------+
Here all is simple. In a loop by the copier object list, get the pointer to the next object. If its ID matches the required one, return the pointer. Upon the loop completion, return NULL — the object with the specified ID is not found in the list.
The method copying part or all the image into the array:
//+------------------------------------------------------------------+ //| Copy part or all of the image to the array | //+------------------------------------------------------------------+ bool CForm::ImageCopy(const int id,const uint x_coord,const uint y_coord,uint &width,uint &height) { CPixelCopier *pc=this.GetPixelCopier(id); if(pc==NULL) { pc=this.CreateNewPixelCopier(id,x_coord,y_coord,width,height); if(pc==NULL) return false; } return pc.CopyImgDataToArray(x_coord,y_coord,width,height); } //+------------------------------------------------------------------+
Here we get the pointer to the copier object by ID. If the object is not found, inform of that and return false. If the pointer to the object is successfully received, return the result of the CopyImgDataToArray() method of the copier object class considered above.
The method copying the part or the entire image from the array to the canvas:
//+------------------------------------------------------------------+ //| Copy the part or the entire image from the array to the canvas | //+------------------------------------------------------------------+ bool CForm::ImagePaste(const int id,const uint x_coord,const uint y_coord) { CPixelCopier *pc=this.GetPixelCopier(id); if(pc==NULL) { ::Print(DFUN,CMessage::Text(MSG_FORM_OBJECT_PC_OBJ_NOT_EXIST_LIST),(string)id); return false; } return pc.CopyImgDataToCanvas(x_coord,y_coord); } //+------------------------------------------------------------------+
The method logic is identical to the one considered above except that now we do not save the area to the array restoring it from the array instead.
All is ready to test the image pixel copier object in action.
Test
Let's make sure the pixel copier object works correctly. The GIF image at the beginning of the article clearly shows how each subsequent image, drawn against the background of the shape object, is superimposed on the previously drawn ones. Now we need to use the pixel copier to first save the background the text is to be superimposed on. Before drawing a new text (visually relocating the drawn text), first restore the background the text is drawn on (overwrite the text), save a part of the image using the new coordinates and display the next text there. This is done to each of the nine displayed texts that are to have different anchor points and displayed at the form sides corresponding to the text anchor points. This way we will be able to check the calculation validity of the saved image part coordinates offsets under the text.
To perform the test, let's use the EA from the previous article and save it to \MQL5\Experts\TestDoEasy\Part78\ as TestDoEasyPart78.mq5.
The EA displays three forms on the chart. The background with the vertical gradient filling is drawn in the bottom-most form. Here I will draw yet another form — the fourth one implementing the horizontal gradient filling in it. The tested texts are to be displayed in this form.
In the area of EA global variables, indicate the need to create four form objects:
//+------------------------------------------------------------------+ //| TestDoEasyPart78.mq5 | //| Copyright 2021, MetaQuotes Ltd. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, MetaQuotes Ltd." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" //--- includes #include <Arrays\ArrayObj.mqh> #include <DoEasy\Services\Select.mqh> #include <DoEasy\Objects\Graph\Form.mqh> //--- defines #define FORMS_TOTAL (4) // Number of created forms //--- input parameters sinput bool InpMovable = true; // Movable forms flag sinput ENUM_INPUT_YES_NO InpUseColorBG = INPUT_YES; // Use chart background color to calculate shadow color sinput color InpColorForm3 = clrCadetBlue; // Third form shadow color (if not background color) //--- global variables CArrayObj list_forms; color array_clr[]; //+------------------------------------------------------------------+
In the OnInit() handler, I will calculate the coordinates of a new form depending on the coordinates of the previous one. There is no need to re-draw the entire chart after creating each subsequent form. Therefore, remove passing to the form update methods in the specified strings (previously, I have explicitly passed true). Now this value is passed at the very end — after creating the last form — in the new code block for creating the fourth form:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Set the permissions to send cursor movement and mouse scroll events ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_MOVE,true); ChartSetInteger(ChartID(),CHART_EVENT_MOUSE_WHEEL,true); //--- Set EA global variables ArrayResize(array_clr,2); array_clr[0]=C'26,100,128'; // Original ≈Dark-azure color array_clr[1]=C'35,133,169'; // Lightened original color //--- Create the specified number of form objects list_forms.Clear(); int total=FORMS_TOTAL; for(int i=0;i<total;i++) { int y=40; if(i>0) { CForm *form_prev=list_forms.At(i-1); if(form_prev==NULL) continue; y=form_prev.BottomEdge()+10; } //--- When creating an object, pass all the required parameters to it CForm *form=new CForm("Form_0"+(string)(i+1),300,y,100,(i<2 ? 70 : 30)); if(form==NULL) continue; //--- Set activity and moveability flags for the form form.SetActive(true); form.SetMovable(false); //--- Set the form ID equal to the loop index and the index in the list of objects form.SetID(i); form.SetNumber(0); // (0 - main form object) Auxiliary objects may be attached to the main one. The main object is able to manage them //--- Set the partial opacity for the middle form and the full one for the rest uchar opacity=(i==1 ? 250 : 255); //--- Set the form style and its color theme depending on the loop index if(i<2) { ENUM_FORM_STYLE style=(ENUM_FORM_STYLE)i; ENUM_COLOR_THEMES theme=(ENUM_COLOR_THEMES)i; //--- Set the form style and theme form.SetFormStyle(style,theme,opacity,true,false); } //--- If this is the first (top) form if(i==0) { //--- Draw a concave field slightly shifted from the center of the form downwards form.DrawFieldStamp(3,10,form.Width()-6,form.Height()-13,form.ColorBackground(),form.Opacity()); form.Update(); } //--- If this is the second form if(i==1) { //--- Draw a concave semi-transparent "tainted glass" field in the center form.DrawFieldStamp(10,10,form.Width()-20,form.Height()-20,clrWheat,200); form.Update(); } //--- If this is the third form if(i==2) { //--- Set the opacity of 200 form.SetOpacity(200); //--- The form background color is set as the first color from the color array form.SetColorBackground(array_clr[0]); //--- Form outlining frame color form.SetColorFrame(clrDarkBlue); //--- Draw the shadow drawing flag form.SetShadow(true); //--- Calculate the shadow color as the chart background color converted to the monochrome one color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100); //--- If the settings specify the usage of the chart background color, replace the monochrome color with 20 units //--- Otherwise, use the color specified in the settings for drawing the shadow color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-20) : InpColorForm3); //--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes //--- Set the shadow opacity to 200, while the blur radius is equal to 4 form.DrawShadow(3,3,clr,200,4); //--- Fill the form background with a vertical gradient form.Erase(array_clr,form.Opacity()); //--- Draw an outlining rectangle at the edges of the form form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity()); //--- Display the text describing the gradient type and update the form form.Text(form.Width()/2,form.Height()/2,TextByLanguage("V-Градиент","V-Gradient"),C'211,233,149',255,TEXT_ANCHOR_CENTER); form.Update(); } //--- If this is the fourth (bottom - tested) form if(i==3) { //--- Set the opacity of 200 form.SetOpacity(200); //--- The form background color is set as the first color from the color array form.SetColorBackground(array_clr[0]); //--- Form outlining frame color form.SetColorFrame(clrDarkBlue); //--- Draw the shadow drawing flag form.SetShadow(true); //--- Calculate the shadow color as the chart background color converted to the monochrome one color clrS=form.ChangeColorSaturation(form.ColorBackground(),-100); //--- If the settings specify the usage of the chart background color, replace the monochrome color with 20 units //--- Otherwise, use the color specified in the settings for drawing the shadow color clr=(InpUseColorBG ? form.ChangeColorLightness(clrS,-20) : InpColorForm3); //--- Draw the form shadow with the right-downwards offset from the form by three pixels along all axes //--- Set the shadow opacity to 200, while the blur radius is equal to 4 form.DrawShadow(3,3,clr,200,4); //--- Fill the form background with a horizontal gradient form.Erase(array_clr,form.Opacity(),false); //--- Draw an outlining rectangle at the edges of the form form.DrawRectangle(0,0,form.Width()-1,form.Height()-1,form.ColorFrame(),form.Opacity()); //--- Display the text describing the gradient type and update the form //--- Specify the text parameters (text coordinates in the center of the form) and the anchor point (located at the center as well) string text=TextByLanguage("H-Градиент","H-Gradient"); int text_x=form.Width()/2; int text_y=form.Height()/2; ENUM_TEXT_ANCHOR anchor=TEXT_ANCHOR_CENTER; //--- Find out the width and height of the outlining text rectangle (to be used as the size of the saved area) int text_w=0,text_h=0; form.TextSize(text,text_w,text_h); //--- Calculate coordinate offsets for the saved area depending on the text anchor point int shift_x=0,shift_y=0; form.TextGetShiftXY(text,anchor,shift_x,shift_y); //--- If a background area with calculated coordinates and size under the future text is successfully saved if(form.ImageCopy(0,text_x+shift_x,text_y+shift_y,text_w,text_h)) { //--- Draw the text and update the form together with redrawing a chart form.Text(text_x,text_y,text,C'211,233,149',255,anchor); form.Update(true); } } //--- Add objects to the list if(!list_forms.Add(form)) { delete form; continue; } } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Each new form creation code string is accompanied by detailed comments here. After creating the form and before drawing a text on it, we need to save the background area the text is to be located on. Later, in another handler, we first restore the form background by overwriting the text with it and display the texts in the new locations preserving the background under them in the same way, and restoring it with each new movement of the text to new coordinates.
All this will be done in the OnChartEvent() handler in the new code block:
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- If clicking on an object if(id==CHARTEVENT_OBJECT_CLICK) { //--- If the clicked object belongs to the EA if(StringFind(sparam,MQLInfoString(MQL_PROGRAM_NAME))==0) { //--- Get the object ID from it int form_id=(int)StringToInteger(StringSubstr(sparam,StringLen(sparam)-1))-1; //--- Find this form object in the loop by all forms created in the EA for(int i=0;i<list_forms.Total();i++) { CForm *form=list_forms.At(i); if(form==NULL) continue; //--- If the clicked object has the ID of 3 and the form has the same ID if(form_id==3 && form.ID()==3) { //--- Set the text parameters string text=TextByLanguage("H-Градиент","H-Gradient"); //--- Get the size of the future text int text_w=0,text_h=0; form.TextSize(text,text_w,text_h); //--- Get the anchor point of the last drawn text (this is the form which contained the last drawn text) ENUM_TEXT_ANCHOR anchor=form.TextAnchor(); //--- Get the coordinates of the last drawn text int text_x=form.TextLastX(); int text_y=form.TextLastY(); //--- Calculate the coordinate offset of the saved rectangle area depending on the text anchor point int shift_x=0,shift_y=0; form.TextGetShiftXY(text,anchor,shift_x,shift_y); //--- Set the text anchor initial point (0 = LEFT_TOP) out of nine possible ones static int n=0; //--- If the previously copied form background image is successfully restored when creating the form object in OnInit() if(form.ImagePaste(0,text_x+shift_x,text_y+shift_y)) { //--- Depending on the n variable, set the new text anchor point switch(n) { case 0 : anchor=TEXT_ANCHOR_LEFT_TOP; text_x=1; text_y=1; break; case 1 : anchor=TEXT_ANCHOR_CENTER_TOP; text_x=form.Width()/2; text_y=1; break; case 2 : anchor=TEXT_ANCHOR_RIGHT_TOP; text_x=form.Width()-2; text_y=1; break; case 3 : anchor=TEXT_ANCHOR_LEFT_CENTER; text_x=1; text_y=form.Height()/2; break; case 4 : anchor=TEXT_ANCHOR_CENTER; text_x=form.Width()/2; text_y=form.Height()/2; break; case 5 : anchor=TEXT_ANCHOR_RIGHT_CENTER; text_x=form.Width()-2; text_y=form.Height()/2; break; case 6 : anchor=TEXT_ANCHOR_LEFT_BOTTOM; text_x=1; text_y=form.Height()-2; break; case 7 : anchor=TEXT_ANCHOR_CENTER_BOTTOM;text_x=form.Width()/2; text_y=form.Height()-2; break; case 8 : anchor=TEXT_ANCHOR_RIGHT_BOTTOM; text_x=form.Width()-2; text_y=form.Height()-2; break; default: anchor=TEXT_ANCHOR_CENTER; text_x=form.Width()/2; text_y=form.Height()/2; break; } //--- According to the new anchor point, get the new offsets of the saved area coordinates form.TextGetShiftXY(text,anchor,shift_x,shift_y); //--- If the background area is successfully saved at new coordinates if(form.ImageCopy(0,text_x+shift_x,text_y+shift_y,text_w,text_h)) { //--- Draw the text in new coordinates and update the form form.Text(text_x,text_y,text,C'211,233,149',255,anchor); form.Update(); } //--- Increase the object click counter (and also the pointer to the text anchor point), //--- and if the value exceeds 8, reset the value to zero (from 0 to 8 = nine anchor points) n++; if(n>8) n=0; } } } } } } //+------------------------------------------------------------------+
Find detailed descriptions in the code comments. If you have any questions, feel free to ask them in the comments below.
Compile the EA and launch it on the chart.
Let's click on the bottom form and make sure that everything works as intended:
What's next?
In the next article, I will continue the development of the animation concept in the library and start working on sprite animation.
All files of the current version of the library are attached below together with the test EA file for MQL5 for you to test and download.
Leave your questions and suggestions in the comments.
*Previous articles within the series:
Graphics in DoEasy Library (part 73): Form object of a graphical element
Graphics in DoEasy Library (part 74): Basic graphical element powered by the CCanvas class
Graphics in DoEasy Library (part 75): Methods of handling primitives and text in the basic graphical element
Graphics in DoEasy Library (part 76): Form object and predefined color themes
Graphics in DoEasy Library (part 77): Shadow object class
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/9612





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