
DoEasy 函数库中的图形(第七十八部分):函数库中的动画原理。 图片切分
内容
概述
图形界面天然地暗含非静态图像的存在。 任何显示的数据(例如,表格中显示的数据)都可能随时间而变化。 GUI 元素可通过各种视觉效果来针对用户的操作做出反应。
我将创建安置各种视觉效果的方法,并赋予函数库处理灵动动画的能力。 动画基于静态图像的连续变化(逐帧)。
CCanvas 类能够在画布上绘制图像。 绘制一系列图像,并保存在数组中,我们可以构建一定的序列,最终成为动态图像。 但如果我们只是按顺序在画布上逐一绘制每幅图像,那么它们会简单地相互重叠,最终导致一堆混乱的像素,如下图所示(这里我只是将文本显示在会话窗对象的不同位置):
为了避免这种情况,我们需要完全擦除之前的图像,然后重新绘制背景,并在其上显示文本(我在之前的一篇文章中曾做过,而在放置文本时,在会话窗章节里讲述过文本锚点方法)。 仅当重绘会话窗的尺寸较小及复杂度较低时,此选项才可行。 另一种选项是将一部分叠加文本的背景保存在内存里(在数组中),然后添加文本。 当需要重新定位到新坐标时,用之前保存在数组里的背景图片覆盖需绘制的文本(恢复背景),并在新位置绘制文本(最初保存的文本所在位置的背景部分已被移走)。 因此,图像叠加位置的背景会不断地存储到内存中,并在需要更改图像时恢复。
这是我在函数库中引入灵动动画概念的最小元素:
- 保存必要坐标处的背景
- 显示坐标处的图像
- 当重绘图像时恢复背景
为实现这一切,我将创建一个小型类来存储图像坐标和尺寸。 在类中也将创建依据这些坐标和尺寸保存背景图像一部分的方法。 此外,我们还需要第二个方法,将保存在数组中的背景存储起来(将背景保存到数组时,尺寸和坐标也会保存在类变量之中)。
为什么我要创建一个类而不是为会话窗对象创建两个这样的方法? 此处的一切都很简单:如果我们只需要显示文本或单个动画图像,那么有这两个方法足矣。 但如果我们需要在会话窗的不同位置显示多个文本,类就更方便了。 每个动画图像都有自己的类实例,能够单独管理。
这种概念允许利用先前绘制的图像作为背景来绘制一些内容 — 我们一并保存背景和绘制的图像,而后可从背景中删除它们。
我将用这个概念来开发在会话窗对象上创建、存储和显示各种灵动动画的类 — 每个类实例都包含一系列可动态添加到列表中,并进行处理的图像。
改进库类
如同往常一样,我们往 \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 }; //+------------------------------------------------------------------+
及与新添加的索引相对应的消息文本:
//--- 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"}, }; //+---------------------------------------------------------------------+
由于我们要绘制的图像和文本,要么在现有的会话窗对象上,要么是从图形元素对象继承而来,或者在自定义程序的任何其他 GUI 对象上,我们需要手头上始终有对象的初始外观,从而令我们可以随时恢复原状。
当然,我们可以重新绘制它,但从一个数组复制到另一个数组会迅速得多。
为此,我将在图形元素对象类的 \MQL5\Include\DoEasy\Objects\Graph\GCnvElement.mqh 中实现一些修改和改进。
在类的受保护部分,声明数组,可在创建初始对象(其外观)后立即包含其所有像素,以及将 CCanvas 类实例的图形资源保存到数组中的方法:
//+------------------------------------------------------------------+ //| 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:
因此,牺牲少量内存,我们即可通过简单地将一个数组复制到另一个数组来快速将程序界面上的任何元素外观恢复到其原始形式。
在类的私密部分,声明两个变量,存储最后绘制文本的 X 和 Y 坐标:
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
在类的公开部分,编写返回指向当前类实例指针的方法,及声明在指定数组中保存图像的方法:
//--- 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[]);
该方法允许类返回自身指针,在需要时将类的指针传递给下面会研讨的像素复制器类,而复制 CCanvas 实例图形资源的方法则必须快速把会话窗外观复制到基于函数库程序中的数组。
在处理文本的方法的代码块中,添加两个返回最后绘制文本的 X 和 Y 坐标的方法:
//+------------------------------------------------------------------+ //| 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
这些方法只返回相应变量的值。
为了令这些数值始终保持相关性,需把传递给方法的参数、坐标,输出至以当前字体显示文本的方法的变量之中:
//--- 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); }
绘制的文本可以有九个锚点:
例如,如果文本锚点位于右下角 (Right|Bottom),那么这将是起始 XY 坐标。 函数库中的所有初始坐标都对应于矩形左上角 (Left|Top)。 因此,如果我们采用初始文本坐标保存图像,文本将位于所保存图像的右下角。 我们无法调整保存的文本叠加位置的背景区域。
因此,我们需要计算文本轮廓矩形坐标的偏移量,需要将该背景位置保存到数组中,以便后续恢复。 未来文本的宽度和高度应预先计算 — 在绘制文本之前。 我们仅需指定文本自身。 CCanvas 类的 TextSize() 方法返回轮廓矩形的宽度和高度。
在类的公开部分,根据文本对齐方法,声明返回 X/Y 偏移量的方法:
//--- 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 | //+------------------------------------------------------------------+
下面将研讨该方法。
在参数型类构造函数中初始化最后绘制文本的坐标:
//+------------------------------------------------------------------+ //| 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); } } //+------------------------------------------------------------------+
在受保护构造函数中以相同的方式初始化变量:
//+------------------------------------------------------------------+ //| 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)) { ...
现在我们来研讨上面所声明方法的实现。
实现将图像保存到数组的方法:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
该方法接收调用它的方法或函数的名称(以便查找可能的错误),以及指向图形资源数据(图像像素)要写入的数组链接。
利用 ResourceReadImage() 函数将 CCanvas 类创建的包含会话窗图像的图形资源的数据读入数组。 如果发生资源读取错误,则发通知,并返回 false。 如果一切顺利,则返回 true。 存储在资源中的所有图像像素都被写入传递给该方法的数组。
该方法将图形资源保存到数组:
//+------------------------------------------------------------------+ //| Save the graphical resource to the array | //+------------------------------------------------------------------+ bool CGCnvElement::ResourceCopy(const string source) { return this.ImageCopy(DFUN,this.m_data_array); } //+------------------------------------------------------------------+
该方法返回调用上述方法的结果。 唯一不同之处在于,图形资源数据会被写入之前就已声明的特殊数组中,它存储了整个会话窗对象的图像副本,而非所传递的数组链接。
该方法返回相对于文本锚点的坐标偏移:
//+------------------------------------------------------------------+ //| 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; } } //+------------------------------------------------------------------+
此处,我们首先获取传递给方法的文本尺寸(尺寸在声明的变量中设置),然后依据传递给方法的锚点方法,简单地计算相对于初始文本坐标,移动 X 和 Y 坐标所需的像素数量。
现在到了改进阴影对象类的时候了。 由于我刚刚添加了读取图形资源的方法,和一个可以存储图形资源副本的常量数组,因此可以从阴影对象类中删除多余的变量、数组和代码模块。
我们来改进 \MQL5\Include\DoEasy\Objects\Graph\ShadowObj.mqh 文件。
从高斯模糊方法中删除数组和不必要的变量:
//+------------------------------------------------------------------+ //| 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
在读取图形资源数据的块中,以调用如上所示方法 来替换代码:
//--- 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;
我将在整个代码中使用图形元素对象类的 Width() 和 Height() 方法,来替代删除的 res_w 和 res_h 变量。 我还将用 m_data_array 数组代替 res_data 数组,现在用它来存储图形资源的副本。
一般来说,所有的改进都归结为用图形元素对象类的方法来替换不必要并删除的变量:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
现在,开发类的一切准备工作就绪。 它的对象将令我们能够管控画布上任何图形元素的绘制,从而令我们以后可以轻松地恢复叠加了新绘图像的背景。 进而,这也允许我创建处理灵动动画的类。
用于复制和粘贴图像部分的类
会话窗对象类是继承层次结构中的最小对象,在其内我们能够操控动画。
由于保存和恢复部分图像的类较小,故我们将其直接放在会话窗对象类文件\MQL5\Include\DoEasy\Objects\Graph\Form.mqh 当中。 我将把这个类命名为 “pixel copier”,它清晰地描述了其目标。
每个像素复制器类对象都有一个自定义 ID,可令我们为正在操控的对象图像进行定义。 可通过其 ID 引用所需的类对象,以便可以单独处理每个动画对象。 例如,如果我们需要同时管理和更改三个图像,其中两个是文本,一个是图像,那么,当为每个图像创建复制器对象时,我们只需为它们分配不同的 ID — text1 = ID0,text2 = ID1,image = ID2。 在这种情况下,每个对象都会存储操控它的所有剩余参数,即:
- 存储图像叠加背景部分的像素数组,
- 叠加图像背景的矩形区域左上角的 X 和 Y 坐标,
- 矩形区域的宽度和高度
- 以及计算出的区域宽度和高度。
我们需要计算出的宽度和高度,以便准确知道矩形复制区域的宽度和高度,以防矩形超出保存会话窗像素的区域。 进而,在还原背景时,我们将不再需要重新计算实际复制的矩形背景区域的宽度和高度,而只需使用存储在对象变量中的已计算数值。
在类的私密部分,声明图形元素对象类的指针(我将它传递给新创建的像素复制器类对象,从而能够使用会话窗的数据,在其中我们已创建了复制器的对象实例),存储会话窗中应保存和恢复图像部分的数组,以及上述所有变量:
//+------------------------------------------------------------------+ //| 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:
在类的公开部分,编写两个复制对象的比较方法、 设置和接收对象属性的方法、类构造函数 — 默认和参数型、并声明两个保存和恢复背景部分的方法:
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){;} }; //+------------------------------------------------------------------+
我们来更详细地研讨这些方法。
该方法比较两个复制器对象:
//--- 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); }
此处的一切都是标准化的,如同其他函数库类一样。 如果比较模式 (mode) 等于 0(默认情况下),则比较当前对象的 ID,和传递给方法的指针所指的那个。 如果当前对象 ID 更大,则返回 1,如果小于则为 -1,如果等于则为 0。 在所有其他情况下(if mode != 0),则返回 -1。 目前,该方法只能比较对象 ID。
在参数型类构造函数的初始化清单中, 把传入参数的数值赋值给所有的类成员变量,而在类主体中,指针值则赋值给指向图形元素对象类的变量。 指针值也在 参数中传递:
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; }
现在新创建的复制对象就会“知道”是哪个对象创建了它,并可以访问它的方法和参数。
该方法将部分或全部图像复制到数组中:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
代码中已有每个方法的详解。 简而言之,如果复制区域的初始坐标在会话窗之外,则没有什么可复制的 — 返回 false。 如果复制区域的初始坐标与会话窗坐标相配,而复制区域的宽度和高度为零,或与会话窗宽度和高度相配,则复制整个会话窗图像。 如果仅有图片的一部分需要保存,首先计算需复制的宽度和高度,如此即可令其不超出会话窗,并复制所有落入复制区域的会话窗图像像素。
该方法从数组复制部分或整个图像至画布:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
方法逻辑在代码注释中业已详述。 与保存一部分图像的方法不同,我们无需在此计算复制区域的坐标和尺寸,因为在第一个方法操作完毕后,它们都已保存在类变量当中。 在此,我们只需将恢复区域的每一行按高度循环逐个像素复制到画布上,如此即可恢复前面方法保存的那部分图像。
现在,我们需要安排从会话窗对象类访问新编写的类。
由于我将动态创建所需数量的复制器对象,因此需要在会话窗对象类中声明该类对象的列表。 每个新创建的复制器对象都被添加到列表中,我们能够从中获取指向所需对象的指针,并操控它们。
在类的私密部分声明以下清单:
//+------------------------------------------------------------------+ //| 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
由于我们不能拥有多个相似 ID 的复制器对象,因此我们需要该方法返回列表中含有指定 ID 对象存在的标志。 我们来声明该方法:
//--- 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:
在类的公开部分,编写返回指向当前会话窗指针的方法,和返回复制器对象列表的方法:
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; }
接下来,声明创建新图像像素复制器对象的方法:
//--- 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);
在简化访问对象属性的方法代码块之前,添加操控图像像素的代码块:
//+------------------------------------------------------------------+ //| 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); //+------------------------------------------------------------------+
在类主体之外实现所声明的方法。
该方法返回标志,指示列表中含有指定 ID 的复制器对象是否存在:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
在此,我们在一个简单的循环中遍历复制器对象列表,获取下一个对象。 如果它的 ID 等于传递给方法的 ID,则返回 true。 直至循环完毕,返回 false。
该方法创建一个新的图像像素复制器对象:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
整个方法逻辑在代码的注释中均有讲述。 如果您有任何疑问,请随时在下面的评论中提问。
依据 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; } //+------------------------------------------------------------------+
这里的一切都很简单。 循环遍历复制器对象列表,获取指向下一个对象的指针。 如果其 ID 与所需的 ID 匹配,则返回指针。 循环完成后,则返回 NULL — 在列表中找不到指定 ID 的对象。
该方法将部分或全部图像复制到数组中:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
在此,我们依据 ID 得到了指向复制器对象的指针。 如果未找到该对象,则发通报,并返回 false。 如果成功收到指向对象的指针,调用上面研讨的复制器对象类的 CopyImgDataToArray() 方法,并返回结果。
该方法从数组复制部分或整个图像至画布:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
方法逻辑除了现在我们不将区域保存到数组中,而是从数组中恢复它,其余与上面研讨的雷同。
测试图像像素复制器对象的一切准备就绪。
测试
我们来确保像素复制器对象能够正常工作。 文章开头的 GIF 图像能够清晰地显示,以造型对象为背景绘制的每个后续图像,如何叠加在先前已绘制的图像上。 现在,我们需要用像素复制器先保存文本将要叠加的背景。 在绘制新文本之前(视觉上重新定位已绘制文本),首先要恢复绘制文本前的背景(覆盖文本),用新坐标保存图像的一部分,并在该处显示下一个文本。 文本可有九个不同的显示锚点,并可能处于会话窗的不同边缘,故针对它们中的每一个执行此操作显示在相应锚点。 以这种方式,我们就可以检查计算出的文本之下需保存图像部分坐标偏移的有效性。
为了执行测试,我们借用 来自上一篇文章中的 EA,将其保存到 \MQL5\Experts\TestDoEasy\Part78\,命名为 TestDoEasyPart78.mq5。
EA 在图表上显示三个会话窗。 在最底部的会话窗里绘制的是填充了垂直渐变色的背景。 在此,我将绘制另一个会话窗 — 这第四个,将在其中实现填充水平渐变色。 测试文本会显示在这个会话窗里。
在 EA 全局变量区域,指明需创建四个会话窗表单对象:
//+------------------------------------------------------------------+ //| 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[]; //+------------------------------------------------------------------+
在 OnInit() 处理程序中,我将根据前一个会话窗的坐标计算新会话窗的坐标。 每个后续会话窗创建后,无需重新绘制整个图表。 因此,会话窗更新方法的指定代码中,删除所传递参数(以前,我曾显式地传递了 true)。 现在,这个值移至创建第四个会话窗的新代码模块的末尾传递 — 在创建最后一个会话窗之后:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
每个新的会话窗创建代码都附有详细注释。 创建会话窗之后,在其上绘制文本之前,我们需要保存文本所在区域的背景。 稍后,在另一个处理程序中,我们首先通过覆盖文本来恢复会话窗背景,并以相同的方式在预留背景的新位置显示文本,并随着文本每次移到新坐标,恢复原坐标处背景。
所有这一切都在新代码块中的 OnChartEvent() 处理程序里完成:
//+------------------------------------------------------------------+ //| 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; } } } } } } //+------------------------------------------------------------------+
请在代码注释中查找详细说明。 如果您有任何疑问,请随时在下面的评论中提问。
编译 EA,并在图表上启动它。
我们单击底部会话窗,并确保一切按预期工作:
下一步是什么?
在下一篇文章中,我将继续开发函数库中的动画概念,并开始研究灵动动画。
以下是该函数库当前版本的所有文件,以及 MQL5 的测试 EA 文件,供您测试和下载。
请您在评论中留下问题和建议。
*该系列的前几篇文章:
DoEasy 函数库中的图形(第七十三部分):图形元素的交互窗对象
DoEasy 函数库中的图形(第七十四部分):由 CCanvas 类提供强力支持的基本图形元素
DoEasy 函数库中的图形(第七十五部分):处理基本图形元素图元和文本的方法
DoEasy 函数库中的图形(第七十六部分):会话窗对象和预定义的颜色主题
DoEasy 函数库中的图形(第七十七部分):阴影对象类
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/9612

