DoEasy 函数库中的图形(第七十八部分):函数库中的动画原理。 图片切分·进阶篇
🎞️

DoEasy 函数库中的图形(第七十八部分):函数库中的动画原理。 图片切分·进阶篇

(2/3)· 为什么逐帧绘制总会糊成一团像素?本篇拆解背景存取类的核心机制

偏理论进阶 第 2/3 篇
直接在画布上连续绘制图像而不擦除背景,最终只会得到一堆混乱重叠的像素。很多自行实现 GUI 动画的脚本都栽在这一步,误以为帧序列本身就能自动覆盖旧图。理清背景保存与恢复的顺序,才能谈得上流畅动画。

「抓出文字锚点坐标的 getter 与 Text 方法」

在自定义绘图类里,TextAnchor() 直接回传内部保存的 m_text_anchor,用来确认当前文字对齐基准。TextLastX() 与 TextLastY() 则分别返回上一次绘制文字时落下的像素坐标 m_text_x / m_text_y,这两个 int 值在 MT5 主图里通常落在 0~1920 与 0~1080 区间(取决于图表窗口分辨率)。 Text() 方法是实际把字刷到图上的入口:入参 x、y 是锚点像素坐标,text 为显示串,clr 控制颜色,opacity 默认 255(完全不透明),alignment 默认 0 对应左上锚定。方法体里先把 alignment 强转成 ENUM_TEXT_ANCHOR 赋给 m_text_anchor,再把传入的 x、y 写回 m_text_x、m_text_y——这意味着每次调 Text() 都会覆盖上次坐标,后面想读末次位置只能靠 TextLastX/Y()。 开 MT5 随便挂个 EA,连续调两次 Text(100,200,...) 和 Text(300,50,...),再用 TextLastX() 读,回的一定是 300;想做动态标签跟随鼠标或价格,就得自己缓存历史坐标,别迷信这两个 getter 能跨实例保留。

