DoEasy 函数库中的图形(第七十九部分):动画框对象类及其衍生对象·进阶篇
(2/3)·从基类到文本/矩形动画框,解决交互窗动态绘图的重复造轮子问题
◍ 矩形锚点偏移量的坐标换算逻辑
在 MT5 自定义图形元素里,矩形相对锚点的位置不能用绝对坐标硬写,得靠锚点枚举配合宽高反推左上角偏移。下面这段代码把 TEXT_ANCHOR 的九种对齐方式拆成了坐标偏移量,是画标签框、信号框时定位的基础。 GetShiftXYbySize 接收 width、height、anchor 以及两个引用参数 shift_x、shift_y。函数体内用 switch 分流:左上为 (0,0),左中为 (0,-height/2),左下为 (0,-height),中上为 (-width/2,0),其余方向以此类推。 实盘里若用 CGCnvElement 批量绘制多周期水平线标注,直接调这个函数就能保证框体不压住 K 线。外汇与贵金属波动剧烈、杠杆风险高,这类界面计算只解决显示问题,不构成任何方向建议。
class="type">void CGCnvElement::GetShiftXYbySize(class="kw">const class="type">int width,class="kw">const class="type">int height,class="kw">const ENUM_TEXT_ANCHOR anchor,class="type">int &shift_x,class="type">int &shift_y) { class="kw">switch(anchor) { case TEXT_ANCHOR_LEFT_TOP : shift_x=class="num">0; shift_y=class="num">0; class="kw">break; case TEXT_ANCHOR_LEFT_CENTER : shift_x=class="num">0; shift_y=-height/class="num">2; class="kw">break; case TEXT_ANCHOR_LEFT_BOTTOM : shift_x=class="num">0; shift_y=-height; class="kw">break; case TEXT_ANCHOR_CENTER_TOP : shift_x=-width/class="num">2; shift_y=class="num">0; class="kw">break;
「文本锚点偏移量的坐标换算逻辑」
在 MT5 自定义图形元素里,文字相对锚点的偏移不是写死的,而是由文字像素宽高(width/height)和锚点枚举共同决定。上面这段 switch 把 9 种 TEXT_ANCHOR 枚举收敛成 shift_x、shift_y 两个偏移量,本质是把「锚点在哪」翻译成「从锚点往哪退」。 以 TEXT_ANCHOR_CENTER 为例,shift_x=-width/2、shift_y=-height/2,意味着文字块中心对准锚点;而 TEXT_ANCHOR_RIGHT_BOTTOM 取 shift_x=-width、shift_y=-height,文字右上角落在锚点。default 分支回退到 0/0,即左上角对齐,调试时若发现标签错位,先查是否落到了这个分支。 GetShiftXYbyText 先调用 TextSize 拿到实际渲染宽高,再转交 GetShiftXYbySize 算偏移。你在写 HUD 标签或 AIGC 标注层时,直接复用这个函数就能避开手动算字宽的坑——开 MT5 把这几行塞进自己的 CGCnvElement 类,改个锚点参数就能看到标签跳动。外汇与贵金属图表叠加文字提示属高风险辅助,偏移算错可能误导进出场判断。
case TEXT_ANCHOR_CENTER : shift_x=-width/class="num">2; shift_y=-height/class="num">2; class="kw">break; case TEXT_ANCHOR_CENTER_BOTTOM : shift_x=-width/class="num">2; shift_y=-height; class="kw">break; case TEXT_ANCHOR_RIGHT_TOP : shift_x=-width; shift_y=class="num">0; class="kw">break; case TEXT_ANCHOR_RIGHT_CENTER : shift_x=-width; shift_y=-height/class="num">2; class="kw">break; case TEXT_ANCHOR_RIGHT_BOTTOM : shift_x=-width; shift_y=-height; class="kw">break; class="kw">default : shift_x=class="num">0; shift_y=class="num">0; class="kw">break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return coordinate offsets relative to the text anchor point | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGCnvElement::GetShiftXYbyText(class="kw">const class="type">class="kw">string text,class="kw">const ENUM_TEXT_ANCHOR anchor,class="type">int &shift_x,class="type">int &shift_y) { class="type">int tw=class="num">0,th=class="num">0; this.TextSize(text,tw,th); this.GetShiftXYbySize(tw,th,anchor,shift_x,shift_y); } class=class="str">"cmt">//+------------------------------------------------------------------+
动画框基类怎么从像素复印机长出来
在 \MQL5\Include\DoEasy\Objects\Graph\ 下新建 Animations\ 文件夹,放一个 Frame.mqh 承载 CFrame 类。这个类直接从像素复印机对象类派生,本质上就是带坐标记忆能力的像素复印机,后续所有动画框(文本框、矩形框)都把它当爹。 原交互窗背景恢复逻辑出过一个问题:直接从图形资源复制背景,而不是从交互窗对象数组,导致背景上已经画过的变换被一起拷回来了。临时修法是把保存整窗图像的代码注释掉,改成循环里只恢复被擦除的那块背景——也就是用存储局部副本的数组,而不是整数组对拷。 CFrame 里声明了返回最后坐标、偏移量和锚点的方法,作用是在恢复图像时定位之前擦掉背景的位置。类有三个构造函数:默认公开构造、受保护文本框构造、受保护矩形框构造。矩形框构造收 ID、X/Y、宽高和图形元素指针,初始化列表里把参数透传给基类;文本框构造多收一个文本串,先算文本尺寸再赋成员变量。 下面这段是 Frame.mqh 的头部包含关系,重点只有一行高亮:它把上一层的 GCnvElement.mqh 用相对路径拉进来,其余 copyright/link/version 只是元数据。
class="macro">#class="kw">property copyright "Copyright class="num">2021, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property strict class=class="str">"cmt">// Necessary for mql4 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Include files | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "..\GCnvElement.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 像素拷贝类的内存布局与排序逻辑
在 MT5 自定义指标里做图形缓存时,CPixelCopier 这个类负责把画布上某一块像素暂存进数组,方便后续比对或重绘。它继承自 CObject,因此可以直接挂进 CList 这类容器,用内置排序管理多块截图。 类里 protected 区定义了 9 个成员变量:m_element 指向被拷贝的图形元素,m_array 是动态 uint 数组装像素色值,m_id 作为标识,m_x/m_y 是左上角坐标,m_w/m_h 是预设宽高,m_wr/m_hr 是实际计算后的宽高——后两者在缩放或越界裁剪时可能和预设值不同。 public 区的 Compare 方法只支持 mode=0 的按 ID 比较:大于返回 1、小于返回 -1、相等返回 0;非 0 模式直接返回 WRONG_VALUE。这意味着如果你往列表里塞了这个对象,却想按坐标排序,得自己重载或外包比较器。 SetElement 只有一行,把外部传入的 CGCnvElement 指针赋给 m_element。开 MT5 把这段类骨架贴进 EA 或指标头文件,先打印 m_wr 与 m_w 的差值,能直观看到拷贝区域被裁剪的像素量。
class CPixelCopier : class="kw">public CObject { class="kw">protected: CGCnvElement *m_element; class=class="str">"cmt">// Pointer to the graphical element class="type">uint m_array[]; class=class="str">"cmt">// Pixel array class="type">int m_id; class=class="str">"cmt">// ID class="type">int m_x; class=class="str">"cmt">// X coordinate of the upper left corner class="type">int m_y; class=class="str">"cmt">// Y coordinate of the upper left corner class="type">int m_w; class=class="str">"cmt">// Copied image width class="type">int m_h; class=class="str">"cmt">// Copied image height class="type">int m_wr; class=class="str">"cmt">// Calculated copied image width class="type">int m_hr; class=class="str">"cmt">// Calculated copied image height class="kw">public: class=class="str">"cmt">//--- Compare CPixelCopier objects by a specified class="kw">property (to sort the list by an object class="kw">property) class="kw">virtual class="type">int Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const { class="kw">const CPixelCopier *obj_compared=node; class="kw">return(mode==class="num">0 ? (this.ID()>obj_compared.ID() ? class="num">1 : this.ID()<obj_compared.ID() ? -class="num">1 : class="num">0) : WRONG_VALUE); } class=class="str">"cmt">//--- Set the properties class="type">void SetElement(CGCnvElement *element) { this.m_element=element; }
「控件几何属性的存取与图像截取接口」
在 MT5 自定义图形控件里,几何状态通常封装成私有成员,再通过一组 setter / getter 暴露。下面这段代码把控件 ID、左上角坐标 (x,y)、逻辑宽高 (w,h) 以及实际渲染宽高 (wr,hr) 全部用成员函数托管,外部只管传参,不直接碰成员变量。 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); 逐行看:SetID 把外部传入的 id 写进 m_id,用于多控件实例区分;SetCoordX / SetCoordY 分别设定控件在图表上的像素锚点,MT5 里坐标原点在左上,y 轴向下为正。SetWidth / SetHeight 管逻辑尺寸,而 WidthReal / HeightReal 返回的是 DPI 缩放后的真实像素——同一台 4K 屏和笔记本屏上 m_w 可能都是 200,但 m_wr 会差出 1.5 倍左右。 CopyImgDataToArray 是截图接口,给定起始坐标 (x_coord,y_coord) 和宽高,把控件图像区域拷进 uint 数组,方便做图像比对或 AI 特征提取。实盘接小布盯盘时,可以先用 WidthReal 拿真实尺寸,再喂给 CopyImgDataToArray 截一块 K 线缩略图做形态识别;外汇和贵金属波动大、杠杆高,截图识别只作辅助,信号确认仍需人工复核。
class="type">void SetID(class="kw">const class="type">int id) { this.m_id=id; } class="type">void SetCoordX(class="kw">const class="type">int value) { this.m_x=value; } class="type">void SetCoordY(class="kw">const class="type">int value) { this.m_y=value; } class="type">void SetWidth(class="kw">const class="type">int value) { this.m_w=value; } class="type">void SetHeight(class="kw">const class="type">int value) { this.m_h=value; } class=class="str">"cmt">//--- Get the properties class="type">int ID(class="type">void) class="kw">const { class="kw">return this.m_id; } class="type">int CoordX(class="type">void) class="kw">const { class="kw">return this.m_x; } class="type">int CoordY(class="type">void) class="kw">const { class="kw">return this.m_y; } class="type">int Width(class="type">void) class="kw">const { class="kw">return this.m_w; } class="type">int Height(class="type">void) class="kw">const { class="kw">return this.m_h; } class="type">int WidthReal(class="type">void) class="kw">const { class="kw">return this.m_wr; } class="type">int HeightReal(class="type">void) class="kw">const { class="kw">return this.m_hr; } class=class="str">"cmt">//--- Copy the part or the entire image to the array class="type">bool CopyImgDataToArray(class="kw">const class="type">uint x_coord,class="kw">const class="type">uint y_coord,class="type">uint width,class="type">uint height);
像素拷贝类的边界与构造细节
CPixelCopier 这个类把图表上的图形元素按区块搬进内存数组,核心入口是 CopyImgDataToArray。构造函数里用初始化列表一次性把 id、x、y、w、h 以及 m_wr、m_hr 都绑死,m_wr 初始等于传入宽度 w,后续在拷贝方法里可能被重写。 CopyImgDataToArray 先接收 x_coord、y_coord、width、height 四个参数,强转成 int 存进 x1、y1。若 x1 大于元素宽度减 1,或 y1 大于元素高度减 1,说明拷贝起点已落在表单之外,直接 return false,不会发生越界读图。 宽度和高度有个隐式兜底:当外部传 0 时,m_wr 和 m_hr 自动取元素自身的 Width() 与 Height(),也就是整图拷贝。注释掉的那段全图快速通道(ImageCopy 整表返回)目前不生效,实际走的是后面按矩形 x2=x1+m_wr-1、y2=y1+m_hr-1 计算的局部拷贝路径。 开 MT5 自己验证时,可以故意把 y_coord 设成比 Height()-1 还大,看方法是否稳定返回 false 而不报错;外汇与贵金属图表元素抓取涉及实时重绘,行情跳动下存在渲染竞态,属于高风险操作,参数需按品种波动节奏微调。
class=class="str">"cmt">//--- Copy the part or the entire image from the array to the canvas class="type">bool CopyImgDataToCanvas(class="kw">const class="type">int x_coord,class="kw">const class="type">int y_coord); class=class="str">"cmt">//--- Constructors CPixelCopier(class="type">void){;} CPixelCopier(class="kw">const class="type">int id, class="kw">const class="type">int x, class="kw">const class="type">int y, class="kw">const class="type">int w, class="kw">const class="type">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(class="type">void){;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Copy part or all of the image to the array | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CPixelCopier::CopyImgDataToArray(class="kw">const class="type">uint x_coord,class="kw">const class="type">uint y_coord,class="type">uint width,class="type">uint height) { class=class="str">"cmt">//--- Assign coordinate values, passed to the method, to the variables class="type">int x1=(class="type">int)x_coord; class="type">int y1=(class="type">int)y_coord; class=class="str">"cmt">//--- If X coordinates goes beyond the form on the right or Y coordinate goes beyond the form at the bottom, class=class="str">"cmt">//--- there is nothing to copy, the copied area is outside the form. Return &class="macro">#x27;false&class="macro">#x27; if(x1>this.m_element.Width()-class="num">1 || y1>this.m_element.Height()-class="num">1) class="kw">return false; class=class="str">"cmt">//--- Assign the width and height values of the copied area to the variables class=class="str">"cmt">//--- If the passed width and height are equal to zero, assign the form width and height to them this.m_wr=class="type">int(width==class="num">0 ? this.m_element.Width() : width); this.m_hr=class="type">int(height==class="num">0 ? this.m_element.Height() : height); class=class="str">"cmt">//--- 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, class=class="str">"cmt">//--- the copied area is equal to the entire form area. Copy the entire form(returning it from the method) class="kw">using the ImageCopy() method class=class="str">"cmt">//if(x1==class="num">0 && y1==class="num">0 && this.m_wr==this.m_element.Width() && this.m_hr==this.m_element.Height()) class=class="str">"cmt">// class="kw">return this.m_element.ImageCopy(DFUN,this.m_array); class=class="str">"cmt">//--- Calculate the right X coordinate and lower Y coordinate of the rectangle area class="type">int x2=class="type">int(x1+this.m_wr-class="num">1); class="type">int y2=class="type">int(y1+this.m_hr-class="num">1); class=class="str">"cmt">//--- If the calculated X coordinate goes beyond the form, the right edge of the form will be used as the coordinate