DoEasy 函数库中的图形(第七十九部分):动画框对象类及其衍生对象·进阶篇
🎞️

DoEasy 函数库中的图形(第七十九部分):动画框对象类及其衍生对象·进阶篇

(2/3)·从基类到文本/矩形动画框,解决交互窗动态绘图的重复造轮子问题

实战向进阶 第 2/3 篇
不少人在 MT5 里手动重绘交互窗背景,对象一移动就残留拖影。把背景保存与恢复逻辑写死在脚本里,后续加新造型就得改一片代码。本篇用类继承把这套动作收拢,避免越写越乱。

◍ 矩形锚点偏移量的坐标换算逻辑

在 MT5 自定义图形元素里,矩形相对锚点的位置不能用绝对坐标硬写,得靠锚点枚举配合宽高反推左上角偏移。下面这段代码把 TEXT_ANCHOR 的九种对齐方式拆成了坐标偏移量,是画标签框、信号框时定位的基础。 GetShiftXYbySize 接收 width、height、anchor 以及两个引用参数 shift_x、shift_y。函数体内用 switch 分流:左上为 (0,0),左中为 (0,-height/2),左下为 (0,-height),中上为 (-width/2,0),其余方向以此类推。 实盘里若用 CGCnvElement 批量绘制多周期水平线标注,直接调这个函数就能保证框体不压住 K 线。外汇与贵金属波动剧烈、杠杆风险高,这类界面计算只解决显示问题,不构成任何方向建议。

MQL5 / C++
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 类,改个锚点参数就能看到标签跳动。外汇与贵金属图表叠加文字提示属高风险辅助,偏移算错可能误导进出场判断。

MQL5 / C++
   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 只是元数据。

MQL5 / C++
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 的差值,能直观看到拷贝区域被裁剪的像素量。

MQL5 / C++
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 线缩略图做形态识别;外汇和贵金属波动大、杠杆高,截图识别只作辅助,信号确认仍需人工复核。

MQL5 / C++
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 而不报错;外汇与贵金属图表元素抓取涉及实时重绘,行情跳动下存在渲染竞态,属于高风险操作,参数需按品种波动节奏微调。

MQL5 / C++
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
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到交互窗绘制对象的层级与背景占用,你专注决策而非排查重绘 bug。

常见问题

基类统管位置、尺寸、背景保存恢复,衍生类各自实现文本或矩形绘制,新增造型类型不必动底层,维护成本倾向更低。
造型类型枚举直接映射 CCanvas 的数据访问、图元、填充图元等分组方法,单个框内绘制什么由该枚举决定。
数组单元格可能含无效值,函数通过引用传出极值并用 bool 表示是否有效,调用方需先判断再使用,避免误用 -1 类无效数据。
小布暂不直接编译 mqh 库,但可把交互窗对象列表和背景占用导出为快照,在品种页叠加查看,省去手动打印调试。
基类在对象创建时自动存底层背景,删除或移动时触发恢复,列表类统一管理,恢复动作概率上即时且无残留。