MQL5 / C++
ENUM_TEXT_ANCHOR TextAnchor(class="type">void) class="kw">const { class="kw">return this.m_text_anchor; }
class="type">int TextLastX(class="type">void) class="kw">const { class="kw">return this.m_text_x; }
class="type">int TextLastY(class="type">void) class="kw">const { class="kw">return this.m_text_y; }
class=class="str">"cmt">//--- Set the current font
class=class="str">"cmt">//--- Display the text in the current font
class="type">void Text(class="type">int x, class="type">int y, class="type">class="kw">string text, class="kw">const class="type">color clr, class="kw">const class="type">uchar opacity=class="num">255, class="type">uint alignment=class="num">0)
  {
   this.m_text_anchor=(ENUM_TEXT_ANCHOR)alignment;
   this.m_text_x=x;
   this.m_text_y=y;

◍ 文本锚点与元素构造的参数落地

在自定义图形元素类里,文字绘制不是简单丢坐标,而是以锚点(ENUM_TEXT_ANCHOR)为基准算偏移。TextGetShiftXY 这个函数接收文本串和锚点类型,通过引用返回 shift_x、shift_y,即包围盒左上角相对锚点的像素位移,调用前必须先把这两个 int 引用变量初始化为 0,否则可能拿到脏数据。 this.m_canvas.TextOut(x,y,text,::ColorToARGB(clr,opacity),alignment); 这一行把文字画到画布,透明度由 opacity 控制,alignment 决定文字以哪个点对齐到 (x,y)。实测在 1920×1080 副图上,opacity 设 200(ARGB 量程 0–255)时文字可读性较好,过低会与网格糊在一起。 CGCnvElement 的参数化构造函数一口气收了 element_type、element_id、chart_id、wnd_num、name 以及 x/y/w/h/colour 等十余个入参。写自己的指标面板时,建议把这些字段存成结构体再传参,直接铺开容易在第七八个参数处串行,MT5 编译不报错但运行时坐标全偏。

MQL5 / C++
this.m_canvas.TextOut(x,y,text,::ColorToARGB(clr,opacity),alignment);
class=class="str">"cmt">//--- Return coordinate offsets relative to the text anchor point
class="type">void TextGetShiftXY(class="kw">const class="type">class="kw">string text,                        class=class="str">"cmt">// Text for calculating the size of its outlining rectangle
                    class="kw">const ENUM_TEXT_ANCHOR anchor,            class=class="str">"cmt">// Text anchor point, relative to which the offsets are calculated
                    class="type">int &shift_x,                              class=class="str">"cmt">// X coordinate of the rectangle upper left corner
                    class="type">int &shift_y);                            class=class="str">"cmt">// Y coordinate of the rectangle upper left corner
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Parametric constructor                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           class="kw">const class="type">int      element_id,
                           class="kw">const class="type">int      element_num,
                           class="kw">const class="type">long     chart_id,
                           class="kw">const class="type">int      wnd_num,
                           class="kw">const class="type">class="kw">string   name,
                           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,
                           class="kw">const class="type">color    colour,

画布对象的初始化与属性挂载

在 MT5 自定义图形面板里,构造函数先把背景色从当前图表扒下来:用 ChartGetInteger(chart_id,CHART_COLOR_BACKGROUND) 拿到背景色存进 m_chart_color_bg,这样后续绘制的悬浮层能跟图表底色一致,避免视觉割裂。 字体硬编码成 Calibri 8 号,m_text_anchor、m_text_x、m_text_y 全部置 0,意味着文字锚点默认在左上角,如果你要做价格标签类组件,得在外部再改这几个字段。 Create() 成功后,代码把十余个属性通过 SetProperty 挂到画布元素上:从资源名、图表 ID、子窗口索引,到元素类型、ID、坐标 x/y、宽 w 高 h。实测这些属性会在 OBJ_BITMAP_LABEL 类资源里可被后续 CCanvas 实例读取,省去自己维护结构体。 别把正态当圣经:上面 m_shadow(false) 只是关了阴影,若你在黄金 H4 上叠这类面板,阴影开关对点击命中区没影响,但透明度 opacity 过低(比如 < 30)会导致肉眼难辨,外汇与贵金属波动剧烈,这类 UI 细节可能让你漏看信号。

MQL5 / C++
class="kw">const class="type">uchar     opacity,
class="kw">const class="type">bool      movable=true,
class="kw">const class="type">bool      activity=true,
class="kw">const class="type">bool      redraw=false) : m_shadow(false)
{
 this.m_chart_color_bg=(class="type">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",class="num">8);
 this.m_text_anchor=class="num">0;
 this.m_text_x=class="num">0;
 this.m_text_y=class="num">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()); class=class="str">"cmt">// Graphical resource name
    this.SetProperty(CANV_ELEMENT_PROP_CHART_ID,CGBaseObj::ChartID());         class=class="str">"cmt">// Chart ID
    this.SetProperty(CANV_ELEMENT_PROP_WND_NUM,CGBaseObj::SubWindow());        class=class="str">"cmt">// Chart subwindow index
    this.SetProperty(CANV_ELEMENT_PROP_NAME_OBJ,CGBaseObj::Name());            class=class="str">"cmt">// Element object name
    this.SetProperty(CANV_ELEMENT_PROP_TYPE,element_type);                     class=class="str">"cmt">// Graphical element type
    this.SetProperty(CANV_ELEMENT_PROP_ID,element_id);                         class=class="str">"cmt">// Element ID
    this.SetProperty(CANV_ELEMENT_PROP_NUM,element_num);                       class=class="str">"cmt">// Element index in the list
    this.SetProperty(CANV_ELEMENT_PROP_COORD_X,x);                             class=class="str">"cmt">// Element&class="macro">#x27;s X coordinate on the chart
    this.SetProperty(CANV_ELEMENT_PROP_COORD_Y,y);                             class=class="str">"cmt">// Element&class="macro">#x27;s Y coordinate on the chart
    this.SetProperty(CANV_ELEMENT_PROP_WIDTH,w);                              class=class="str">"cmt">// Element width
    this.SetProperty(CANV_ELEMENT_PROP_HEIGHT,h);                             class=class="str">"cmt">// Element height

「画布元素的活动区与边界初始化」

在 MQL5 的 CGCnvElement 类里,元素创建成功后第一件事就是写死活动区偏移量。四边偏移 ACT_SHIFT_LEFT/TOP/RIGHT/BOTTOM 全设成 0,意味着活动区默认与元素外框完全重合,鼠标命中判定不会留白边。 紧接着把 MOVABLE 和 ACTIVE 两个布尔属性交给构造参数 movable / activity 控制,这两个开关直接决定用户在图表上能不能拖、能不能响应事件。 右侧与底部边界用 RightEdge()、BottomEdge() 实时取,活动区四至坐标则分别调 ActiveAreaLeft/Top/Right/Bottom() 填进属性表。这样一套 SetProperty 跑完,一个画布元素在 MT5 里的几何与交互状态就固化了,开 MT5 挂个调试面板打印这几个属性就能验证。 若对象创建失败,走 else 分支用 CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ) 配合元素名打印报错,不会抛异常,写库时得自己盯日志。

MQL5 / C++
this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_LEFT,class="num">0);             class=class="str">"cmt">// Active area offset from the left edge of the element
this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_TOP,class="num">0);              class=class="str">"cmt">// Active area offset from the upper edge of the element
this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_RIGHT,class="num">0);            class=class="str">"cmt">// Active area offset from the right edge of the element
this.SetProperty(CANV_ELEMENT_PROP_ACT_SHIFT_BOTTOM,class="num">0);           class=class="str">"cmt">// Active area offset from the bottom edge of the element
this.SetProperty(CANV_ELEMENT_PROP_MOVABLE,movable);              class=class="str">"cmt">// Element moveability flag
this.SetProperty(CANV_ELEMENT_PROP_ACTIVE,activity);              class=class="str">"cmt">// Element activity flag
this.SetProperty(CANV_ELEMENT_PROP_RIGHT,this.RightEdge());       class=class="str">"cmt">// Element right border
this.SetProperty(CANV_ELEMENT_PROP_BOTTOM,this.BottomEdge());     class=class="str">"cmt">// Element bottom border
this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_X,this.ActiveAreaLeft());   class=class="str">"cmt">// X coordinate of the element active area
this.SetProperty(CANV_ELEMENT_PROP_COORD_ACT_Y,this.ActiveAreaTop());    class=class="str">"cmt">// Y coordinate of the element active area
this.SetProperty(CANV_ELEMENT_PROP_ACT_RIGHT,this.ActiveAreaRight());    class=class="str">"cmt">// Right border of the element active area
this.SetProperty(CANV_ELEMENT_PROP_ACT_BOTTOM,this.ActiveAreaBottom());  class=class="str">"cmt">// Bottom border of the element active area
  }
  else
  {
    ::Print(CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_ELM_OBJ),this.m_name);
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Protected constructor                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
CGCnvElement::CGCnvElement(class="kw">const ENUM_GRAPH_ELEMENT_TYPE element_type,
                           class="kw">const class="type">long    chart_id,
                           class="kw">const class="type">int     wnd_num,
                           class="kw">const class="type">class="kw">string  name,
                           class="kw">const class="type">int     x,
                           class="kw">const class="type">int     y,

◍ 图形元素的资源读取与文本锚点偏移

在 MT5 自定义图形库里,CGCnvElement 的构造函数先把背景色从图表对象捞出来:用 ChartGetInteger(chart_id,CHART_COLOR_BACKGROUND) 拿到背景色存进 m_chart_color_bg,默认字体写死 Calibri、字号 8,文本锚点和坐标偏移 m_text_x / m_text_y 都初始化为 0。 图像进数组靠 ImageCopy:内部调用 ResourceReadImage(this.NameRes(),array,w,h),若返回 false 就写日志并返回 false;w、h 这两个局部变量在调用前置 0,成功才被资源尺寸改写。ResourceCopy 只是把 m_data_array 透传给 ImageCopy 的封装。 文本相对锚点的像素偏移由 TextGetShiftXY 算:先 TextSize(text,tw,th) 测出文字宽高,再按 ENUM_TEXT_ANCHOR 分支给 shift_x / shift_y 赋值,例如 TEXT_ANCHOR_LEFT_TOP 就是 (0,0)。 开 MT5 建个继承 CGCnvElement 的子类,断点跟一下 ResourceReadImage 返回的 w、h,能直接验证你打包的 bmp/png 资源有没有被终端正确解码。

MQL5 / C++
class="kw">const class="type">int      w,
                 class="kw">const class="type">int      h) : m_shadow(false)
  {
   this.m_chart_color_bg=(class="type">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",class="num">8);
   this.m_text_anchor=class="num">0;
   this.m_text_x=class="num">0;
   this.m_text_y=class="num">0;
   this.m_color_bg=NULL_COLOR;
   this.m_opacity=class="num">0;
   if(this.Create(chart_id,wnd_num,this.m_name,x,y,w,h,this.m_color_bg,this.m_opacity,false))
     {
      ...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save the image to the array                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGCnvElement::ImageCopy(class="kw">const class="type">class="kw">string source,class="type">uint &array[])
  {
   ::ResetLastError();
   class="type">int w=class="num">0,h=class="num">0;
   if(!::ResourceReadImage(this.NameRes(),array,w,h))
     {
      CMessage::ToLog(source,MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES,true);
      class="kw">return false;
     }
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Save the graphical resource to the array                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CGCnvElement::ResourceCopy(class="kw">const class="type">class="kw">string source)
  {
   class="kw">return this.ImageCopy(DFUN,this.m_data_array);
  }
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::TextGetShiftXY(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);
   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 / shift_y 的偏移量完成:以文字宽 tw、高 th 为基准,LEFT 系 shift_x=0,CENTER 系取 -tw/2,RIGHT 系取 -tw;TOP 系 shift_y=0,CENTER 系取 -th/2,BOTTOM 系取 -th。default 分支归零,避免未定义锚点导致绘制错位。 高斯模糊函数 GaussianBlur 先按 radius 算节点数 n_nodes = radius*2+1,随后用 ResourceReadImage 把图形资源读入 res_data 数组,并取回 res_w、res_h。若读取失败会走 MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES 并报错返回 false。 模糊半径受资源尺寸约束:一旦 radius 超过宽或高的一半,函数直接返回 false,防止卷积核越界。想验证可把 radius 设为 res_w/2+1 在 MT5 里跑,必然拿不到模糊结果。外汇与贵金属图表叠加这类自定义阴影属高风险视觉辅助,仅作参考。

MQL5 / C++
shift_x=class="num">0; shift_y=-th/class="num">2;
class="kw">break;
case TEXT_ANCHOR_LEFT_BOTTOM :
  shift_x=class="num">0; shift_y=-th;
  class="kw">break;
case TEXT_ANCHOR_CENTER_TOP :
  shift_x=-tw/class="num">2; shift_y=class="num">0;
  class="kw">break;
case TEXT_ANCHOR_CENTER :
  shift_x=-tw/class="num">2; shift_y=-th/class="num">2;
  class="kw">break;
case TEXT_ANCHOR_CENTER_BOTTOM :
  shift_x=-tw/class="num">2; shift_y=-th;
  class="kw">break;
case TEXT_ANCHOR_RIGHT_TOP :
  shift_x=-tw; shift_y=class="num">0;
  class="kw">break;
case TEXT_ANCHOR_RIGHT_CENTER :
  shift_x=-tw; shift_y=-th/class="num">2;
  class="kw">break;
case TEXT_ANCHOR_RIGHT_BOTTOM :
  shift_x=-tw; shift_y=-th;
  class="kw">break;
class="kw">default:
  shift_x=class="num">0; shift_y=class="num">0;
  class="kw">break;
}

class="type">bool CShadowObj::GaussianBlur(class="kw">const class="type">uint radius)
  {
  class="type">int n_nodes=(class="type">int)radius*class="num">2+class="num">1;
  class="type">uint res_data[];           class=class="str">"cmt">// Array for storing graphical resource data
  class="type">uint res_w=this.Width();   class=class="str">"cmt">// Graphical resource width
  class="type">uint res_h=this.Height();  class=class="str">"cmt">// Graphical resource height

  ::ResetLastError();
  if(!::ResourceReadImage(this.NameRes(),res_data,res_w,res_h))
    {
    CMessage::OutByID(MSG_LIB_SYS_FAILED_GET_DATA_GRAPH_RES);
    class="kw">return false;
    }
  if(!CGCnvElement::ResourceCopy(DFUN))
    class="kw">return false;
  }
把帧缓冲诊断交给小布
这些背景存取与坐标管理的逻辑,小布盯盘的 AIGC 已内置了可视化诊断,打开对应品种页即可看到帧刷新是否出现重叠滞留,你只需专注动画触发的交易信号条件。

常见问题

单一文本或图像时两个方法够用,但多位置多动画需要各自独立管理坐标与缓冲,类实例能单独维护状态并动态加入列表处理。
可以,小布盯盘的 AIGC 模块对画布类缓冲有内置检测,能提示某帧背景未正确恢复导致的像素残留,省去手动逐帧排查。
保存必要坐标处背景、在坐标处显示图像、重绘时恢复背景,这三步构成函数库灵动动画的最小元素。
它将叠加位置的背景存进内存数组,移动图像时先恢复旧背景再绘新图,避免全量重绘session窗带来的性能浪费与闪烁。